diff --git a/packages/nx/src/daemon/server/project-graph-incremental-recomputation.ts b/packages/nx/src/daemon/server/project-graph-incremental-recomputation.ts index 81f4830135ff7..4e3d569f8b546 100644 --- a/packages/nx/src/daemon/server/project-graph-incremental-recomputation.ts +++ b/packages/nx/src/daemon/server/project-graph-incremental-recomputation.ts @@ -316,7 +316,11 @@ async function processFilesAndCreateAndSerializeProjectGraph( serializedSourceMaps: null, }; } else { - writeCache(g.projectFileMapCache, g.projectGraph); + writeCache( + g.projectFileMapCache, + g.projectGraph, + projectConfigurationsResult.sourceMaps + ); return g; } } catch (err) { diff --git a/packages/nx/src/project-graph/nx-deps-cache.ts b/packages/nx/src/project-graph/nx-deps-cache.ts index 6b03530cb39b6..2c5d236403e6e 100644 --- a/packages/nx/src/project-graph/nx-deps-cache.ts +++ b/packages/nx/src/project-graph/nx-deps-cache.ts @@ -18,6 +18,7 @@ import { } from '../utils/fileutils'; import { PackageJson } from '../utils/package-json'; import { nxVersion } from '../utils/versions'; +import { ConfigurationSourceMaps } from './utils/project-configuration-utils'; export interface FileMapCache { version: string; @@ -34,6 +35,8 @@ export const nxProjectGraph = join( ); export const nxFileMap = join(workspaceDataDirectory, 'file-map.json'); +export const nxSourceMaps = join(workspaceDataDirectory, 'source-maps.json'); + export function ensureCacheDirectory(): void { try { if (!existsSync(workspaceDataDirectory)) { @@ -102,6 +105,31 @@ export function readProjectGraphCache(): null | ProjectGraph { return data ?? null; } +export function readSourceMapsCache(): null | ConfigurationSourceMaps { + performance.mark('read source-maps:start'); + ensureCacheDirectory(); + + let data = null; + try { + if (fileExists(nxSourceMaps)) { + data = readJsonFile(nxSourceMaps); + } + } catch (error) { + console.log( + `Error reading '${nxSourceMaps}'. Continue the process without the cache.` + ); + console.log(error); + } + + performance.mark('read source-maps:end'); + performance.measure( + 'read cache', + 'read source-maps:start', + 'read source-maps:end' + ); + return data ?? null; +} + export function createProjectFileMapCache( nxJson: NxJsonConfiguration<'*' | string[]>, packageJsonDeps: Record, @@ -123,7 +151,8 @@ export function createProjectFileMapCache( export function writeCache( cache: FileMapCache, - projectGraph: ProjectGraph + projectGraph: ProjectGraph, + sourceMaps: ConfigurationSourceMaps ): void { performance.mark('write cache:start'); let retry = 1; @@ -137,6 +166,7 @@ export function writeCache( const unique = (Math.random().toString(16) + '0000000').slice(2, 10); const tmpProjectGraphPath = `${nxProjectGraph}~${unique}`; const tmpFileMapPath = `${nxFileMap}~${unique}`; + const tmpSourceMapPath = `${nxFileMap}~${unique}`; try { writeJsonFile(tmpProjectGraphPath, projectGraph); @@ -144,6 +174,9 @@ export function writeCache( writeJsonFile(tmpFileMapPath, cache); renameSync(tmpFileMapPath, nxFileMap); + + writeJsonFile(tmpSourceMapPath, sourceMaps); + renameSync(tmpSourceMapPath, nxSourceMaps); done = true; } catch (err: any) { if (err instanceof Error) { diff --git a/packages/nx/src/project-graph/project-graph.ts b/packages/nx/src/project-graph/project-graph.ts index 69e37c6fa70c8..63691973dbc4d 100644 --- a/packages/nx/src/project-graph/project-graph.ts +++ b/packages/nx/src/project-graph/project-graph.ts @@ -25,6 +25,7 @@ import { import { readFileMapCache, readProjectGraphCache, + readSourceMapsCache, writeCache, } from './nx-deps-cache'; import { ConfigurationResult } from './utils/project-configuration-utils'; @@ -170,7 +171,7 @@ export async function buildProjectGraphAndSourceMapsWithoutDaemon() { throw new ProjectGraphError(errors, projectGraph, sourceMaps); } else { if (cacheEnabled) { - writeCache(projectFileMapCache, projectGraph); + writeCache(projectFileMapCache, projectGraph, sourceMaps); } return { projectGraph, sourceMaps }; } @@ -280,7 +281,16 @@ export async function createProjectGraphAndSourceMapsAsync( 'Waiting for graph construction in another process to complete' ); await lock.wait(); - return { projectGraph: readCachedGraphAndHydrateFileMap() }; + const sourceMaps = readSourceMapsCache(); + if (!sourceMaps) { + throw new Error( + 'The project graph was computed in another process, but the source maps are missing.' + ); + } + return { + projectGraph: await readCachedGraphAndHydrateFileMap(), + sourceMaps, + }; } lock.lock(); try { diff --git a/packages/nx/src/utils/file-lock.ts b/packages/nx/src/utils/file-lock.ts index 4f7fd2e8e94e9..2e8046cd44e22 100644 --- a/packages/nx/src/utils/file-lock.ts +++ b/packages/nx/src/utils/file-lock.ts @@ -1,4 +1,5 @@ -import { existsSync, rmSync, watch, writeFileSync } from 'fs'; +import { existsSync, rmSync, watch, writeFileSync, mkdirSync } from 'fs'; +import { dirname } from 'path'; export class FileLock { locked: boolean; @@ -6,7 +7,10 @@ export class FileLock { private lockFilePath: string; lockPromise: Promise; - constructor(private file: string) { + constructor(file: string) { + // Ensure the directory exists + mkdirSync(dirname(file), { recursive: true }); + this.lockFilePath = `${file}.lock`; this.locked = existsSync(this.lockFilePath); } @@ -16,7 +20,7 @@ export class FileLock { throw new Error(`File ${this.lockFilePath} is already locked`); } this.locked = true; - writeFileSync(this.file, ''); + writeFileSync(this.lockFilePath, ''); } unlock() { @@ -24,26 +28,35 @@ export class FileLock { throw new Error(`File ${this.lockFilePath} is not locked`); } this.locked = false; - rmSync(this.file); + try { + rmSync(this.lockFilePath); + } catch (err) { + if (err.code !== 'ENOENT') { + throw err; + } + } } wait(timeout?: number) { return new Promise((res, rej) => { try { + // If the file watcher is supported, we can use it to wait for the lock file to be deleted. let watcher = watch(this.lockFilePath); - watcher.on('change', (eventType) => { - if (eventType === 'delete') { + // For whatever reason, the node file watcher can sometimes + // emit rename events instead of delete events. + if (eventType === 'delete' || eventType === 'rename') { this.locked = false; - res(); watcher.close(); + res(); } }); } catch { // File watching is not supported let start = Date.now(); - setInterval(() => { - if (!this.locked || !existsSync(this.file)) { + let interval = setInterval(() => { + if (!this.locked || !existsSync(this.lockFilePath)) { + clearInterval(interval); res(); }