220 lines
5.8 KiB
Plaintext
220 lines
5.8 KiB
Plaintext
/**
|
|
* 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
|
|
|
|
'use strict';
|
|
|
|
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,
|
|
};
|