@ardatan/relay-compiler
Version:
A compiler tool for building GraphQL-driven applications.
220 lines (190 loc) • 5.94 kB
Flow
/**
* 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.
*
* @format
* @flow strict-local
*/
// flowlint ambiguous-object-type:error
;
const crypto = require('crypto');
const nullthrows = require('nullthrows');
const {getName} = require('./GraphQLASTUtils');
import type CodegenDirectory, {Filesystem} from '../codegen/CodegenDirectory';
import type {SourceChanges} from './Sources';
import type {ExecutableDefinitionNode} from 'graphql';
type Filename = string;
type NodeName = string;
type FileSha1Hex = string;
type ArtifactFiles = Set<Filename>;
type ArtifactsMetadata = Map<Filename, FileSha1Hex>;
export type ArtifactMap = {|
// Map nodeName to the set of artifacts for this node
+artifacts: Map<NodeName, ArtifactFiles>,
// Artifacts metadata (for now it's just sha1 of the file)
+metadata: ArtifactsMetadata,
|};
export type ArtifactState = ArtifactMap;
export type SerializedArtifactState = $ReadOnlyArray<
[string, $ReadOnlyArray<[string, string]>],
>;
function createEmptyState(): ArtifactState {
return {
artifacts: new Map(),
metadata: new Map(),
};
}
function serializeState(state: ArtifactState): SerializedArtifactState {
const json = [];
for (const [name, artifacts] of state.artifacts) {
json.push([
name,
Array.from(artifacts).map(filename => {
return [filename, state.metadata.get(filename) ?? ''];
}),
]);
}
return json;
}
function deserializeState(json: SerializedArtifactState): ArtifactState {
const metadata = new Map();
const artifacts = new Map();
json.forEach(([name, artifactArray]) => {
const artifactsFiles = new Set();
artifactArray.forEach(([filename, sha1hex]) => {
artifactsFiles.add(filename);
metadata.set(filename, sha1hex);
});
artifacts.set(name, artifactsFiles);
});
return {
artifacts,
metadata,
};
}
function updateState(
state: ArtifactState,
changes: SourceChanges<ExecutableDefinitionNode>,
generatedArtifacts: ArtifactMap,
filesystem: Filesystem,
resolveFullPath: (relativeFilePath: string) => string,
): ArtifactState {
const nextState = {
artifacts: new Map(state.artifacts),
metadata: new Map(state.metadata),
};
const deletionCandidates = new Set();
const addedNames = new Set();
for (const {ast} of changes.added) {
addedNames.add(getName(ast));
}
// For every removed AST node, delete the generated artifacts tracked for that
// node, unless the AST node was also added when the file was moved or the
// AST changed which shows up as added and removed in changes.
for (const {ast} of changes.removed) {
const name = getName(ast);
if (addedNames.has(name)) {
// Update, we deal with that when iterating the added nodes.
continue;
}
const entry = nextState.artifacts.get(name);
if (entry == null) {
// No existing artifacts to delete
continue;
}
for (const outdatedFile of entry.keys()) {
deletionCandidates.add(outdatedFile);
}
nextState.artifacts.delete(name);
}
for (const [name, artifacts] of generatedArtifacts.artifacts) {
const oldEntry = nextState.artifacts.get(name);
if (oldEntry != null) {
for (const outdatedFile of oldEntry) {
if (!artifacts.has(outdatedFile)) {
deletionCandidates.add(outdatedFile);
}
}
}
nextState.artifacts.set(name, artifacts);
for (const filename of artifacts.keys()) {
nextState.metadata.set(
filename,
generatedArtifacts.metadata.get(filename) ?? '',
);
}
}
if (deletionCandidates.size === 0) {
return nextState;
}
const nextGeneratedArtifacts = new Set();
for (const [, artifact] of eachNameAndArtifact(nextState)) {
nextGeneratedArtifacts.add(artifact);
}
for (const candidate of deletionCandidates) {
const someoneElseArtifact = nextGeneratedArtifacts.has(candidate);
if (someoneElseArtifact) {
// Sometimes, there are artifacts that are generated by multiple files
// If this candidate is also generated by someone else in
// artifact map, we just skip it here
continue;
}
const candidatePath = resolveFullPath(candidate);
if (filesystem.existsSync(candidatePath)) {
filesystem.unlinkSync(candidatePath);
nextState.metadata.delete(candidate);
}
}
return nextState;
}
function producedFiles(
dirs: $ReadOnlyArray<{|
baseDir: string,
dir: CodegenDirectory,
|}>,
artifactsMetadata: ArtifactsMetadata,
): ArtifactsMetadata {
const result = new Map();
dirs.forEach(({baseDir, dir}) => {
const {deleted, updated, created, unchanged} = dir.changes;
if (deleted.length > 0) {
throw new Error('Did not expect to see a deletion entry here.');
}
[...updated, ...created].forEach(filename => {
const name = dir.getPath(filename).substr(baseDir.length + 1);
const sha1hex = sha1(nullthrows(dir.read(filename)));
result.set(name, sha1hex);
});
unchanged.forEach(filename => {
const name = dir.getPath(filename).substr(baseDir.length + 1);
const sha1hex = artifactsMetadata.get(name);
result.set(name, sha1hex ?? sha1(nullthrows(dir.read(filename))));
});
});
return result;
}
function* eachNameAndArtifact(
artifacts: ArtifactState,
): Iterator<[string, string]> {
for (const [name, artifactsForSource] of artifacts.artifacts) {
for (const artifactFile of artifactsForSource.keys()) {
yield [name, artifactFile];
}
}
}
function sha1(content: string): string {
return crypto
.createHash('sha1')
.update(content)
.digest('hex');
}
module.exports = {
createEmptyState,
serializeState,
deserializeState,
updateState,
producedFiles,
eachNameAndArtifact,
};