UNPKG

@parcel/core

Version:
963 lines (948 loc) • 38.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = exports.RequestGraph = void 0; exports.getWatcherOptions = getWatcherOptions; exports.requestTypes = exports.requestGraphEdgeTypes = void 0; function _assert() { const data = _interopRequireWildcard(require("assert")); _assert = function () { return data; }; return data; } function _path2() { const data = _interopRequireDefault(require("path")); _path2 = function () { return data; }; return data; } function _graph() { const data = require("@parcel/graph"); _graph = function () { return data; }; return data; } function _logger() { const data = _interopRequireDefault(require("@parcel/logger")); _logger = function () { return data; }; return data; } function _rust() { const data = require("@parcel/rust"); _rust = function () { return data; }; return data; } function _utils() { const data = require("@parcel/utils"); _utils = function () { return data; }; return data; } function _nullthrows() { const data = _interopRequireDefault(require("nullthrows")); _nullthrows = function () { return data; }; return data; } var _constants = require("./constants"); var _projectPath = require("./projectPath"); var _ConfigRequest = require("./requests/ConfigRequest"); var _serializer = require("./serializer"); var _utils2 = require("./utils"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } const requestGraphEdgeTypes = exports.requestGraphEdgeTypes = { subrequest: 2, invalidated_by_update: 3, invalidated_by_delete: 4, invalidated_by_create: 5, invalidated_by_create_above: 6, dirname: 7 }; class FSBailoutError extends Error { name = 'FSBailoutError'; } const FILE = 0; const REQUEST = 1; const FILE_NAME = 2; const ENV = 3; const OPTION = 4; const GLOB = 5; const CONFIG_KEY = 6; const requestTypes = exports.requestTypes = { parcel_build_request: 1, bundle_graph_request: 2, asset_graph_request: 3, entry_request: 4, target_request: 5, parcel_config_request: 6, path_request: 7, dev_dep_request: 8, asset_request: 9, config_request: 10, write_bundles_request: 11, package_request: 12, write_bundle_request: 13, validation_request: 14 }; const nodeFromFilePath = filePath => ({ id: (0, _projectPath.fromProjectPathRelative)(filePath), type: FILE }); const nodeFromGlob = glob => ({ id: (0, _projectPath.fromProjectPathRelative)(glob), type: GLOB, value: glob }); const nodeFromFileName = fileName => ({ id: 'file_name:' + fileName, type: FILE_NAME }); const nodeFromRequest = request => ({ id: request.id, type: REQUEST, requestType: request.requestType, invalidateReason: _constants.INITIAL_BUILD }); const nodeFromEnv = (env, value) => ({ id: 'env:' + env, type: ENV, value }); const nodeFromOption = (option, value) => ({ id: 'option:' + option, type: OPTION, hash: (0, _utils2.hashFromOption)(value) }); const nodeFromConfigKey = (fileName, configKey, contentHash) => ({ id: `config_key:${(0, _projectPath.fromProjectPathRelative)(fileName)}:${configKey}`, type: CONFIG_KEY, configKey, contentHash }); const keyFromEnvContentKey = contentKey => contentKey.slice('env:'.length); const keyFromOptionContentKey = contentKey => contentKey.slice('option:'.length); class RequestGraph extends _graph().ContentGraph { invalidNodeIds = new Set(); incompleteNodeIds = new Set(); incompleteNodePromises = new Map(); globNodeIds = new Set(); envNodeIds = new Set(); optionNodeIds = new Set(); // Unpredictable nodes are requests that cannot be predicted whether they should rerun based on // filesystem changes alone. They should rerun on each startup of Parcel. unpredicatableNodeIds = new Set(); invalidateOnBuildNodeIds = new Set(); configKeyNodes = new Map(); // $FlowFixMe[prop-missing] static deserialize(opts) { // $FlowFixMe[prop-missing] let deserialized = new RequestGraph(opts); deserialized.invalidNodeIds = opts.invalidNodeIds; deserialized.incompleteNodeIds = opts.incompleteNodeIds; deserialized.globNodeIds = opts.globNodeIds; deserialized.envNodeIds = opts.envNodeIds; deserialized.optionNodeIds = opts.optionNodeIds; deserialized.unpredicatableNodeIds = opts.unpredicatableNodeIds; deserialized.invalidateOnBuildNodeIds = opts.invalidateOnBuildNodeIds; deserialized.configKeyNodes = opts.configKeyNodes; return deserialized; } // $FlowFixMe[prop-missing] serialize() { return { ...super.serialize(), invalidNodeIds: this.invalidNodeIds, incompleteNodeIds: this.incompleteNodeIds, globNodeIds: this.globNodeIds, envNodeIds: this.envNodeIds, optionNodeIds: this.optionNodeIds, unpredicatableNodeIds: this.unpredicatableNodeIds, invalidateOnBuildNodeIds: this.invalidateOnBuildNodeIds, configKeyNodes: this.configKeyNodes }; } // addNode for RequestGraph should not override the value if added multiple times addNode(node) { let nodeId = this._contentKeyToNodeId.get(node.id); if (nodeId != null) { return nodeId; } nodeId = super.addNodeByContentKey(node.id, node); if (node.type === GLOB) { this.globNodeIds.add(nodeId); } else if (node.type === ENV) { this.envNodeIds.add(nodeId); } else if (node.type === OPTION) { this.optionNodeIds.add(nodeId); } return nodeId; } removeNode(nodeId) { this.invalidNodeIds.delete(nodeId); this.incompleteNodeIds.delete(nodeId); this.incompleteNodePromises.delete(nodeId); this.unpredicatableNodeIds.delete(nodeId); this.invalidateOnBuildNodeIds.delete(nodeId); let node = (0, _nullthrows().default)(this.getNode(nodeId)); if (node.type === GLOB) { this.globNodeIds.delete(nodeId); } else if (node.type === ENV) { this.envNodeIds.delete(nodeId); } else if (node.type === OPTION) { this.optionNodeIds.delete(nodeId); } else if (node.type === CONFIG_KEY) { for (let configKeyNodes of this.configKeyNodes.values()) { configKeyNodes.delete(nodeId); } } return super.removeNode(nodeId); } getRequestNode(nodeId) { let node = (0, _nullthrows().default)(this.getNode(nodeId)); if (node.type === REQUEST) { return node; } throw new (_assert().AssertionError)({ message: `Expected a request node: ${node.type} (${typeof node.type}) does not equal ${REQUEST} (${typeof REQUEST}).`, expected: REQUEST, actual: node.type }); } replaceSubrequests(requestNodeId, subrequestContentKeys) { let subrequestNodeIds = []; for (let key of subrequestContentKeys) { if (this.hasContentKey(key)) { subrequestNodeIds.push(this.getNodeIdByContentKey(key)); } } this.replaceNodeIdsConnectedTo(requestNodeId, subrequestNodeIds, null, requestGraphEdgeTypes.subrequest); } invalidateNode(nodeId, reason) { let node = (0, _nullthrows().default)(this.getNode(nodeId)); (0, _assert().default)(node.type === REQUEST); node.invalidateReason |= reason; this.invalidNodeIds.add(nodeId); let parentNodes = this.getNodeIdsConnectedTo(nodeId, requestGraphEdgeTypes.subrequest); for (let parentNode of parentNodes) { this.invalidateNode(parentNode, reason); } } invalidateUnpredictableNodes() { for (let nodeId of this.unpredicatableNodeIds) { let node = (0, _nullthrows().default)(this.getNode(nodeId)); (0, _assert().default)(node.type !== FILE && node.type !== GLOB); this.invalidateNode(nodeId, _constants.STARTUP); } } invalidateOnBuildNodes() { for (let nodeId of this.invalidateOnBuildNodeIds) { let node = (0, _nullthrows().default)(this.getNode(nodeId)); (0, _assert().default)(node.type !== FILE && node.type !== GLOB); this.invalidateNode(nodeId, _constants.STARTUP); } } invalidateEnvNodes(env) { for (let nodeId of this.envNodeIds) { let node = (0, _nullthrows().default)(this.getNode(nodeId)); (0, _assert().default)(node.type === ENV); if (env[keyFromEnvContentKey(node.id)] !== node.value) { let parentNodes = this.getNodeIdsConnectedTo(nodeId, requestGraphEdgeTypes.invalidated_by_update); for (let parentNode of parentNodes) { this.invalidateNode(parentNode, _constants.ENV_CHANGE); } } } } invalidateOptionNodes(options) { for (let nodeId of this.optionNodeIds) { let node = (0, _nullthrows().default)(this.getNode(nodeId)); (0, _assert().default)(node.type === OPTION); if ((0, _utils2.hashFromOption)(options[keyFromOptionContentKey(node.id)]) !== node.hash) { let parentNodes = this.getNodeIdsConnectedTo(nodeId, requestGraphEdgeTypes.invalidated_by_update); for (let parentNode of parentNodes) { this.invalidateNode(parentNode, _constants.OPTION_CHANGE); } } } } invalidateOnConfigKeyChange(requestNodeId, filePath, configKey, contentHash) { let configKeyNodeId = this.addNode(nodeFromConfigKey(filePath, configKey, contentHash)); let nodes = this.configKeyNodes.get(filePath); if (!nodes) { nodes = new Set(); this.configKeyNodes.set(filePath, nodes); } nodes.add(configKeyNodeId); if (!this.hasEdge(requestNodeId, configKeyNodeId, requestGraphEdgeTypes.invalidated_by_update)) { this.addEdge(requestNodeId, configKeyNodeId, // Store as an update edge, but file deletes are handled too requestGraphEdgeTypes.invalidated_by_update); } } invalidateOnFileUpdate(requestNodeId, filePath) { let fileNodeId = this.addNode(nodeFromFilePath(filePath)); if (!this.hasEdge(requestNodeId, fileNodeId, requestGraphEdgeTypes.invalidated_by_update)) { this.addEdge(requestNodeId, fileNodeId, requestGraphEdgeTypes.invalidated_by_update); } } invalidateOnFileDelete(requestNodeId, filePath) { let fileNodeId = this.addNode(nodeFromFilePath(filePath)); if (!this.hasEdge(requestNodeId, fileNodeId, requestGraphEdgeTypes.invalidated_by_delete)) { this.addEdge(requestNodeId, fileNodeId, requestGraphEdgeTypes.invalidated_by_delete); } } invalidateOnFileCreate(requestNodeId, input) { let node; if (input.glob != null) { node = nodeFromGlob(input.glob); } else if (input.fileName != null && input.aboveFilePath != null) { let aboveFilePath = input.aboveFilePath; // Create nodes and edges for each part of the filename pattern. // For example, 'node_modules/foo' would create two nodes and one edge. // This creates a sort of trie structure within the graph that can be // quickly matched by following the edges. This is also memory efficient // since common sub-paths (e.g. 'node_modules') are deduplicated. let parts = input.fileName.split('/').reverse(); let lastNodeId; for (let part of parts) { let fileNameNode = nodeFromFileName(part); let fileNameNodeId = this.addNode(fileNameNode); if (lastNodeId != null && !this.hasEdge(lastNodeId, fileNameNodeId, requestGraphEdgeTypes.dirname)) { this.addEdge(lastNodeId, fileNameNodeId, requestGraphEdgeTypes.dirname); } lastNodeId = fileNameNodeId; } // The `aboveFilePath` condition asserts that requests are only invalidated // if the file being created is "above" it in the filesystem (e.g. the file // is created in a parent directory). There is likely to already be a node // for this file in the graph (e.g. the source file) that we can reuse for this. node = nodeFromFilePath(aboveFilePath); let nodeId = this.addNode(node); // Now create an edge from the `aboveFilePath` node to the first file_name node // in the chain created above, and an edge from the last node in the chain back to // the `aboveFilePath` node. When matching, we will start from the first node in // the chain, and continue following it to parent directories until there is an // edge pointing an `aboveFilePath` node that also points to the start of the chain. // This indicates a complete match, and any requests attached to the `aboveFilePath` // node will be invalidated. let firstId = 'file_name:' + parts[0]; let firstNodeId = this.getNodeIdByContentKey(firstId); if (!this.hasEdge(nodeId, firstNodeId, requestGraphEdgeTypes.invalidated_by_create_above)) { this.addEdge(nodeId, firstNodeId, requestGraphEdgeTypes.invalidated_by_create_above); } (0, _assert().default)(lastNodeId != null); if (!this.hasEdge(lastNodeId, nodeId, requestGraphEdgeTypes.invalidated_by_create_above)) { this.addEdge(lastNodeId, nodeId, requestGraphEdgeTypes.invalidated_by_create_above); } } else if (input.filePath != null) { node = nodeFromFilePath(input.filePath); } else { throw new Error('Invalid invalidation'); } let nodeId = this.addNode(node); if (!this.hasEdge(requestNodeId, nodeId, requestGraphEdgeTypes.invalidated_by_create)) { this.addEdge(requestNodeId, nodeId, requestGraphEdgeTypes.invalidated_by_create); } } invalidateOnStartup(requestNodeId) { this.getRequestNode(requestNodeId); this.unpredicatableNodeIds.add(requestNodeId); } invalidateOnBuild(requestNodeId) { this.getRequestNode(requestNodeId); this.invalidateOnBuildNodeIds.add(requestNodeId); } invalidateOnEnvChange(requestNodeId, env, value) { let envNode = nodeFromEnv(env, value); let envNodeId = this.addNode(envNode); if (!this.hasEdge(requestNodeId, envNodeId, requestGraphEdgeTypes.invalidated_by_update)) { this.addEdge(requestNodeId, envNodeId, requestGraphEdgeTypes.invalidated_by_update); } } invalidateOnOptionChange(requestNodeId, option, value) { let optionNode = nodeFromOption(option, value); let optionNodeId = this.addNode(optionNode); if (!this.hasEdge(requestNodeId, optionNodeId, requestGraphEdgeTypes.invalidated_by_update)) { this.addEdge(requestNodeId, optionNodeId, requestGraphEdgeTypes.invalidated_by_update); } } clearInvalidations(nodeId) { this.unpredicatableNodeIds.delete(nodeId); this.invalidateOnBuildNodeIds.delete(nodeId); this.replaceNodeIdsConnectedTo(nodeId, [], null, requestGraphEdgeTypes.invalidated_by_update); this.replaceNodeIdsConnectedTo(nodeId, [], null, requestGraphEdgeTypes.invalidated_by_delete); this.replaceNodeIdsConnectedTo(nodeId, [], null, requestGraphEdgeTypes.invalidated_by_create); } getInvalidations(requestNodeId) { if (!this.hasNode(requestNodeId)) { return []; } // For now just handling updates. Could add creates/deletes later if needed. let invalidations = this.getNodeIdsConnectedFrom(requestNodeId, requestGraphEdgeTypes.invalidated_by_update); return invalidations.map(nodeId => { let node = (0, _nullthrows().default)(this.getNode(nodeId)); switch (node.type) { case FILE: return { type: 'file', filePath: (0, _projectPath.toProjectPathUnsafe)(node.id) }; case ENV: return { type: 'env', key: keyFromEnvContentKey(node.id) }; case OPTION: return { type: 'option', key: keyFromOptionContentKey(node.id) }; } }).filter(Boolean); } getSubRequests(requestNodeId) { if (!this.hasNode(requestNodeId)) { return []; } let subRequests = this.getNodeIdsConnectedFrom(requestNodeId, requestGraphEdgeTypes.subrequest); return subRequests.map(nodeId => { let node = (0, _nullthrows().default)(this.getNode(nodeId)); (0, _assert().default)(node.type === REQUEST); return node; }); } getInvalidSubRequests(requestNodeId) { if (!this.hasNode(requestNodeId)) { return []; } let subRequests = this.getNodeIdsConnectedFrom(requestNodeId, requestGraphEdgeTypes.subrequest); return subRequests.filter(id => this.invalidNodeIds.has(id)).map(nodeId => { let node = (0, _nullthrows().default)(this.getNode(nodeId)); (0, _assert().default)(node.type === REQUEST); return node; }); } invalidateFileNameNode(node, filePath, matchNodes) { // If there is an edge between this file_name node and one of the original file nodes pointed to // by the original file_name node, and the matched node is inside the current directory, invalidate // all connected requests pointed to by the file node. let dirname = _path2().default.dirname((0, _projectPath.fromProjectPathRelative)(filePath)); let nodeId = this.getNodeIdByContentKey(node.id); for (let matchNode of matchNodes) { let matchNodeId = this.getNodeIdByContentKey(matchNode.id); if (this.hasEdge(nodeId, matchNodeId, requestGraphEdgeTypes.invalidated_by_create_above) && (0, _utils().isDirectoryInside)((0, _projectPath.fromProjectPathRelative)((0, _projectPath.toProjectPathUnsafe)(matchNode.id)), dirname)) { let connectedNodes = this.getNodeIdsConnectedTo(matchNodeId, requestGraphEdgeTypes.invalidated_by_create); for (let connectedNode of connectedNodes) { this.invalidateNode(connectedNode, _constants.FILE_CREATE); } } } // Find the `file_name` node for the parent directory and // recursively invalidate connected requests as described above. let basename = _path2().default.basename(dirname); let contentKey = 'file_name:' + basename; if (this.hasContentKey(contentKey)) { if (this.hasEdge(nodeId, this.getNodeIdByContentKey(contentKey), requestGraphEdgeTypes.dirname)) { let parent = (0, _nullthrows().default)(this.getNodeByContentKey(contentKey)); (0, _assert().default)(parent.type === FILE_NAME); this.invalidateFileNameNode(parent, (0, _projectPath.toProjectPathUnsafe)(dirname), matchNodes); } } } async respondToFSEvents(events, options, threshold) { let didInvalidate = false; let count = 0; let predictedTime = 0; let startTime = Date.now(); for (let { path: _path, type } of events) { if (++count === 256) { let duration = Date.now() - startTime; predictedTime = duration * (events.length >> 8); if (predictedTime > threshold) { _logger().default.warn({ origin: '@parcel/core', message: 'Building with clean cache. Cache invalidation took too long.', meta: { trackableEvent: 'cache_invalidation_timeout', watcherEventCount: events.length, predictedTime } }); throw new FSBailoutError('Responding to file system events exceeded threshold, start with empty cache.'); } } let _filePath = (0, _projectPath.toProjectPath)(options.projectRoot, _path); let filePath = (0, _projectPath.fromProjectPathRelative)(_filePath); let hasFileRequest = this.hasContentKey(filePath); // If we see a 'create' event for the project root itself, // this means the project root was moved and we need to // re-run all requests. if (type === 'create' && filePath === '') { _logger().default.verbose({ origin: '@parcel/core', message: 'Watcher reported project root create event. Invalidate all nodes.', meta: { trackableEvent: 'project_root_create' } }); for (let [id, node] of this.nodes.entries()) { if ((node === null || node === void 0 ? void 0 : node.type) === REQUEST) { this.invalidNodeIds.add(id); } } return true; } // sometimes mac os reports update events as create events. // if it was a create event, but the file already exists in the graph, // then also invalidate nodes connected by invalidated_by_update edges. if (hasFileRequest && (type === 'create' || type === 'update')) { let nodeId = this.getNodeIdByContentKey(filePath); let nodes = this.getNodeIdsConnectedTo(nodeId, requestGraphEdgeTypes.invalidated_by_update); for (let connectedNode of nodes) { didInvalidate = true; this.invalidateNode(connectedNode, _constants.FILE_UPDATE); } if (type === 'create') { let nodes = this.getNodeIdsConnectedTo(nodeId, requestGraphEdgeTypes.invalidated_by_create); for (let connectedNode of nodes) { didInvalidate = true; this.invalidateNode(connectedNode, _constants.FILE_CREATE); } } } else if (type === 'create') { let basename = _path2().default.basename(filePath); let fileNameNode = this.getNodeByContentKey('file_name:' + basename); if (fileNameNode != null && fileNameNode.type === FILE_NAME) { let fileNameNodeId = this.getNodeIdByContentKey('file_name:' + basename); // Find potential file nodes to be invalidated if this file name pattern matches let above = []; for (const nodeId of this.getNodeIdsConnectedTo(fileNameNodeId, requestGraphEdgeTypes.invalidated_by_create_above)) { let node = (0, _nullthrows().default)(this.getNode(nodeId)); // these might also be `glob` nodes which get handled below, we only care about files here. if (node.type === FILE) { above.push(node); } } if (above.length > 0) { didInvalidate = true; this.invalidateFileNameNode(fileNameNode, _filePath, above); } } for (let globeNodeId of this.globNodeIds) { let globNode = this.getNode(globeNodeId); (0, _assert().default)(globNode && globNode.type === GLOB); if ((0, _utils().isGlobMatch)(filePath, (0, _projectPath.fromProjectPathRelative)(globNode.value))) { let connectedNodes = this.getNodeIdsConnectedTo(globeNodeId, requestGraphEdgeTypes.invalidated_by_create); for (let connectedNode of connectedNodes) { didInvalidate = true; this.invalidateNode(connectedNode, _constants.FILE_CREATE); } } } } else if (hasFileRequest && type === 'delete') { let nodeId = this.getNodeIdByContentKey(filePath); for (let connectedNode of this.getNodeIdsConnectedTo(nodeId, requestGraphEdgeTypes.invalidated_by_delete)) { didInvalidate = true; this.invalidateNode(connectedNode, _constants.FILE_DELETE); } // Delete the file node since it doesn't exist anymore. // This ensures that files that don't exist aren't sent // to requests as invalidations for future requests. this.removeNode(nodeId); } let configKeyNodes = this.configKeyNodes.get(_filePath); if (configKeyNodes && (type === 'delete' || type === 'update')) { for (let nodeId of configKeyNodes) { let isInvalid = type === 'delete'; if (type === 'update') { let node = this.getNode(nodeId); (0, _assert().default)(node && node.type === CONFIG_KEY); let contentHash = await (0, _ConfigRequest.getConfigKeyContentHash)(_filePath, node.configKey, options); isInvalid = node.contentHash !== contentHash; } if (isInvalid) { for (let connectedNode of this.getNodeIdsConnectedTo(nodeId, requestGraphEdgeTypes.invalidated_by_update)) { this.invalidateNode(connectedNode, type === 'delete' ? _constants.FILE_DELETE : _constants.FILE_UPDATE); } didInvalidate = true; this.removeNode(nodeId); } } } } let duration = Date.now() - startTime; _logger().default.verbose({ origin: '@parcel/core', message: `RequestGraph.respondToFSEvents duration: ${duration}`, meta: { trackableEvent: 'fsevent_response_time', duration, predictedTime } }); return didInvalidate && this.invalidNodeIds.size > 0; } } exports.RequestGraph = RequestGraph; class RequestTracker { stats = new Map(); constructor({ graph, farm, options }) { this.graph = graph || new RequestGraph(); this.farm = farm; this.options = options; } // TODO: refactor (abortcontroller should be created by RequestTracker) setSignal(signal) { this.signal = signal; } startRequest(request) { let didPreviouslyExist = this.graph.hasContentKey(request.id); let requestNodeId; if (didPreviouslyExist) { requestNodeId = this.graph.getNodeIdByContentKey(request.id); // Clear existing invalidations for the request so that the new // invalidations created during the request replace the existing ones. this.graph.clearInvalidations(requestNodeId); } else { requestNodeId = this.graph.addNode(nodeFromRequest(request)); } this.graph.incompleteNodeIds.add(requestNodeId); this.graph.invalidNodeIds.delete(requestNodeId); let { promise, deferred } = (0, _utils().makeDeferredWithPromise)(); this.graph.incompleteNodePromises.set(requestNodeId, promise); return { requestNodeId, deferred }; } // If a cache key is provided, the result will be removed from the node and stored in a separate cache entry storeResult(nodeId, result, cacheKey) { let node = this.graph.getNode(nodeId); if (node && node.type === REQUEST) { node.result = result; node.resultCacheKey = cacheKey; } } hasValidResult(nodeId) { return this.graph.hasNode(nodeId) && !this.graph.invalidNodeIds.has(nodeId) && !this.graph.incompleteNodeIds.has(nodeId); } async getRequestResult(contentKey, ifMatch) { let node = (0, _nullthrows().default)(this.graph.getNodeByContentKey(contentKey)); (0, _assert().default)(node.type === REQUEST); if (ifMatch != null && node.resultCacheKey !== ifMatch) { return null; } if (node.result != undefined) { // $FlowFixMe let result = node.result; return result; } else if (node.resultCacheKey != null && ifMatch == null) { let key = node.resultCacheKey; (0, _assert().default)(this.options.cache.hasLargeBlob(key)); let cachedResult = (0, _serializer.deserialize)(await this.options.cache.getLargeBlob(key)); node.result = cachedResult; return cachedResult; } } completeRequest(nodeId) { this.graph.invalidNodeIds.delete(nodeId); this.graph.incompleteNodeIds.delete(nodeId); this.graph.incompleteNodePromises.delete(nodeId); let node = this.graph.getNode(nodeId); if (node && node.type === REQUEST) { node.invalidateReason = _constants.VALID; } } rejectRequest(nodeId) { this.graph.incompleteNodeIds.delete(nodeId); this.graph.incompleteNodePromises.delete(nodeId); let node = this.graph.getNode(nodeId); if ((node === null || node === void 0 ? void 0 : node.type) === REQUEST) { this.graph.invalidateNode(nodeId, _constants.ERROR); } } respondToFSEvents(events, threshold) { return this.graph.respondToFSEvents(events, this.options, threshold); } hasInvalidRequests() { return this.graph.invalidNodeIds.size > 0; } getInvalidRequests() { let invalidRequests = []; for (let id of this.graph.invalidNodeIds) { let node = (0, _nullthrows().default)(this.graph.getNode(id)); (0, _assert().default)(node.type === REQUEST); invalidRequests.push(node); } return invalidRequests; } replaceSubrequests(requestNodeId, subrequestContextKeys) { this.graph.replaceSubrequests(requestNodeId, subrequestContextKeys); } async runRequest(request, opts) { let hasKey = this.graph.hasContentKey(request.id); let requestId = hasKey ? this.graph.getNodeIdByContentKey(request.id) : undefined; let hasValidResult = requestId != null && this.hasValidResult(requestId); if (!(opts !== null && opts !== void 0 && opts.force) && hasValidResult) { // $FlowFixMe[incompatible-type] return this.getRequestResult(request.id); } if (requestId != null) { let incompletePromise = this.graph.incompleteNodePromises.get(requestId); if (incompletePromise != null) { // There is a another instance of this request already running, wait for its completion and reuse its result try { if (await incompletePromise) { // $FlowFixMe[incompatible-type] return this.getRequestResult(request.id); } } catch (e) { // Rerun this request } } } let previousInvalidations = requestId != null ? this.graph.getInvalidations(requestId) : []; let { requestNodeId, deferred } = this.startRequest({ id: request.id, type: REQUEST, requestType: request.type, invalidateReason: _constants.INITIAL_BUILD }); let { api, subRequestContentKeys } = this.createAPI(requestNodeId, previousInvalidations); try { let node = this.graph.getRequestNode(requestNodeId); this.stats.set(request.type, (this.stats.get(request.type) ?? 0) + 1); let result = await request.run({ input: request.input, api, farm: this.farm, invalidateReason: node.invalidateReason, options: this.options }); (0, _utils2.assertSignalNotAborted)(this.signal); this.completeRequest(requestNodeId); deferred.resolve(true); return result; } catch (err) { if (!(err instanceof _utils2.BuildAbortError) && request.type === requestTypes.dev_dep_request) { _logger().default.verbose({ origin: '@parcel/core', message: `Failed DevDepRequest`, meta: { trackableEvent: 'failed_dev_dep_request', hasKey, hasValidResult } }); } this.rejectRequest(requestNodeId); deferred.resolve(false); throw err; } finally { this.graph.replaceSubrequests(requestNodeId, [...subRequestContentKeys]); } } flushStats() { let requestTypeEntries = {}; for (let key of Object.keys(requestTypes)) { requestTypeEntries[requestTypes[key]] = key; } let formattedStats = {}; for (let [requestType, count] of this.stats.entries()) { let requestTypeName = requestTypeEntries[requestType]; formattedStats[requestTypeName] = count; } this.stats = new Map(); return formattedStats; } createAPI(requestId, previousInvalidations) { let subRequestContentKeys = new Set(); return { api: { invalidateOnFileCreate: input => this.graph.invalidateOnFileCreate(requestId, input), invalidateOnConfigKeyChange: (filePath, configKey, contentHash) => this.graph.invalidateOnConfigKeyChange(requestId, filePath, configKey, contentHash), invalidateOnFileDelete: filePath => this.graph.invalidateOnFileDelete(requestId, filePath), invalidateOnFileUpdate: filePath => this.graph.invalidateOnFileUpdate(requestId, filePath), invalidateOnStartup: () => this.graph.invalidateOnStartup(requestId), invalidateOnBuild: () => this.graph.invalidateOnBuild(requestId), invalidateOnEnvChange: env => this.graph.invalidateOnEnvChange(requestId, env, this.options.env[env]), invalidateOnOptionChange: option => this.graph.invalidateOnOptionChange(requestId, option, this.options[option]), getInvalidations: () => previousInvalidations, storeResult: (result, cacheKey) => { this.storeResult(requestId, result, cacheKey); }, getSubRequests: () => this.graph.getSubRequests(requestId), getInvalidSubRequests: () => this.graph.getInvalidSubRequests(requestId), getPreviousResult: ifMatch => { var _this$graph$getNode; let contentKey = (0, _nullthrows().default)((_this$graph$getNode = this.graph.getNode(requestId)) === null || _this$graph$getNode === void 0 ? void 0 : _this$graph$getNode.id); return this.getRequestResult(contentKey, ifMatch); }, getRequestResult: id => this.getRequestResult(id), canSkipSubrequest: contentKey => { if (this.graph.hasContentKey(contentKey) && this.hasValidResult(this.graph.getNodeIdByContentKey(contentKey))) { subRequestContentKeys.add(contentKey); return true; } return false; }, runRequest: (subRequest, opts) => { subRequestContentKeys.add(subRequest.id); return this.runRequest(subRequest, opts); } }, subRequestContentKeys }; } async writeToCache(signal) { let cacheKey = getCacheKey(this.options); let requestGraphKey = `${cacheKey}-RequestGraph`; if (this.options.shouldDisableCache) { return; } let keys = [requestGraphKey]; let promises = []; for (let node of this.graph.nodes) { if (!node || node.type !== REQUEST) { continue; } let resultCacheKey = node.resultCacheKey; if (resultCacheKey != null && node.result != null) { keys.push(resultCacheKey); promises.push(this.options.cache.setLargeBlob(resultCacheKey, (0, _serializer.serialize)(node.result), { signal })); delete node.result; } } promises.push(this.options.cache.setLargeBlob(requestGraphKey, (0, _serializer.serialize)(this.graph), { signal })); let opts = getWatcherOptions(this.options); let snapshotPath = _path2().default.join(this.options.cacheDir, `snapshot-${cacheKey}` + '.txt'); promises.push(this.options.inputFS.writeSnapshot(this.options.watchDir, snapshotPath, opts)); try { await Promise.all(promises); } catch (err) { if (signal !== null && signal !== void 0 && signal.aborted) { // If writing to the cache was aborted, delete all of the keys to avoid inconsistent states. for (let key of keys) { try { await this.options.cache.deleteLargeBlob(key); } catch (err) { // ignore. } } } else { throw err; } } } static async init({ farm, options }) { let graph = await loadRequestGraph(options); return new RequestTracker({ farm, graph, options }); } } exports.default = RequestTracker; function getWatcherOptions({ watchIgnore = [], cacheDir, watchDir, watchBackend }) { const uniqueDirs = [...new Set([...watchIgnore, ...['.git', '.hg'], cacheDir])]; const ignore = uniqueDirs.map(dir => _path2().default.resolve(watchDir, dir)); return { ignore, backend: watchBackend }; } function getCacheKey(options) { return (0, _rust().hashString)(`${_constants.PARCEL_VERSION}:${JSON.stringify(options.entries)}:${options.mode}:${options.shouldBuildLazily ? 'lazy' : 'eager'}:${options.watchBackend ?? ''}`); } async function loadRequestGraph(options) { if (options.shouldDisableCache) { return new RequestGraph(); } let cacheKey = getCacheKey(options); let requestGraphKey = `${cacheKey}-RequestGraph`; const snapshotPath = _path2().default.join(options.cacheDir, `snapshot-${cacheKey}` + '.txt'); if (await options.cache.hasLargeBlob(requestGraphKey)) { try { let requestGraph = (0, _serializer.deserialize)(await options.cache.getLargeBlob(requestGraphKey)); let opts = getWatcherOptions(options); let events = await options.inputFS.getEventsSince(options.watchDir, snapshotPath, opts); requestGraph.invalidateUnpredictableNodes(); requestGraph.invalidateOnBuildNodes(); requestGraph.invalidateEnvNodes(options.env); requestGraph.invalidateOptionNodes(options); await requestGraph.respondToFSEvents(options.unstableFileInvalidations || events, options, 10000); return requestGraph; } catch (e) { // Prevent logging fs events took too long warning logErrorOnBailout(options, snapshotPath, e); // This error means respondToFSEvents timed out handling the invalidation events // In this case we'll return a fresh RequestGraph return new RequestGraph(); } } return new RequestGraph(); } function logErrorOnBailout(options, snapshotPath, e) { if (e.message && e.message.includes('invalid clockspec')) { const snapshotContents = options.inputFS.readFileSync(snapshotPath, 'utf-8'); _logger().default.warn({ origin: '@parcel/core', message: `Error reading clockspec from snapshot, building with clean cache.`, meta: { snapshotContents: snapshotContents, trackableEvent: 'invalid_clockspec_error' } }); } else if (!(e instanceof FSBailoutError)) { _logger().default.warn({ origin: '@parcel/core', message: `Unexpected error loading cache from disk, building with clean cache.`, meta: { errorMessage: e.message, errorStack: e.stack, trackableEvent: 'cache_load_error' } }); } }