UNPKG

12.6 kBJavaScriptView Raw
1import { cosmiconfig, defaultLoaders } from 'cosmiconfig';
2import { TypeScriptLoader } from 'cosmiconfig-typescript-loader';
3import { resolve } from 'path';
4import { DetailedError, createProfiler, createNoopProfiler, getCachedDocumentNodeFromSchema, } from '@graphql-codegen/plugin-helpers';
5import { env } from 'string-env-interpolation';
6import yargs from 'yargs';
7import { findAndLoadGraphQLConfig } from './graphql-config.js';
8import { loadSchema, loadDocuments, defaultSchemaLoadOptions, defaultDocumentsLoadOptions } from './load.js';
9import { print } from 'graphql';
10import yaml from 'yaml';
11import { createRequire } from 'module';
12import { promises } from 'fs';
13import { createHash } from 'crypto';
14const { lstat } = promises;
15export function generateSearchPlaces(moduleName) {
16 const extensions = ['json', 'yaml', 'yml', 'js', 'ts', 'config.js'];
17 // gives codegen.json...
18 const regular = extensions.map(ext => `${moduleName}.${ext}`);
19 // gives .codegenrc.json... but no .codegenrc.config.js
20 const dot = extensions.filter(ext => ext !== 'config.js').map(ext => `.${moduleName}rc.${ext}`);
21 return [...regular.concat(dot), 'package.json'];
22}
23function customLoader(ext) {
24 function loader(filepath, content) {
25 if (typeof process !== 'undefined' && 'env' in process) {
26 content = env(content);
27 }
28 if (ext === 'json') {
29 return defaultLoaders['.json'](filepath, content);
30 }
31 if (ext === 'yaml') {
32 try {
33 const result = yaml.parse(content, { prettyErrors: true, merge: true });
34 return result;
35 }
36 catch (error) {
37 error.message = `YAML Error in ${filepath}:\n${error.message}`;
38 throw error;
39 }
40 }
41 if (ext === 'js') {
42 return defaultLoaders['.js'](filepath, content);
43 }
44 if (ext === 'ts') {
45 return TypeScriptLoader()(filepath, content);
46 }
47 }
48 return loader;
49}
50export async function loadCodegenConfig({ configFilePath, moduleName, searchPlaces: additionalSearchPlaces, packageProp, loaders: customLoaders, }) {
51 configFilePath = configFilePath || process.cwd();
52 moduleName = moduleName || 'codegen';
53 packageProp = packageProp || moduleName;
54 const cosmi = cosmiconfig(moduleName, {
55 searchPlaces: generateSearchPlaces(moduleName).concat(additionalSearchPlaces || []),
56 packageProp,
57 loaders: {
58 '.json': customLoader('json'),
59 '.yaml': customLoader('yaml'),
60 '.yml': customLoader('yaml'),
61 '.js': customLoader('js'),
62 '.ts': customLoader('ts'),
63 noExt: customLoader('yaml'),
64 ...customLoaders,
65 },
66 });
67 const pathStats = await lstat(configFilePath);
68 return pathStats.isDirectory() ? cosmi.search(configFilePath) : cosmi.load(configFilePath);
69}
70export async function loadContext(configFilePath) {
71 const graphqlConfig = await findAndLoadGraphQLConfig(configFilePath);
72 if (graphqlConfig) {
73 return new CodegenContext({
74 graphqlConfig,
75 });
76 }
77 const result = await loadCodegenConfig({ configFilePath });
78 if (!result) {
79 if (configFilePath) {
80 throw new DetailedError(`Config ${configFilePath} does not exist`, `
81 Config ${configFilePath} does not exist.
82
83 $ graphql-codegen --config ${configFilePath}
84
85 Please make sure the --config points to a correct file.
86 `);
87 }
88 throw new DetailedError(`Unable to find Codegen config file!`, `
89 Please make sure that you have a configuration file under the current directory!
90 `);
91 }
92 if (result.isEmpty) {
93 throw new DetailedError(`Found Codegen config file but it was empty!`, `
94 Please make sure that you have a valid configuration file under the current directory!
95 `);
96 }
97 return new CodegenContext({
98 filepath: result.filepath,
99 config: result.config,
100 });
101}
102function getCustomConfigPath(cliFlags) {
103 const configFile = cliFlags.config;
104 return configFile ? resolve(process.cwd(), configFile) : null;
105}
106export function buildOptions() {
107 return {
108 c: {
109 alias: 'config',
110 type: 'string',
111 describe: 'Path to GraphQL codegen YAML config file, defaults to "codegen.yml" on the current directory',
112 },
113 w: {
114 alias: 'watch',
115 describe: 'Watch for changes and execute generation automatically. You can also specify a glob expression for custom watch list.',
116 coerce: (watch) => {
117 if (watch === 'false') {
118 return false;
119 }
120 if (typeof watch === 'string' || Array.isArray(watch)) {
121 return watch;
122 }
123 return !!watch;
124 },
125 },
126 r: {
127 alias: 'require',
128 describe: 'Loads specific require.extensions before running the codegen and reading the configuration',
129 type: 'array',
130 default: [],
131 },
132 o: {
133 alias: 'overwrite',
134 describe: 'Overwrites existing files',
135 type: 'boolean',
136 },
137 s: {
138 alias: 'silent',
139 describe: 'Suppresses printing errors',
140 type: 'boolean',
141 },
142 e: {
143 alias: 'errors-only',
144 describe: 'Only print errors',
145 type: 'boolean',
146 },
147 profile: {
148 describe: 'Use profiler to measure performance',
149 type: 'boolean',
150 },
151 p: {
152 alias: 'project',
153 describe: 'Name of a project in GraphQL Config',
154 type: 'string',
155 },
156 v: {
157 alias: 'verbose',
158 describe: 'output more detailed information about performed tasks',
159 type: 'boolean',
160 default: false,
161 },
162 d: {
163 alias: 'debug',
164 describe: 'Print debug logs to stdout',
165 type: 'boolean',
166 default: false,
167 },
168 };
169}
170export function parseArgv(argv = process.argv) {
171 return yargs(argv).options(buildOptions()).parse(argv);
172}
173export async function createContext(cliFlags = parseArgv(process.argv)) {
174 if (cliFlags.require && cliFlags.require.length > 0) {
175 const relativeRequire = createRequire(process.cwd());
176 await Promise.all(cliFlags.require.map(mod => import(relativeRequire.resolve(mod, {
177 paths: [process.cwd()],
178 }))));
179 }
180 const customConfigPath = getCustomConfigPath(cliFlags);
181 const context = await loadContext(customConfigPath);
182 updateContextWithCliFlags(context, cliFlags);
183 return context;
184}
185export function updateContextWithCliFlags(context, cliFlags) {
186 const config = {
187 configFilePath: context.filepath,
188 };
189 if (cliFlags.watch !== undefined) {
190 config.watch = cliFlags.watch;
191 }
192 if (cliFlags.overwrite === true) {
193 config.overwrite = cliFlags.overwrite;
194 }
195 if (cliFlags.silent === true) {
196 config.silent = cliFlags.silent;
197 }
198 if (cliFlags.verbose === true || process.env.VERBOSE) {
199 config.verbose = true;
200 }
201 if (cliFlags.debug === true || process.env.DEBUG) {
202 config.debug = true;
203 }
204 if (cliFlags.errorsOnly === true) {
205 config.errorsOnly = cliFlags.errorsOnly;
206 }
207 if (cliFlags['ignore-no-documents'] !== undefined) {
208 // for some reason parsed value is `'false'` string so this ensure it always is a boolean.
209 config.ignoreNoDocuments = cliFlags['ignore-no-documents'] === true;
210 }
211 if (cliFlags['emit-legacy-common-js-imports'] !== undefined) {
212 // for some reason parsed value is `'false'` string so this ensure it always is a boolean.
213 config.emitLegacyCommonJSImports = cliFlags['emit-legacy-common-js-imports'] === true;
214 }
215 if (cliFlags.project) {
216 context.useProject(cliFlags.project);
217 }
218 if (cliFlags.profile === true) {
219 context.useProfiler();
220 }
221 if (cliFlags.check === true) {
222 context.enableCheckMode();
223 }
224 context.updateConfig(config);
225}
226export class CodegenContext {
227 constructor({ config, graphqlConfig, filepath, }) {
228 this._checkMode = false;
229 this._pluginContext = {};
230 this.checkModeStaleFiles = [];
231 this._config = config;
232 this._graphqlConfig = graphqlConfig;
233 this.filepath = this._graphqlConfig ? this._graphqlConfig.filepath : filepath;
234 this.cwd = this._graphqlConfig ? this._graphqlConfig.dirpath : process.cwd();
235 this.profiler = createNoopProfiler();
236 }
237 useProject(name) {
238 this._project = name;
239 }
240 getConfig(extraConfig) {
241 if (!this.config) {
242 if (this._graphqlConfig) {
243 const project = this._graphqlConfig.getProject(this._project);
244 this.config = {
245 ...project.extension('codegen'),
246 schema: project.schema,
247 documents: project.documents,
248 pluginContext: this._pluginContext,
249 };
250 }
251 else {
252 this.config = { ...this._config, pluginContext: this._pluginContext };
253 }
254 }
255 return {
256 ...extraConfig,
257 ...this.config,
258 };
259 }
260 updateConfig(config) {
261 this.config = {
262 ...this.getConfig(),
263 ...config,
264 };
265 }
266 enableCheckMode() {
267 this._checkMode = true;
268 }
269 get checkMode() {
270 return this._checkMode;
271 }
272 useProfiler() {
273 this.profiler = createProfiler();
274 const now = new Date(); // 2011-10-05T14:48:00.000Z
275 const datetime = now.toISOString().split('.')[0]; // 2011-10-05T14:48:00
276 const datetimeNormalized = datetime.replace(/-|:/g, ''); // 20111005T144800
277 this.profilerOutput = `codegen-${datetimeNormalized}.json`;
278 }
279 getPluginContext() {
280 return this._pluginContext;
281 }
282 async loadSchema(pointer) {
283 const config = this.getConfig(defaultSchemaLoadOptions);
284 if (this._graphqlConfig) {
285 // TODO: SchemaWithLoader won't work here
286 return addHashToSchema(this._graphqlConfig.getProject(this._project).loadSchema(pointer, 'GraphQLSchema', config));
287 }
288 return addHashToSchema(loadSchema(pointer, config));
289 }
290 async loadDocuments(pointer) {
291 const config = this.getConfig(defaultDocumentsLoadOptions);
292 if (this._graphqlConfig) {
293 // TODO: pointer won't work here
294 return addHashToDocumentFiles(this._graphqlConfig.getProject(this._project).loadDocuments(pointer, config));
295 }
296 return addHashToDocumentFiles(loadDocuments(pointer, config));
297 }
298}
299export function ensureContext(input) {
300 return input instanceof CodegenContext ? input : new CodegenContext({ config: input });
301}
302function hashContent(content) {
303 return createHash('sha256').update(content).digest('hex');
304}
305function hashSchema(schema) {
306 return hashContent(print(getCachedDocumentNodeFromSchema(schema)));
307}
308function addHashToSchema(schemaPromise) {
309 return schemaPromise.then(schema => {
310 // It's consumed later on. The general purpose is to use it for caching.
311 if (!schema.extensions) {
312 schema.extensions = {};
313 }
314 schema.extensions['hash'] = hashSchema(schema);
315 return schema;
316 });
317}
318function hashDocument(doc) {
319 if (doc.rawSDL) {
320 return hashContent(doc.rawSDL);
321 }
322 if (doc.document) {
323 return hashContent(print(doc.document));
324 }
325 return null;
326}
327function addHashToDocumentFiles(documentFilesPromise) {
328 return documentFilesPromise.then(documentFiles => documentFiles.map(doc => {
329 doc.hash = hashDocument(doc);
330 return doc;
331 }));
332}
333export function shouldEmitLegacyCommonJSImports(config, outputPath) {
334 const globalValue = config.emitLegacyCommonJSImports === undefined ? true : !!config.emitLegacyCommonJSImports;
335 // const outputConfig = config.generates[outputPath];
336 // if (!outputConfig) {
337 // debugLog(`Couldn't find a config of ${outputPath}`);
338 // return globalValue;
339 // }
340 // if (isConfiguredOutput(outputConfig) && typeof outputConfig.emitLegacyCommonJSImports === 'boolean') {
341 // return outputConfig.emitLegacyCommonJSImports;
342 // }
343 return globalValue;
344}