1 | import { cosmiconfig, defaultLoaders } from 'cosmiconfig';
|
2 | import { TypeScriptLoader } from 'cosmiconfig-typescript-loader';
|
3 | import { resolve } from 'path';
|
4 | import { DetailedError, createProfiler, createNoopProfiler, getCachedDocumentNodeFromSchema, } from '@graphql-codegen/plugin-helpers';
|
5 | import { env } from 'string-env-interpolation';
|
6 | import yargs from 'yargs';
|
7 | import { findAndLoadGraphQLConfig } from './graphql-config.js';
|
8 | import { loadSchema, loadDocuments, defaultSchemaLoadOptions, defaultDocumentsLoadOptions } from './load.js';
|
9 | import { print } from 'graphql';
|
10 | import yaml from 'yaml';
|
11 | import { createRequire } from 'module';
|
12 | import { promises } from 'fs';
|
13 | import { createHash } from 'crypto';
|
14 | const { lstat } = promises;
|
15 | export function generateSearchPlaces(moduleName) {
|
16 | const extensions = ['json', 'yaml', 'yml', 'js', 'ts', 'config.js'];
|
17 |
|
18 | const regular = extensions.map(ext => `${moduleName}.${ext}`);
|
19 |
|
20 | const dot = extensions.filter(ext => ext !== 'config.js').map(ext => `.${moduleName}rc.${ext}`);
|
21 | return [...regular.concat(dot), 'package.json'];
|
22 | }
|
23 | function 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 | }
|
50 | export 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 | }
|
70 | export 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 | }
|
102 | function getCustomConfigPath(cliFlags) {
|
103 | const configFile = cliFlags.config;
|
104 | return configFile ? resolve(process.cwd(), configFile) : null;
|
105 | }
|
106 | export 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 | }
|
170 | export function parseArgv(argv = process.argv) {
|
171 | return yargs(argv).options(buildOptions()).parse(argv);
|
172 | }
|
173 | export 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 | }
|
185 | export 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 |
|
209 | config.ignoreNoDocuments = cliFlags['ignore-no-documents'] === true;
|
210 | }
|
211 | if (cliFlags['emit-legacy-common-js-imports'] !== undefined) {
|
212 |
|
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 | }
|
226 | export 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();
|
275 | const datetime = now.toISOString().split('.')[0];
|
276 | const datetimeNormalized = datetime.replace(/-|:/g, '');
|
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 |
|
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 |
|
294 | return addHashToDocumentFiles(this._graphqlConfig.getProject(this._project).loadDocuments(pointer, config));
|
295 | }
|
296 | return addHashToDocumentFiles(loadDocuments(pointer, config));
|
297 | }
|
298 | }
|
299 | export function ensureContext(input) {
|
300 | return input instanceof CodegenContext ? input : new CodegenContext({ config: input });
|
301 | }
|
302 | function hashContent(content) {
|
303 | return createHash('sha256').update(content).digest('hex');
|
304 | }
|
305 | function hashSchema(schema) {
|
306 | return hashContent(print(getCachedDocumentNodeFromSchema(schema)));
|
307 | }
|
308 | function addHashToSchema(schemaPromise) {
|
309 | return schemaPromise.then(schema => {
|
310 |
|
311 | if (!schema.extensions) {
|
312 | schema.extensions = {};
|
313 | }
|
314 | schema.extensions['hash'] = hashSchema(schema);
|
315 | return schema;
|
316 | });
|
317 | }
|
318 | function 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 | }
|
327 | function addHashToDocumentFiles(documentFilesPromise) {
|
328 | return documentFilesPromise.then(documentFiles => documentFiles.map(doc => {
|
329 | doc.hash = hashDocument(doc);
|
330 | return doc;
|
331 | }));
|
332 | }
|
333 | export function shouldEmitLegacyCommonJSImports(config, outputPath) {
|
334 | const globalValue = config.emitLegacyCommonJSImports === undefined ? true : !!config.emitLegacyCommonJSImports;
|
335 |
|
336 |
|
337 |
|
338 |
|
339 |
|
340 |
|
341 |
|
342 |
|
343 | return globalValue;
|
344 | }
|