/**
* 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
*/
'use strict';
const Bundler = require('./Bundler');
const DeltaBundler = require('./DeltaBundler');
const ResourceNotFoundError = require('./IncrementalBundler/ResourceNotFoundError');
const crypto = require('crypto');
const fs = require('fs');
const getGraphId = require('./lib/getGraphId');
const getPrependedScripts = require('./lib/getPrependedScripts');
const path = require('path');
const transformHelpers = require('./lib/transformHelpers');
import type {Options as DeltaBundlerOptions} from './DeltaBundler/types.flow';
import type {DeltaResult, Module, Graph} from './DeltaBundler';
import type {GraphId} from './lib/getGraphId';
import type {TransformInputOptions} from './lib/transformHelpers';
import type {ConfigT} from 'metro-config/src/configTypes.flow';
export opaque type RevisionId: string = string;
export type OutputGraph = Graph<>;
type OtherOptions = {|
+onProgress: $PropertyType<DeltaBundlerOptions<>, 'onProgress'>,
|};
export type GraphRevision = {|
// Identifies the last computed revision.
+id: RevisionId,
+date: Date,
+graphId: GraphId,
+graph: OutputGraph,
+prepend: $ReadOnlyArray<Module<>>,
|};
function createRevisionId(): RevisionId {
return crypto.randomBytes(8).toString('hex');
}
function revisionIdFromString(str: string): RevisionId {
return str;
}
class IncrementalBundler {
_config: ConfigT;
_bundler: Bundler;
_deltaBundler: DeltaBundler<>;
_revisionsById: Map<RevisionId, Promise<GraphRevision>> = new Map();
_revisionsByGraphId: Map<GraphId, Promise<GraphRevision>> = new Map();
static revisionIdFromString = revisionIdFromString;
constructor(config: ConfigT) {
this._config = config;
this._bundler = new Bundler(config);
this._deltaBundler = new DeltaBundler(this._bundler);
}
end() {
this._deltaBundler.end();
this._bundler.end();
}
getBundler(): Bundler {
return this._bundler;
}
getDeltaBundler(): DeltaBundler<> {
return this._deltaBundler;
}
getRevision(revisionId: RevisionId): ?Promise<GraphRevision> {
return this._revisionsById.get(revisionId);
}
getRevisionByGraphId(graphId: GraphId): ?Promise<GraphRevision> {
return this._revisionsByGraphId.get(graphId);
}
async buildGraphForEntries(
entryFiles: $ReadOnlyArray<string>,
transformOptions: TransformInputOptions,
otherOptions?: OtherOptions = {
onProgress: null,
},
): Promise<OutputGraph> {
const absoluteEntryFiles = entryFiles.map(entryFile =>
path.resolve(this._config.projectRoot, entryFile),
);
await Promise.all(
absoluteEntryFiles.map(
entryFile =>
new Promise((resolve, reject) => {
// This should throw an error if the file doesn't exist.
// Using this instead of fs.exists to account for SimLinks.
fs.realpath(entryFile, err => {
if (err) {
reject(new ResourceNotFoundError(entryFile));
} else {
resolve();
}
});
}),
),
);
const graph = await this._deltaBundler.buildGraph(absoluteEntryFiles, {
resolve: await transformHelpers.getResolveDependencyFn(
this._bundler,
transformOptions.platform,
),
transform: await transformHelpers.getTransformFn(
absoluteEntryFiles,
this._bundler,
this._deltaBundler,
this._config,
transformOptions,
),
onProgress: otherOptions.onProgress,
});
this._config.serializer.experimentalSerializerHook(graph, {
added: graph.dependencies,
modified: new Map(),
deleted: new Set(),
reset: true,
});
return graph;
}
async buildGraph(
entryFile: string,
transformOptions: TransformInputOptions,
otherOptions?: OtherOptions = {
onProgress: null,
},
): Promise<{|+prepend: $ReadOnlyArray<Module<>>, +graph: OutputGraph|}> {
const graph = await this.buildGraphForEntries(
[entryFile],
transformOptions,
otherOptions,
);
const transformOptionsWithoutType = {
customTransformOptions: transformOptions.customTransformOptions,
dev: transformOptions.dev,
experimentalImportSupport: transformOptions.experimentalImportSupport,
hot: transformOptions.hot,
minify: transformOptions.minify,
platform: transformOptions.platform,
};
const prepend = await getPrependedScripts(
this._config,
transformOptionsWithoutType,
this._bundler,
this._deltaBundler,
);
return {
prepend,
graph,
};
}
// TODO T34760750 (alexkirsz) Eventually, I'd like to get to a point where
// this class exposes only initializeGraph and updateGraph.
async initializeGraph(
entryFile: string,
transformOptions: TransformInputOptions,
otherOptions?: OtherOptions = {
onProgress: null,
},
): Promise<{revision: GraphRevision, delta: DeltaResult<>}> {
const graphId = getGraphId(entryFile, transformOptions);
const revisionId = createRevisionId();
const revisionPromise = (async () => {
const {graph, prepend} = await this.buildGraph(
entryFile,
transformOptions,
otherOptions,
);
return {
id: revisionId,
date: new Date(),
graphId,
graph,
prepend,
};
})();
this._revisionsById.set(revisionId, revisionPromise);
this._revisionsByGraphId.set(graphId, revisionPromise);
const revision = await revisionPromise;
const delta = {
added: revision.graph.dependencies,
modified: new Map(),
deleted: new Set(),
reset: true,
};
return {
revision,
delta,
};
}
async updateGraph(
revision: GraphRevision,
reset: boolean,
): Promise<{revision: GraphRevision, delta: DeltaResult<>}> {
const delta = await this._deltaBundler.getDelta(revision.graph, {reset});
this._config.serializer.experimentalSerializerHook(revision.graph, delta);
if (
delta.added.size > 0 ||
delta.modified.size > 0 ||
delta.deleted.size > 0
) {
this._revisionsById.delete(revision.id);
revision = {
...revision,
// Generate a new revision id, to be used to verify the next incremental
// request.
id: crypto.randomBytes(8).toString('hex'),
date: new Date(),
};
const revisionPromise = Promise.resolve(revision);
this._revisionsById.set(revision.id, revisionPromise);
this._revisionsByGraphId.set(revision.graphId, revisionPromise);
}
return {revision, delta};
}
}
module.exports = IncrementalBundler;