UNPKG

nx

Version:

The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.

661 lines (660 loc) 34.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.currentSourceMaps = exports.currentProjectGraph = exports.currentProjectFileMapCache = exports.fileMapWithFiles = void 0; exports.getCachedSerializedProjectGraphPromise = getCachedSerializedProjectGraphPromise; exports.scheduleProjectGraphRecomputation = scheduleProjectGraphRecomputation; exports.registerProjectGraphRecomputationListener = registerProjectGraphRecomputationListener; exports.invalidateGraphCache = invalidateGraphCache; exports.getRecomputationGeneration = getRecomputationGeneration; exports.isKnownWorkspaceFile = isKnownWorkspaceFile; const perf_hooks_1 = require("perf_hooks"); const nx_json_1 = require("../../config/nx-json"); const file_hasher_1 = require("../../hasher/file-hasher"); const build_project_graph_1 = require("../../project-graph/build-project-graph"); const error_types_1 = require("../../project-graph/error-types"); const file_map_utils_1 = require("../../project-graph/file-map-utils"); const nx_deps_cache_1 = require("../../project-graph/nx-deps-cache"); const get_plugins_1 = require("../../project-graph/plugins/get-plugins"); const retrieve_workspace_files_1 = require("../../project-graph/utils/retrieve-workspace-files"); const fileutils_1 = require("../../utils/fileutils"); const workspace_context_1 = require("../../utils/workspace-context"); const workspace_root_1 = require("../../utils/workspace-root"); const progress_topics_1 = require("../../utils/progress-topics"); const client_socket_context_1 = require("./client-socket-context"); const dotenv_graph_changes_1 = require("./dotenv-graph-changes"); const file_change_events_1 = require("./file-watching/file-change-events"); const file_watcher_sockets_1 = require("./file-watching/file-watcher-sockets"); const project_graph_listener_sockets_1 = require("./project-graph-listener-sockets"); const watcher_1 = require("./watcher"); const logger_1 = require("../logger"); let cachedSerializedProjectGraphPromise; // Maps file path to a version counter that increments on each modification. // This lets us detect mid-flight re-modifications when clearing processed files. const collectedUpdatedFiles = new Map(); const collectedDeletedFiles = new Map(); const projectGraphRecomputationListeners = new Set(); let storedWorkspaceConfigHash; let knownExternalNodes = {}; let fileChangeCounter = 0; let recomputationGeneration = 0; // The graph the settled cached promise serves, with the generation its // computation claimed. Set only when a computation's own success becomes the // cached result; any newer kickoff or invalidation clears it, so non-null // means the cached promise is settled and this is exactly what it serves. The // warm-reuse dotenv check classifies against this, never against // `currentProjectGraph`: a slow stale computation overwrites that after the // winner settles and only then chains away. The candidate is written by the // computation itself (which knows its generation) and promoted by // kickOffRecompute after the outer plugin-hash and cached-pointer identity // checks, which can still reject the result. let servedGraphState = null; let servedGraphCandidate = null; // True after the first successful persistProjectGraphToDisk call. Until // that happens, "project-graph.json missing on disk" is the expected // state (we just haven't written it yet) and must not trigger a reset. let cacheHasBeenPersisted = false; /** * Freshness-gated recompute. Each IIFE snapshots the nx.json `plugins` * hash at kickoff and re-reads at commit; if it changed mid-flight, bail * and kick a successor instead of clobbering the winner. Without this, * `cachedSerializedProjectGraphPromise` is last-kickoff-wins and can * return a graph built against a stale plugin set * (see spread.test.ts "middle plugin" flake). */ function kickOffRecompute() { // The cached pointer is about to hold an unsettled promise, so whatever the // previous state described is no longer what the cache serves. servedGraphState = null; let myPromise; myPromise = (async () => { // Must resolve, never reject: kickOffRecompute() runs fire-and-forget, so // a rejected myPromise crashes the daemon (unhandled rejection). A throwing // prologue (e.g. plugin load fails) becomes an errorResult the next requester surfaces. try { // Single read shared with getPluginsSeparated below. This collapses // what would otherwise be two independent nx.json reads (our snap + // the plugin loader's) into one, so the snap hash and the plugin // set the compute uses always reflect the same disk state. const nxJson = (0, nx_json_1.readNxJson)(workspace_root_1.workspaceRoot); const myPluginsHash = (0, file_hasher_1.hashObject)(nxJson.plugins ?? []); const plugins = await (0, get_plugins_1.getPluginsSeparated)(nxJson, workspace_root_1.workspaceRoot); // Plugin set we just loaded may already be stale vs disk. if (isStale(myPluginsHash)) return chainToSuccessor(myPromise); const result = await processFilesAndCreateAndSerializeProjectGraph(plugins); // Compute may have run against plugins that are now stale. if (isStale(myPluginsHash)) return chainToSuccessor(myPromise); if (cachedSerializedProjectGraphPromise === myPromise && result.projectGraph) { // The graph identity ties the candidate's generation to exactly this // result; a chained result never matches (its computation's kickoff // replaced the cached pointer, failing the identity check above). if (servedGraphCandidate?.graph === result.projectGraph) { servedGraphState = servedGraphCandidate; } notifyProjectGraphRecomputationListeners(result.projectGraph, result.sourceMaps, result.error); persistProjectGraphToDisk(result); } return result; } catch (e) { return errorResult(e); } })(); cachedSerializedProjectGraphPromise = myPromise; } function isStale(expectedHash) { return readNxJsonPluginsHash() !== expectedHash; } /** * Starts a successor recompute only when this IIFE is still the cached one. * If a newer recompute already replaced the cached pointer, that newer * recompute will produce the fresh result and we just need to return the * pointer so awaiters chain onto it. */ function chainToSuccessor(myPromise) { logger_1.serverLogger.log('Discarding stale recompute result (nx.json plugins changed mid-compute).'); if (cachedSerializedProjectGraphPromise === myPromise) kickOffRecompute(); return cachedSerializedProjectGraphPromise; } function readNxJsonPluginsHash() { return (0, file_hasher_1.hashObject)((0, nx_json_1.readNxJson)(workspace_root_1.workspaceRoot).plugins ?? []); } async function getCachedSerializedProjectGraphPromise(socket) { // Subscribe the requesting client to the graph-construction topic // for the duration of the await, so in-flight progress/log messages // — including those produced by a recomputation that was already // started before this caller arrived — are broadcast to them. if (socket) { (0, client_socket_context_1.subscribeClientToTopic)(socket, progress_topics_1.ProgressTopics.GraphConstruction); } try { // Drain anything the native watcher has buffered before deciding // whether the cached graph is fresh. Without this, a file change // that already arrived in the watcher's accumulator but hasn't // flushed past IDLE_WINDOW yet would be invisible to the staleness // check below — the daemon would return a stale graph. await (0, watcher_1.flushPendingWorkspaceChanges)(); await resetInternalStateIfNxDepsMissing(); // Yield one macrotask boundary so any TSFN-queued watcher callbacks // run before we read collected*. Without this, an event that left // the native side but is still queued in libuv's I/O queue (behind // our request handler) would be invisible here and we'd serve a // stale graph. Placed right before the read so no microtask gap // separates them. await new Promise(setImmediate); // The queue can hold evidence against the settled cached graph itself: a // tracked dotenv edit only the outputs watcher has delivered yet queues // without invalidating, and the workspace watcher's recomputation may lag // this request. Classified against the exact graph the cache serves; an // in-flight computation needs no check here because its own pre-serve // replay consumes the queue. Content hashes are dropped before the // successor can read, as in the error retry: one recorded when the edit // was classified could suppress a revert landing mid-read. Fails toward // recomputing, since a stale graph on a dotenv edit is the bug this // prevents. try { if (servedGraphState && (0, dotenv_graph_changes_1.hasRelevantPendingDotEnvEvidence)(servedGraphState.graph, servedGraphState.generation)) { (0, dotenv_graph_changes_1.clearDotEnvFileHashes)(); invalidateGraphCache(); } } catch (e) { logger_1.serverLogger.log(`Failed to check pending dotenv changes against the cached graph; recomputing to be safe: ${e instanceof Error ? e.message : e}`); (0, dotenv_graph_changes_1.clearDotEnvFileHashes)(); invalidateGraphCache(); } // If no compute exists or events are still in collected*, kick one off. // Otherwise reuse whatever is already in flight or cached. const needsRecompute = !cachedSerializedProjectGraphPromise || collectedUpdatedFiles.size > 0 || collectedDeletedFiles.size > 0; if (needsRecompute) { logger_1.serverLogger.log(cachedSerializedProjectGraphPromise ? `Recomputing project graph because of ${collectedUpdatedFiles.size} updated and ${collectedDeletedFiles.size} deleted files.` : 'No in-memory cached project graph found. Recomputing it...'); kickOffRecompute(); } else { logger_1.serverLogger.log('Reusing in-memory cached project graph because no files changed.'); } // A stale compute returns cachedSerializedProjectGraphPromise (the // newer compute that replaced it); promise unwrapping flattens the // chain so we always end up with the latest real result. const result = await cachedSerializedProjectGraphPromise; // Even when the loop didn't recompute, write the cache if it's stale on // disk relative to the in-memory result. This protects against // non-daemon processes overwriting the daemon's valid graph with a // stale/errored one. if (!needsRecompute && result.projectGraph && result.projectFileMapCache && result.sourceMaps) { (0, nx_deps_cache_1.writeCacheIfStale)(result.projectFileMapCache, result.projectGraph, result.sourceMaps, extractErrors(result.error)); } const errors = extractErrors(result.error); if (errors?.length) { cachedSerializedProjectGraphPromise = null; } return result; } catch (e) { // We return the project graph, but we don't want to persist the cache to // serve the same state, as it could cause issues if the error is caused by something // transient cachedSerializedProjectGraphPromise = null; return { error: e, serializedProjectGraph: null, serializedSourceMaps: null, sourceMaps: null, projectGraph: null, projectFileMapCache: null, rustReferences: null, }; } finally { if (socket) { (0, client_socket_context_1.unsubscribeClientFromTopic)(socket, progress_topics_1.ProgressTopics.GraphConstruction); } } } function scheduleProjectGraphRecomputation(createdFiles, updatedFiles, deletedFiles) { ++fileChangeCounter; // Hash the changed files up front and drop no-op rewrites before they can // trigger an expensive recompute. Restoring a cached task output, a // `git checkout` back to the same content, or a formatter that changes // nothing all rewrite a file (new inode) the watcher reports as changed // even though the bytes are identical. updateFilesInContext updates the // workspace context and returns only the files whose content actually // changed. Hashing here — once per watcher batch — rather than inside the // recompute keeps it off the stale-retry path, which would otherwise see // "no change" after the first pass already updated the context hashes. perf_hooks_1.performance.mark('hash-watched-changes-start'); const changedFileHashes = createdFiles.length > 0 || updatedFiles.length > 0 || deletedFiles.length > 0 ? ((0, workspace_context_1.updateFilesInContext)(workspace_root_1.workspaceRoot, [...createdFiles, ...updatedFiles], deletedFiles) ?? {}) : {}; perf_hooks_1.performance.mark('hash-watched-changes-end'); perf_hooks_1.performance.measure('hash changed files from watcher', 'hash-watched-changes-start', 'hash-watched-changes-end'); for (const [f, hash] of Object.entries(changedFileHashes)) { collectedDeletedFiles.delete(f); collectedUpdatedFiles.set(f, { version: fileChangeCounter, hash }); } for (let f of deletedFiles) { collectedUpdatedFiles.delete(f); collectedDeletedFiles.set(f, fileChangeCounter); } // The native watcher already coalesces a burst of events into one batch, // so socket + listener notifications dispatch immediately. if (Object.keys(changedFileHashes).length > 0 || deletedFiles.length > 0) { (0, file_change_events_1.notifyFileChangeListeners)({ createdFiles, updatedFiles, deletedFiles }); (0, file_watcher_sockets_1.notifyFileWatcherSockets)(createdFiles, updatedFiles, deletedFiles); // Bump generation synchronously so any in-flight compute fails its // next isStale() check and chains to the newer one. kickOffRecompute // would also bump on first resume, but only after its first await — // a window during which the old compute could falsely pass. ++recomputationGeneration; kickOffRecompute(); } else { // First call (initial startup) — no events but we still need a graph. if (!cachedSerializedProjectGraphPromise) { kickOffRecompute(); } } } function registerProjectGraphRecomputationListener(listener) { projectGraphRecomputationListeners.add(listener); } function computeWorkspaceConfigHash(projectsConfigurations) { const projectConfigurationStrings = Object.entries(projectsConfigurations) .sort(([projectNameA], [projectNameB]) => projectNameA.localeCompare(projectNameB)) .map(([projectName, projectConfig]) => `${projectName}:${JSON.stringify(projectConfig)}`); return (0, file_hasher_1.hashArray)(projectConfigurationStrings); } async function processCollectedUpdatedAndDeletedFiles({ projects, externalNodes, projectRootMap }, updatedFileHashes, deletedFiles) { try { const configHash = computeWorkspaceConfigHash(projects); // Config changed → can't incrementally update; refetch the file map // from disk. Returning instead of mutating module state lets the caller // gate the commit on its staleness check, so a slower stale compute // can't clobber a faster newer one's already-committed state. if (configHash !== storedWorkspaceConfigHash) { const fresh = await (0, retrieve_workspace_files_1.retrieveWorkspaceFiles)(workspace_root_1.workspaceRoot, projectRootMap); return { fileMap: fresh, configHash, knownExternalNodes: externalNodes }; } // Config unchanged → patch the existing file map in place. if (exports.fileMapWithFiles) { return { fileMap: (0, file_map_utils_1.updateFileMap)(projects, exports.fileMapWithFiles.rustReferences, updatedFileHashes, deletedFiles), configHash, }; } // No prior map (first compute on this daemon). const fresh = await (0, retrieve_workspace_files_1.retrieveWorkspaceFiles)(workspace_root_1.workspaceRoot, projectRootMap); return { fileMap: fresh, configHash }; } catch (e) { // this is expected // for instance, project.json can be incorrect or a file we are trying to has // has been deleted // we are resetting internal state to start from scratch next time a file changes // given the user the opportunity to fix the error // if Nx requests the project graph prior to the error being fixed, // the error will be propagated logger_1.serverLogger.log(`Error detected when recomputing project file map: ${e.message}`); resetInternalState(); throw e; } } /** * Discards the cached graph when a graph input outside the file watcher's view * (e.g. the daemon env) changes. Clearing the cached promise makes the next * request trigger a fresh computation; the generation bump marks any in-flight * compute stale so it chains to that successor instead of committing, because * a compute passing its chainToLatest checks would serve a graph built under * the old input to whoever already awaits it. */ function invalidateGraphCache() { // We intentionally do NOT call getCachedSerializedProjectGraphPromise() here // because assigning its return Promise to the module-level variable causes a // deadlock: the async function resumes, sees the variable is non-null (pointing // at its own Promise), takes the "reuse" branch, and awaits itself forever. cachedSerializedProjectGraphPromise = null; ++recomputationGeneration; servedGraphState = null; } /** * The current recomputation generation, stamped on queued dotenv events so * the pre-serve drain can prove whether a computation started before an * event arrived. */ function getRecomputationGeneration() { return recomputationGeneration; } // isKnownWorkspaceFile's membership set, derived lazily from the map object it // was built from; every `fileMapWithFiles` write clears it, so a replaced map // generation is not retained through the memo. let knownWorkspaceFiles; let knownWorkspaceFilesSource; /** * Whether the ignore-filtered workspace file map knows `path`. The workspace * watcher applies the same ignore rules, so a change to a known file also * reaches scheduleProjectGraphRecomputation; an unknown file is either * ignored, or created since the last recompute committed. */ function isKnownWorkspaceFile(path) { if (!exports.fileMapWithFiles) { return false; } if (knownWorkspaceFilesSource !== exports.fileMapWithFiles) { const { projectFileMap, nonProjectFiles } = exports.fileMapWithFiles.fileMap; knownWorkspaceFiles = new Set(); for (const { file } of nonProjectFiles) { knownWorkspaceFiles.add(file); } for (const files of Object.values(projectFileMap)) { for (const { file } of files) { knownWorkspaceFiles.add(file); } } knownWorkspaceFilesSource = exports.fileMapWithFiles; } return knownWorkspaceFiles.has(path); } async function processFilesAndCreateAndSerializeProjectGraph(separatedPlugins) { const plugins = [ ...separatedPlugins.specifiedPlugins, ...separatedPlugins.defaultPlugins, ]; const myGeneration = ++recomputationGeneration; // A hash recorded before this claim can describe bytes this computation // never reads (a callback classified between a forced invalidation and this // claim records one no handler-side clear has seen), and retained it could // suppress a coalesced revert landing mid-read. Bounding every hash to the // window since the last claim closes that for any successor, however it was // forced. A callback landing after this claim re-records and re-queues, and // its stamp then marks this computation stale at the drain. (0, dotenv_graph_changes_1.clearDotEnvFileHashes)(); // A newer kickOffRecompute has already replaced // cachedSerializedProjectGraphPromise. Returning it lets the async // unwrap chain our caller onto the newer compute's result; pass // notifyAbort=true when the graph-phase counters still need balancing. const chainToLatest = (notifyAbort) => { if (myGeneration === recomputationGeneration) return null; if (notifyAbort) notifyPluginsGraphAborted(plugins); // Defensive: if the cache was cleared (e.g. resetInternalState ran) // there is nothing to chain to. Returning undefined lets `if (stale) // return stale` fall through and the compute commits stale data. // Kick off a successor so we always have a real promise to chain to. if (!cachedSerializedProjectGraphPromise) { kickOffRecompute(); } return cachedSerializedProjectGraphPromise; }; try { const updatedFilesSnapshot = new Map(collectedUpdatedFiles); const deletedFilesSnapshot = new Map(collectedDeletedFiles); const updatedFiles = [...updatedFilesSnapshot.keys()]; const deletedFiles = [...deletedFilesSnapshot.keys()]; // Hashes were already computed (and the workspace context updated) in // scheduleProjectGraphRecomputation, which also dropped no-op rewrites. // Reuse them so the context isn't re-hashed on every (possibly stale) // recompute attempt. const updatedFileHashes = {}; for (const [f, { hash }] of updatedFilesSnapshot) { updatedFileHashes[f] = hash; } logger_1.serverLogger.requestLog(`Updated workspace context based on watched changes, recomputing project graph...`); logger_1.serverLogger.requestLog(updatedFiles); logger_1.serverLogger.requestLog(deletedFiles); const nxJson = (0, nx_json_1.readNxJson)(workspace_root_1.workspaceRoot); global.NX_GRAPH_CREATION = true; let projectConfigurationsResult; let projectConfigurationsError; try { projectConfigurationsResult = await (0, retrieve_workspace_files_1.retrieveProjectConfigurations)(separatedPlugins, workspace_root_1.workspaceRoot, nxJson); } catch (e) { if (e instanceof error_types_1.ProjectConfigurationsError) { projectConfigurationsResult = e.partialProjectConfigurationsResult; projectConfigurationsError = e; } else { throw e; } } const stalePostCreateNodes = chainToLatest(true); if (stalePostCreateNodes) return stalePostCreateNodes; const fileMapUpdate = await processCollectedUpdatedAndDeletedFiles(projectConfigurationsResult, updatedFileHashes, deletedFiles); const stalePreCreateDependencies = chainToLatest(true); if (stalePreCreateDependencies) return stalePreCreateDependencies; // Latest writer commits to module state. Stale computes returned via // chainToLatest above without touching `fileMapWithFiles`, so they // can't clobber a newer compute's write. exports.fileMapWithFiles = fileMapUpdate.fileMap; knownWorkspaceFiles = undefined; knownWorkspaceFilesSource = undefined; storedWorkspaceConfigHash = fileMapUpdate.configHash; if (fileMapUpdate.knownExternalNodes) { knownExternalNodes = fileMapUpdate.knownExternalNodes; } // Drain only after committing — a stale compute that returns at the // staleness check above must leave its snapshot in `collected*` so the // newer compute still sees those files. Removing them earlier would // make the next compute snapshot empty and the file changes vanish // from the daemon's view (project graph misses recently added files). // Match version-stamps so a file modified mid-flight (higher version) // stays in the queue for reprocessing. for (const [f, { version }] of updatedFilesSnapshot) { if (collectedUpdatedFiles.get(f)?.version === version) { collectedUpdatedFiles.delete(f); } } for (const [f, version] of deletedFilesSnapshot) { if (collectedDeletedFiles.get(f) === version) { collectedDeletedFiles.delete(f); } } const g = await createAndSerializeProjectGraph(projectConfigurationsResult); delete global.NX_GRAPH_CREATION; // createDependencies/createMetadata already ran via wrapped hooks, so // graph-phase counters are balanced — no notifyAbort needed. const stalePostBuild = chainToLatest(false); if (stalePostBuild) return stalePostBuild; const errors = [...(projectConfigurationsError?.errors ?? [])]; const aggregate = g.error && (0, error_types_1.isAggregateProjectGraphError)(g.error) && g.error.errors?.length ? g.error : null; // An error result is never cached, notified, or persisted, so a queued // dotenv edit that may already have fixed the failing input earns one // retry instead of surfacing the error. The peek consumes nothing (a // successful successor's drain classifies the entries against a real // graph, which these branches may lack: `g.projectGraph` can be null // here) and a persistent error retries once, because the successor // claims a generation above every recorded stamp. Content hashes are // dropped, though: one recorded during this failed computation could // suppress a callback landing while the successor reads. The catch at // the end of this function stays excluded: a throw there can predate // projectConfigurationsResult and the plugin hooks' balancing, and the // next request recomputes at a fresh generation anyway. const retryForPendingDotEnv = () => { if (!(0, dotenv_graph_changes_1.hasPendingDotEnvEvidence)(myGeneration)) return null; (0, dotenv_graph_changes_1.clearDotEnvFileHashes)(); invalidateGraphCache(); return chainToLatest(false); }; if (g.error && !aggregate) { return retryForPendingDotEnv() ?? errorResult(g.error); } if (aggregate) errors.push(...aggregate.errors); if (errors.length > 0) { return (retryForPendingDotEnv() ?? errorResult(new error_types_1.DaemonProjectGraphError(errors, g.projectGraph, projectConfigurationsResult.sourceMaps))); } // Replay the dotenv events no committed root could classify on arrival // against the graph about to be served. Runs after the last staleness // gate so a stale compute leaves the queue for its successor, and only // on the success path: an error result is never notified or persisted. // Queue evidence is decisive here even for files the workspace watcher // tracks: the two watchers deliver independently, so tracking alone does // not prove that watcher's recomputation was already scheduled. let staleDotEnv; try { const pending = (0, dotenv_graph_changes_1.drainPendingDotEnvEvents)(g.projectGraph, myGeneration); staleDotEnv = pending.overflowed || pending.invalidating.length > 0; } catch (e) { logger_1.serverLogger.log(`Failed to re-check pending dotenv changes; recomputing to be safe: ${e instanceof Error ? e.message : e}`); staleDotEnv = true; } if (staleDotEnv) { // This graph read the file before the queued edit landed. The bump plus // chain hands every awaiting caller the successor's result and keeps // this one from being notified or persisted. invalidateGraphCache(); return chainToLatest(false); } servedGraphCandidate = { graph: g.projectGraph, generation: myGeneration }; return g; } catch (err) { return errorResult(err); } } function errorResult(error) { return { error, projectGraph: null, projectFileMapCache: null, rustReferences: null, serializedProjectGraph: null, serializedSourceMaps: null, sourceMaps: null, }; } function extractErrors(error) { if (!error) return []; return error instanceof error_types_1.DaemonProjectGraphError ? error.errors : [error]; } function persistProjectGraphToDisk(result) { if (!result.projectGraph || !result.projectFileMapCache || !result.sourceMaps) { return; } (0, nx_deps_cache_1.writeCache)(result.projectFileMapCache, result.projectGraph, result.sourceMaps, extractErrors(result.error)); cacheHasBeenPersisted = true; } function copyFileData(d) { return d.map((t) => ({ ...t })); } function copyFileMap(m) { const c = { nonProjectFiles: copyFileData(m.nonProjectFiles), projectFileMap: {}, }; for (let p of Object.keys(m.projectFileMap)) { c.projectFileMap[p] = copyFileData(m.projectFileMap[p]); } return c; } async function createAndSerializeProjectGraph({ projects, sourceMaps, }) { try { perf_hooks_1.performance.mark('create-project-graph-start'); const fileMap = copyFileMap(exports.fileMapWithFiles.fileMap); const rustReferences = exports.fileMapWithFiles.rustReferences; const { projectGraph, projectFileMapCache } = await (0, build_project_graph_1.buildProjectGraphUsingProjectFileMap)(projects, knownExternalNodes, fileMap, rustReferences, exports.currentProjectFileMapCache || (0, nx_deps_cache_1.readFileMapCache)(), await (0, get_plugins_1.getPlugins)((0, nx_json_1.readNxJson)(workspace_root_1.workspaceRoot)), sourceMaps); exports.currentProjectFileMapCache = projectFileMapCache; exports.currentProjectGraph = projectGraph; exports.currentSourceMaps = sourceMaps; perf_hooks_1.performance.mark('create-project-graph-end'); perf_hooks_1.performance.measure('total execution time for createProjectGraph()', 'create-project-graph-start', 'create-project-graph-end'); perf_hooks_1.performance.mark('json-stringify-start'); const serializedProjectGraph = JSON.stringify(projectGraph); const serializedSourceMaps = JSON.stringify(sourceMaps); perf_hooks_1.performance.mark('json-stringify-end'); perf_hooks_1.performance.measure('serialize graph', 'json-stringify-start', 'json-stringify-end'); return { error: null, projectGraph, projectFileMapCache, serializedProjectGraph, serializedSourceMaps, sourceMaps, rustReferences, }; } catch (e) { logger_1.serverLogger.log(`Error detected when creating a project graph: ${e.message}`); return { error: e, projectGraph: null, projectFileMapCache: null, serializedProjectGraph: null, serializedSourceMaps: null, sourceMaps: null, rustReferences: null, }; } } async function resetInternalState() { cachedSerializedProjectGraphPromise = undefined; servedGraphState = null; servedGraphCandidate = null; exports.fileMapWithFiles = undefined; knownWorkspaceFiles = undefined; knownWorkspaceFilesSource = undefined; exports.currentProjectFileMapCache = undefined; exports.currentProjectGraph = undefined; exports.currentSourceMaps = undefined; collectedUpdatedFiles.clear(); collectedDeletedFiles.clear(); cacheHasBeenPersisted = false; (0, workspace_context_1.resetWorkspaceContext)(); } async function resetInternalStateIfNxDepsMissing() { // Only meaningful AFTER we've persisted the cache at least once. // Before then, "file missing" is the expected state — an in-flight // first compute hasn't written yet, and resetting would tear down its // promise mid-await and force a redundant recompute. if (!cacheHasBeenPersisted) { return; } try { if (!(0, fileutils_1.fileExists)(nx_deps_cache_1.nxProjectGraph) && cachedSerializedProjectGraphPromise) { await resetInternalState(); } } catch { // A transient stat error shouldn't nuke state — the next request // will retry. } } function notifyPluginsGraphAborted(plugins) { // At both abort sites, only createNodes has been called. // createDependencies and createMetadata are called later in // createAndSerializeProjectGraph, which hasn't run yet. for (const plugin of plugins) { plugin.notifyPhaseAborted?.('graph', 'createNodes'); } } function notifyProjectGraphRecomputationListeners(projectGraph, sourceMaps, error) { for (const listener of projectGraphRecomputationListeners) { listener(projectGraph, sourceMaps, error); } (0, project_graph_listener_sockets_1.notifyProjectGraphListenerSockets)(projectGraph, sourceMaps, error); }