UNPKG

@ardatan/relay-compiler

Version:

A compiler tool for building GraphQL-driven applications.

368 lines (337 loc) 11.4 kB
/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow * @format */ // flowlint ambiguous-object-type:error 'use strict'; const ASTConvert = require('../core/ASTConvert'); const CodegenDirectory = require('./CodegenDirectory'); const CompilerContext = require('../core/CompilerContext'); const Profiler = require('../core/GraphQLCompilerProfiler'); const RelayParser = require('../core/RelayParser'); const compileRelayArtifacts = require('./compileRelayArtifacts'); const graphql = require('graphql'); const invariant = require('invariant'); const md5 = require('../util/md5'); const nullthrows = require('nullthrows'); const path = require('path'); const writeRelayGeneratedFile = require('./writeRelayGeneratedFile'); const { getReaderSourceDefinitionName, } = require('../core/GraphQLDerivedFromMetadata'); const {isExecutableDefinitionAST} = require('../core/SchemaUtils'); const {Map: ImmutableMap} = require('immutable'); import type {Schema} from '../core/Schema'; import type { FormatModule, PluginInterface, TypeGenerator, } from '../language/RelayLanguagePluginInterface'; import type {ScalarTypeMapping} from '../language/javascript/RelayFlowTypeTransformers'; import type {Reporter} from '../reporters/Reporter'; import type {Filesystem} from './CodegenDirectory'; import type {SourceControl} from './SourceControl'; import type {RelayCompilerTransforms} from './compileRelayArtifacts'; import type {DocumentNode, ValidationContext} from 'graphql'; import type {RequestParameters} from 'relay-runtime'; export type GenerateExtraFiles = ( getOutputDirectory: (path?: string) => CodegenDirectory, compilerContext: CompilerContext, getGeneratedDirectory: (definitionName: string) => CodegenDirectory, ) => void; export type ValidationRule = (context: ValidationContext) => any; export type WriterConfig = { baseDir: string, compilerTransforms: RelayCompilerTransforms, customScalars: ScalarTypeMapping, formatModule: FormatModule, generateExtraFiles?: GenerateExtraFiles, optionalInputFieldsForFlow: $ReadOnlyArray<string>, outputDir?: ?string, generatedDirectories?: $ReadOnlyArray<string>, persistQuery?: ?(text: string) => Promise<string>, schemaExtensions: $ReadOnlyArray<string>, noFutureProofEnums: boolean, useHaste: boolean, extension: string, typeGenerator: TypeGenerator, // Haste style module that exports flow types for GraphQL enums. // TODO(T22422153) support non-haste environments enumsHasteModule?: string, printModuleDependency?: string => string, filesystem?: Filesystem, repersist?: boolean, writeQueryParameters?: ( outputDirectory: CodegenDirectory, filename: string, moduleName: string, requestParams: RequestParameters, ) => void, ... }; function compileAll({ baseDir, baseDocuments, schema, compilerTransforms, documents, reporter, typeGenerator, }: {| baseDir: string, baseDocuments: $ReadOnlyArray<DocumentNode>, schema: Schema, compilerTransforms: RelayCompilerTransforms, documents: $ReadOnlyArray<DocumentNode>, reporter: Reporter, typeGenerator: TypeGenerator, |}) { const definitions = ASTConvert.convertASTDocumentsWithBase( schema, baseDocuments, documents, RelayParser.transform, ); const compilerContext = new CompilerContext(schema).addAll(definitions); const transformedTypeContext = compilerContext.applyTransforms( typeGenerator.transforms, reporter, ); const transformedQueryContext = compilerContext.applyTransforms( [ ...compilerTransforms.commonTransforms, ...compilerTransforms.queryTransforms, ], reporter, ); const artifacts = compileRelayArtifacts( compilerContext, compilerTransforms, reporter, ); return { artifacts, definitions, transformedQueryContext, transformedTypeContext, }; } function writeAll({ config: writerConfig, onlyValidate, baseDocuments, documents, schema, reporter, sourceControl, languagePlugin, }: {| config: WriterConfig, onlyValidate: boolean, // $FlowFixMe[value-as-type] baseDocuments: ImmutableMap<string, DocumentNode>, // $FlowFixMe[value-as-type] documents: ImmutableMap<string, DocumentNode>, schema: Schema, reporter: Reporter, sourceControl: ?SourceControl, languagePlugin?: ?PluginInterface, |}): Promise<Map<string, CodegenDirectory>> { return Profiler.asyncContext('RelayFileWriter.writeAll', async () => { const { artifacts: artifactsWithBase, transformedTypeContext, transformedQueryContext, } = compileAll({ schema, baseDir: writerConfig.baseDir, baseDocuments: baseDocuments.valueSeq().toArray(), compilerTransforms: writerConfig.compilerTransforms, documents: documents.valueSeq().toArray(), reporter, typeGenerator: writerConfig.typeGenerator, }); // Build a context from all the documents const baseDefinitionNames = new Set(); baseDocuments.forEach(doc => { doc.definitions.forEach(def => { if (isExecutableDefinitionAST(def) && def.name) { baseDefinitionNames.add(def.name.value); } }); }); // remove nodes that are present in the base or that derive from nodes // in the base const artifacts = artifactsWithBase.filter(([_definition, node]) => { const sourceName = getReaderSourceDefinitionName(node); return !baseDefinitionNames.has(sourceName); }); const artifactMap = new Map( artifacts.map(([_definition, node]) => [ node.kind === 'Request' ? node.params.name : node.name, node, ]), ); const definitionsMeta = new Map(); const getDefinitionMeta = (definitionName: string) => { const artifact = nullthrows(artifactMap.get(definitionName)); const sourceName = getReaderSourceDefinitionName(artifact); const definitionMeta = definitionsMeta.get(sourceName); invariant( definitionMeta, 'RelayFileWriter: Could not determine source for definition: `%s`.', definitionName, ); return definitionMeta; }; documents.forEach((doc, filePath) => { doc.definitions.forEach(def => { if (def.name) { definitionsMeta.set(def.name.value, { dir: path.join(writerConfig.baseDir, path.dirname(filePath)), ast: def, }); } }); }); const allOutputDirectories: Map<string, CodegenDirectory> = new Map(); const addCodegenDir = dirPath => { const codegenDir = new CodegenDirectory(dirPath, { onlyValidate: onlyValidate, filesystem: writerConfig.filesystem, }); allOutputDirectories.set(dirPath, codegenDir); return codegenDir; }; for (const existingDirectory of writerConfig.generatedDirectories || []) { addCodegenDir(existingDirectory); } let configOutputDirectory; if (writerConfig.outputDir) { configOutputDirectory = addCodegenDir(writerConfig.outputDir); } const getGeneratedDirectory = definitionName => { if (configOutputDirectory) { return configOutputDirectory; } const generatedPath = path.join( getDefinitionMeta(definitionName).dir, '__generated__', ); let cachedDir = allOutputDirectories.get(generatedPath); if (!cachedDir) { cachedDir = addCodegenDir(generatedPath); } return cachedDir; }; const formatModule = Profiler.instrument( writerConfig.formatModule, 'RelayFileWriter:formatModule', ); const persistQuery = writerConfig.persistQuery ? Profiler.instrumentWait( writerConfig.persistQuery, 'RelayFileWriter:persistQuery', ) : null; try { await Promise.all( artifacts.map(async ([definition, node]) => { const nodeName = node.kind === 'Request' ? node.params.name : node.name; if (baseDefinitionNames.has(nodeName)) { // don't add definitions that were part of base context return; } const typeNode = transformedTypeContext.get(nodeName); const typeText = typeNode ? writerConfig.typeGenerator.generate( schema, (typeNode: $FlowFixMe), { customScalars: writerConfig.customScalars, enumsHasteModule: writerConfig.enumsHasteModule, optionalInputFields: writerConfig.optionalInputFieldsForFlow, useHaste: writerConfig.useHaste, useSingleArtifactDirectory: !!writerConfig.outputDir, noFutureProofEnums: writerConfig.noFutureProofEnums, normalizationIR: definition.kind === 'Request' ? definition.root : undefined, }, ) : ''; const sourceHash = Profiler.run('hashGraphQL', () => md5(graphql.print(getDefinitionMeta(nodeName).ast)), ); await writeRelayGeneratedFile( schema, getGeneratedDirectory(nodeName), definition, node, formatModule, typeText, persistQuery, sourceHash, writerConfig.extension, writerConfig.printModuleDependency, writerConfig.repersist ?? false, writerConfig.writeQueryParameters ?? function noop() {}, languagePlugin, ); }), ); const generateExtraFiles = writerConfig.generateExtraFiles; if (generateExtraFiles) { Profiler.run('RelayFileWriter:generateExtraFiles', () => { const configDirectory = writerConfig.outputDir; generateExtraFiles( dir => { const outputDirectory = dir || configDirectory; invariant( outputDirectory, 'RelayFileWriter: cannot generate extra files without specifying ' + 'an outputDir in the config or passing it in.', ); let outputDir = allOutputDirectories.get(outputDirectory); if (!outputDir) { outputDir = addCodegenDir(outputDirectory); } return outputDir; }, transformedQueryContext, getGeneratedDirectory, ); }); } allOutputDirectories.forEach(dir => { dir.deleteExtraFiles(languagePlugin?.keepExtraFile); }); if (sourceControl && !onlyValidate) { await CodegenDirectory.sourceControlAddRemove( sourceControl, Array.from(allOutputDirectories.values()), ); } } catch (error) { let details; try { details = JSON.parse(error.message); } catch (_) {} // eslint-disable-line lint/no-unused-catch-bindings if (details && details.name === 'GraphQL2Exception' && details.message) { throw new Error('GraphQL error writing modules:\n' + details.message); } throw new Error( 'Error writing modules:\n' + String(error.stack || error), ); } return allOutputDirectories; }); } module.exports = { writeAll, };