Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b05d5fee28 | |||
| 14c2d5ba3f | |||
| c43ee13eed |
@@ -8,9 +8,7 @@ import {
|
||||
normalizePath,
|
||||
type NxJsonConfiguration,
|
||||
type ProjectConfiguration,
|
||||
readJsonFile,
|
||||
type TargetConfiguration,
|
||||
writeJsonFile,
|
||||
} from '@nx/devkit';
|
||||
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
|
||||
import { loadConfigFile } from '@nx/devkit/src/utils/config-utils';
|
||||
@@ -19,6 +17,11 @@ import { getLockFileName } from '@nx/js';
|
||||
import { readdirSync } from 'fs';
|
||||
import { hashObject } from 'nx/src/devkit-internals';
|
||||
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
|
||||
import {
|
||||
PluginCache,
|
||||
readPluginCache,
|
||||
safeWritePluginCache,
|
||||
} from 'nx/src/utils/plugin-cache-utils';
|
||||
import { globWithWorkspaceContext } from 'nx/src/utils/workspace-context';
|
||||
import { dirname, join, relative } from 'path';
|
||||
import { NX_PLUGIN_OPTIONS } from '../utils/constants';
|
||||
@@ -31,20 +34,6 @@ export interface CypressPluginOptions {
|
||||
ciComponentTestingTargetName?: string;
|
||||
}
|
||||
|
||||
function readTargetsCache(cachePath: string): Record<string, CypressTargets> {
|
||||
try {
|
||||
return process.env.NX_CACHE_PROJECT_GRAPH !== 'false'
|
||||
? readJsonFile(cachePath)
|
||||
: {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writeTargetsToCache(cachePath: string, results: CypressTargets) {
|
||||
writeJsonFile(cachePath, results);
|
||||
}
|
||||
|
||||
const cypressConfigGlob = '**/cypress.config.{js,ts,mjs,cjs}';
|
||||
const defaultPatterns = {
|
||||
e2e: {
|
||||
@@ -67,17 +56,17 @@ export const createNodes: CreateNodesV2<CypressPluginOptions> = [
|
||||
workspaceDataDirectory,
|
||||
`cypress-${optionsHash}.hash`
|
||||
);
|
||||
const targetsCache = readTargetsCache(cachePath);
|
||||
const cache = readPluginCache<CypressTargets>(cachePath);
|
||||
try {
|
||||
return await createNodesFromFiles(
|
||||
(configFile, options, context) =>
|
||||
createNodesInternal(configFile, options, context, targetsCache),
|
||||
createNodesInternal(configFile, options, context, cache),
|
||||
configFiles,
|
||||
options,
|
||||
context
|
||||
);
|
||||
} finally {
|
||||
writeTargetsToCache(cachePath, targetsCache);
|
||||
safeWritePluginCache(cachePath, cache);
|
||||
}
|
||||
},
|
||||
];
|
||||
@@ -88,7 +77,7 @@ async function createNodesInternal(
|
||||
configFilePath: string,
|
||||
options: CypressPluginOptions,
|
||||
context: CreateNodesContextV2,
|
||||
targetsCache: CypressTargets
|
||||
cache: PluginCache<CypressTargets>
|
||||
) {
|
||||
options = normalizeOptions(options);
|
||||
const projectRoot = dirname(configFilePath);
|
||||
@@ -109,13 +98,13 @@ async function createNodesInternal(
|
||||
[getLockFileName(detectPackageManager(context.workspaceRoot))]
|
||||
);
|
||||
|
||||
targetsCache[hash] ??= await buildCypressTargets(
|
||||
configFilePath,
|
||||
projectRoot,
|
||||
options,
|
||||
context
|
||||
);
|
||||
const { targets, metadata } = targetsCache[hash];
|
||||
if (!cache.has(hash)) {
|
||||
cache.set(
|
||||
hash,
|
||||
await buildCypressTargets(configFilePath, projectRoot, options, context)
|
||||
);
|
||||
}
|
||||
const { targets, metadata } = cache.get(hash);
|
||||
|
||||
const project: Omit<ProjectConfiguration, 'root'> = {
|
||||
projectType: 'application',
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
export {
|
||||
signalToCode,
|
||||
createProjectRootMappingsFromProjectConfigurations,
|
||||
PluginCache,
|
||||
readPluginCache,
|
||||
safeWritePluginCache,
|
||||
safeWriteFileCache,
|
||||
} from 'nx/src/devkit-internals';
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { execFileSync, spawnSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import {
|
||||
logger,
|
||||
workspaceRoot,
|
||||
ProjectConfiguration,
|
||||
writeJsonFile,
|
||||
} from '@nx/devkit';
|
||||
import { existsSync, readdirSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { logger, workspaceRoot, ProjectConfiguration } from '@nx/devkit';
|
||||
import { hashWithWorkspaceContext } from 'nx/src/utils/workspace-context';
|
||||
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
|
||||
import { hashObject } from 'nx/src/hasher/file-hasher';
|
||||
import {
|
||||
PluginCache,
|
||||
readPluginCache,
|
||||
safeWritePluginCache,
|
||||
} from 'nx/src/utils/plugin-cache-utils';
|
||||
|
||||
export interface AnalysisSuccessResult {
|
||||
// Maps project file path -> node configuration
|
||||
@@ -26,7 +26,7 @@ export interface AnalysisErrorResult {
|
||||
}
|
||||
export type AnalysisResult = AnalysisSuccessResult | AnalysisErrorResult;
|
||||
|
||||
const analyzerCaches = new Map<string, Record<string, AnalysisSuccessResult>>();
|
||||
const analyzerCaches = new Map<string, PluginCache<AnalysisSuccessResult>>();
|
||||
|
||||
function getCachePathForOptionsHash(optionsHash: string): string {
|
||||
return join(workspaceDataDirectory, `dotnet-${optionsHash}.hash`);
|
||||
@@ -34,37 +34,21 @@ function getCachePathForOptionsHash(optionsHash: string): string {
|
||||
|
||||
function readAnalyzerCache(
|
||||
optionsHash: string
|
||||
): Record<string, AnalysisSuccessResult> {
|
||||
): PluginCache<AnalysisSuccessResult> {
|
||||
if (analyzerCaches.has(optionsHash)) {
|
||||
return analyzerCaches.get(optionsHash)!;
|
||||
}
|
||||
const cacheFilePath = getCachePathForOptionsHash(optionsHash);
|
||||
try {
|
||||
return JSON.parse(readFileSync(cacheFilePath, 'utf-8'));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
return readPluginCache<AnalysisSuccessResult>(cacheFilePath);
|
||||
}
|
||||
|
||||
function writeAnalyzerCache(
|
||||
optionsHash: string,
|
||||
cache: Record<string, AnalysisSuccessResult>
|
||||
cache: PluginCache<AnalysisSuccessResult>
|
||||
): void {
|
||||
analyzerCaches.set(optionsHash, cache);
|
||||
const cacheFilePath = getCachePathForOptionsHash(optionsHash);
|
||||
const cacheDir = dirname(cacheFilePath);
|
||||
if (!existsSync(cacheDir)) {
|
||||
mkdirSync(cacheDir, { recursive: true });
|
||||
}
|
||||
try {
|
||||
writeJsonFile(cacheFilePath, cache);
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`Failed to write .NET analyzer cache to ${cacheFilePath}: ${
|
||||
(error as Error).message
|
||||
}`
|
||||
);
|
||||
}
|
||||
safeWritePluginCache(cacheFilePath, cache);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,7 +231,7 @@ export async function analyzeProjects(
|
||||
|
||||
const optionsHash = hashObject(options);
|
||||
const analyzerCache = readAnalyzerCache(optionsHash);
|
||||
const cachedResult = analyzerCache[filesHash];
|
||||
const cachedResult = analyzerCache.get(filesHash);
|
||||
if (cachedResult) {
|
||||
// Update cache
|
||||
cache = {
|
||||
@@ -267,10 +251,8 @@ export async function analyzeProjects(
|
||||
result,
|
||||
};
|
||||
// Update persistent cache
|
||||
writeAnalyzerCache(optionsHash, {
|
||||
...analyzerCache,
|
||||
[filesHash]: result,
|
||||
});
|
||||
analyzerCache.set(filesHash, result);
|
||||
writeAnalyzerCache(optionsHash, analyzerCache);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
|
||||
@@ -4,15 +4,17 @@ import {
|
||||
ProjectConfiguration,
|
||||
TargetConfiguration,
|
||||
createNodesFromFiles,
|
||||
readJsonFile,
|
||||
writeJsonFile,
|
||||
logger,
|
||||
} from '@nx/devkit';
|
||||
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { basename, dirname, join } from 'node:path';
|
||||
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
|
||||
import { findProjectForPath } from 'nx/src/devkit-internals';
|
||||
import {
|
||||
PluginCache,
|
||||
readPluginCache,
|
||||
safeWritePluginCache,
|
||||
} from 'nx/src/utils/plugin-cache-utils';
|
||||
|
||||
import {
|
||||
populateGradleReport,
|
||||
@@ -59,14 +61,6 @@ function normalizeOptions(options: GradlePluginOptions): GradlePluginOptions {
|
||||
|
||||
type GradleTargets = Record<string, Partial<ProjectConfiguration>>;
|
||||
|
||||
function readTargetsCache(cachePath: string): GradleTargets {
|
||||
return existsSync(cachePath) ? readJsonFile(cachePath) : {};
|
||||
}
|
||||
|
||||
export function writeTargetsToCache(cachePath: string, results: GradleTargets) {
|
||||
writeJsonFile(cachePath, results);
|
||||
}
|
||||
|
||||
export const createNodesV2: CreateNodesV2<GradlePluginOptions> = [
|
||||
gradleConfigAndTestGlob,
|
||||
async (files, options, context) => {
|
||||
@@ -77,7 +71,7 @@ export const createNodesV2: CreateNodesV2<GradlePluginOptions> = [
|
||||
workspaceDataDirectory,
|
||||
`gradle-${optionsHash}.hash`
|
||||
);
|
||||
const targetsCache = readTargetsCache(cachePath);
|
||||
const cache = readPluginCache<Partial<ProjectConfiguration>>(cachePath);
|
||||
|
||||
await populateGradleReport(
|
||||
context.workspaceRoot,
|
||||
@@ -93,7 +87,7 @@ export const createNodesV2: CreateNodesV2<GradlePluginOptions> = [
|
||||
return createNodesFromFiles(
|
||||
makeCreateNodesForGradleConfigFile(
|
||||
gradleReport,
|
||||
targetsCache,
|
||||
cache,
|
||||
gradleProjectRootToTestFilesMap
|
||||
),
|
||||
buildFiles,
|
||||
@@ -101,7 +95,7 @@ export const createNodesV2: CreateNodesV2<GradlePluginOptions> = [
|
||||
context
|
||||
);
|
||||
} finally {
|
||||
writeTargetsToCache(cachePath, targetsCache);
|
||||
safeWritePluginCache(cachePath, cache);
|
||||
}
|
||||
},
|
||||
];
|
||||
@@ -109,7 +103,7 @@ export const createNodesV2: CreateNodesV2<GradlePluginOptions> = [
|
||||
export const makeCreateNodesForGradleConfigFile =
|
||||
(
|
||||
gradleReport: GradleReport,
|
||||
targetsCache: GradleTargets = {},
|
||||
cache: PluginCache<Partial<ProjectConfiguration>> = new PluginCache(),
|
||||
gradleProjectRootToTestFilesMap: Record<string, string[]> = {}
|
||||
) =>
|
||||
async (
|
||||
@@ -125,14 +119,19 @@ export const makeCreateNodesForGradleConfigFile =
|
||||
options ?? {},
|
||||
context
|
||||
);
|
||||
targetsCache[hash] ??= await createGradleProject(
|
||||
gradleReport,
|
||||
gradleFilePath,
|
||||
options,
|
||||
context,
|
||||
gradleProjectRootToTestFilesMap[projectRoot]
|
||||
);
|
||||
const project = targetsCache[hash];
|
||||
if (!cache.has(hash)) {
|
||||
cache.set(
|
||||
hash,
|
||||
await createGradleProject(
|
||||
gradleReport,
|
||||
gradleFilePath,
|
||||
options,
|
||||
context,
|
||||
gradleProjectRootToTestFilesMap[projectRoot]
|
||||
)
|
||||
);
|
||||
}
|
||||
const project = cache.get(hash);
|
||||
if (!project) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -2,16 +2,18 @@ import {
|
||||
CreateNodesV2,
|
||||
CreateNodesContextV2,
|
||||
ProjectConfiguration,
|
||||
readJsonFile,
|
||||
writeJsonFile,
|
||||
workspaceRoot,
|
||||
ProjectGraphExternalNode,
|
||||
normalizePath,
|
||||
} from '@nx/devkit';
|
||||
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
|
||||
import {
|
||||
PluginCache,
|
||||
readPluginCache,
|
||||
safeWritePluginCache,
|
||||
} from 'nx/src/utils/plugin-cache-utils';
|
||||
|
||||
import { hashObject } from 'nx/src/hasher/file-hasher';
|
||||
import {
|
||||
@@ -29,10 +31,6 @@ import {
|
||||
|
||||
type GradleTargets = Record<string, Partial<ProjectConfiguration>>;
|
||||
|
||||
function readProjectsCache(cachePath: string): GradleTargets {
|
||||
return existsSync(cachePath) ? readJsonFile(cachePath) : {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips nxConfig from project and all targets, returning only Gradle-detected configuration.
|
||||
*/
|
||||
@@ -111,10 +109,6 @@ function extractNxConfigOnly(
|
||||
return result;
|
||||
}
|
||||
|
||||
export function writeTargetsToCache(cachePath: string, results: GradleTargets) {
|
||||
writeJsonFile(cachePath, results);
|
||||
}
|
||||
|
||||
export const createNodesV2: CreateNodesV2<GradlePluginOptions> = [
|
||||
gradleConfigAndTestGlob,
|
||||
async (files, options, context) => {
|
||||
@@ -125,7 +119,7 @@ export const createNodesV2: CreateNodesV2<GradlePluginOptions> = [
|
||||
workspaceDataDirectory,
|
||||
`gradle-${optionsHash}.hash`
|
||||
);
|
||||
const projectsCache = readProjectsCache(cachePath);
|
||||
const cache = readPluginCache<Partial<ProjectConfiguration>>(cachePath);
|
||||
|
||||
await populateProjectGraph(
|
||||
context.workspaceRoot,
|
||||
@@ -153,9 +147,14 @@ export const createNodesV2: CreateNodesV2<GradlePluginOptions> = [
|
||||
);
|
||||
|
||||
// Get project from cache or nodes
|
||||
projectsCache[hash] ??=
|
||||
nodes[projectRoot] ?? nodes[join(workspaceRoot, projectRoot)];
|
||||
const project = projectsCache[hash];
|
||||
if (!cache.has(hash)) {
|
||||
const nodeProject =
|
||||
nodes[projectRoot] ?? nodes[join(workspaceRoot, projectRoot)];
|
||||
if (nodeProject) {
|
||||
cache.set(hash, nodeProject);
|
||||
}
|
||||
}
|
||||
const project = cache.get(hash);
|
||||
|
||||
if (!project) {
|
||||
continue;
|
||||
@@ -195,7 +194,7 @@ export const createNodesV2: CreateNodesV2<GradlePluginOptions> = [
|
||||
|
||||
return results;
|
||||
} finally {
|
||||
writeTargetsToCache(cachePath, projectsCache);
|
||||
safeWritePluginCache(cachePath, cache);
|
||||
}
|
||||
},
|
||||
];
|
||||
@@ -203,7 +202,9 @@ export const createNodesV2: CreateNodesV2<GradlePluginOptions> = [
|
||||
export const makeCreateNodesForGradleConfigFile =
|
||||
(
|
||||
projects: Record<string, Partial<ProjectConfiguration>>,
|
||||
projectsCache: GradleTargets = {},
|
||||
projectsCache: PluginCache<
|
||||
Partial<ProjectConfiguration>
|
||||
> = new PluginCache(),
|
||||
externalNodes: Record<string, ProjectGraphExternalNode> = {}
|
||||
) =>
|
||||
async (
|
||||
@@ -219,9 +220,14 @@ export const makeCreateNodesForGradleConfigFile =
|
||||
options ?? {},
|
||||
context
|
||||
);
|
||||
projectsCache[hash] ??=
|
||||
projects[projectRoot] ?? projects[join(workspaceRoot, projectRoot)];
|
||||
const project = projectsCache[hash];
|
||||
if (!projectsCache.has(hash)) {
|
||||
const nodeProject =
|
||||
projects[projectRoot] ?? projects[join(workspaceRoot, projectRoot)];
|
||||
if (nodeProject) {
|
||||
projectsCache.set(hash, nodeProject);
|
||||
}
|
||||
}
|
||||
const project = projectsCache.get(hash);
|
||||
if (!project) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { join } from 'node:path';
|
||||
import {
|
||||
AggregateCreateNodesError,
|
||||
hashArray,
|
||||
logger,
|
||||
ProjectConfiguration,
|
||||
ProjectGraphExternalNode,
|
||||
readJsonFile,
|
||||
@@ -56,7 +57,15 @@ export function writeProjectGraphReportToCache(
|
||||
...results,
|
||||
};
|
||||
|
||||
writeJsonFile(cachePath, projectGraphReportJson);
|
||||
try {
|
||||
writeJsonFile(cachePath, projectGraphReportJson);
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
`Failed to write Gradle project graph report cache to ${cachePath}: ${
|
||||
e instanceof Error ? e.message : 'unknown error'
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let projectGraphReportCache: ProjectGraphReport;
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import { join } from 'path';
|
||||
import { readJsonFile, writeJsonFile } from '@nx/devkit';
|
||||
import { MavenAnalysisData } from './types';
|
||||
import {
|
||||
PluginCache,
|
||||
readPluginCache,
|
||||
safeWritePluginCache,
|
||||
} from 'nx/src/utils/plugin-cache-utils';
|
||||
|
||||
/**
|
||||
* Read the Maven targets cache from disk
|
||||
*/
|
||||
export function readMavenCache(
|
||||
cachePath: string
|
||||
): Record<string, MavenAnalysisData> {
|
||||
try {
|
||||
return process.env.NX_CACHE_PROJECT_GRAPH !== 'false'
|
||||
? readJsonFile(cachePath)
|
||||
: {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
): PluginCache<MavenAnalysisData> {
|
||||
return readPluginCache<MavenAnalysisData>(cachePath);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -22,9 +20,9 @@ export function readMavenCache(
|
||||
*/
|
||||
export function writeMavenCache(
|
||||
cachePath: string,
|
||||
cache: Record<string, MavenAnalysisData>
|
||||
cache: PluginCache<MavenAnalysisData>
|
||||
): void {
|
||||
writeJsonFile(cachePath, cache);
|
||||
safeWritePluginCache(cachePath, cache);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -54,7 +54,7 @@ export const createNodes: CreateNodesV2<MavenPluginOptions> = [
|
||||
|
||||
try {
|
||||
// Try to get cached data first (skip cache if in verbose mode)
|
||||
let mavenData = isVerbose ? null : mavenCache[hash];
|
||||
let mavenData = isVerbose ? null : mavenCache.get(hash);
|
||||
|
||||
// If no cached data or cache is stale, run fresh Maven analysis
|
||||
if (!mavenData) {
|
||||
@@ -63,7 +63,7 @@ export const createNodes: CreateNodesV2<MavenPluginOptions> = [
|
||||
verbose: isVerbose,
|
||||
});
|
||||
// Cache the results with the hash
|
||||
mavenCache[hash] = mavenData;
|
||||
mavenCache.set(hash, mavenData);
|
||||
}
|
||||
|
||||
// Store in module-level variable for createDependencies to use
|
||||
|
||||
@@ -8,24 +8,19 @@ import {
|
||||
import { workspaceDataDirectory } from '../src/utils/cache-directory';
|
||||
import { join } from 'path';
|
||||
import { ProjectConfiguration } from '../src/config/workspace-json-project-json';
|
||||
import { readJsonFile, writeJsonFile } from '../src/utils/fileutils';
|
||||
import { readJsonFile } from '../src/utils/fileutils';
|
||||
import {
|
||||
PluginCache,
|
||||
readPluginCache,
|
||||
safeWritePluginCache,
|
||||
} from '../src/utils/plugin-cache-utils';
|
||||
|
||||
export type PackageJsonConfigurationCache = {
|
||||
[hash: string]: ProjectConfiguration;
|
||||
};
|
||||
export type PackageJsonConfigurationCache = PluginCache<ProjectConfiguration>;
|
||||
|
||||
const cachePath = join(workspaceDataDirectory, 'package-json.hash');
|
||||
|
||||
export function readPackageJsonConfigurationCache() {
|
||||
try {
|
||||
return readJsonFile<PackageJsonConfigurationCache>(cachePath);
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writeCache(cache: PackageJsonConfigurationCache) {
|
||||
writeJsonFile(cachePath, cache);
|
||||
export function readPackageJsonConfigurationCache(): PackageJsonConfigurationCache {
|
||||
return readPluginCache<ProjectConfiguration>(cachePath);
|
||||
}
|
||||
|
||||
const plugin: NxPluginV2 = {
|
||||
@@ -54,7 +49,7 @@ const plugin: NxPluginV2 = {
|
||||
context
|
||||
);
|
||||
|
||||
writeCache(cache);
|
||||
safeWritePluginCache(cachePath, cache);
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
@@ -47,3 +47,9 @@ export { isUsingPrettierInTree } from './utils/is-using-prettier';
|
||||
export { readYamlFile } from './utils/fileutils';
|
||||
export { globalSpinner } from './utils/spinner';
|
||||
export { signalToCode } from './utils/exit-codes';
|
||||
export {
|
||||
PluginCache,
|
||||
readPluginCache,
|
||||
safeWritePluginCache,
|
||||
safeWriteFileCache,
|
||||
} from './utils/plugin-cache-utils';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { execSync } from 'child_process';
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { mkdirSync, readFileSync } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
import { performance } from 'perf_hooks';
|
||||
import {
|
||||
@@ -19,6 +19,7 @@ import { workspaceDataDirectory } from '../../utils/cache-directory';
|
||||
import { combineGlobPatterns } from '../../utils/globs';
|
||||
import { detectPackageManager } from '../../utils/package-manager';
|
||||
import { nxVersion } from '../../utils/versions';
|
||||
import { safeWriteFileCache } from '../../utils/plugin-cache-utils';
|
||||
import { workspaceRoot } from '../../utils/workspace-root';
|
||||
import { readBunLockFile } from './lock-file/bun-parser';
|
||||
import {
|
||||
@@ -206,11 +207,10 @@ function writeExternalNodesCache(
|
||||
nodes: ProjectGraph['externalNodes'],
|
||||
keyMap: Map<string, any>
|
||||
) {
|
||||
mkdirSync(dirname(externalNodesHashFile), { recursive: true });
|
||||
const serializedKeyMap = serializeKeyMap(keyMap);
|
||||
const cacheData = { nodes, keyMap: serializedKeyMap };
|
||||
writeFileSync(externalNodesCache, JSON.stringify(cacheData, null, 2));
|
||||
writeFileSync(externalNodesHashFile, hash);
|
||||
safeWriteFileCache(externalNodesCache, JSON.stringify(cacheData, null, 2));
|
||||
safeWriteFileCache(externalNodesHashFile, hash);
|
||||
}
|
||||
|
||||
function readCachedExternalNodes(): {
|
||||
@@ -228,9 +228,8 @@ function writeDependenciesCache(
|
||||
hash: string,
|
||||
dependencies: RawProjectGraphDependency[]
|
||||
) {
|
||||
mkdirSync(dirname(dependenciesHashFile), { recursive: true });
|
||||
writeFileSync(dependenciesCache, JSON.stringify(dependencies, null, 2));
|
||||
writeFileSync(dependenciesHashFile, hash);
|
||||
safeWriteFileCache(dependenciesCache, JSON.stringify(dependencies, null, 2));
|
||||
safeWriteFileCache(dependenciesHashFile, hash);
|
||||
}
|
||||
|
||||
function readCachedDependencies(): RawProjectGraphDependency[] {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import '../../internal-testing-utils/mock-fs';
|
||||
|
||||
import { vol } from 'memfs';
|
||||
import { PluginCache } from '../../utils/plugin-cache-utils';
|
||||
import { createNodeFromPackageJson, createNodesV2 } from './create-nodes';
|
||||
|
||||
describe('nx package.json workspaces plugin', () => {
|
||||
@@ -47,8 +48,14 @@ describe('nx package.json workspaces plugin', () => {
|
||||
'/root'
|
||||
);
|
||||
|
||||
expect(createNodeFromPackageJson('package.json', '/root', {}, false))
|
||||
.toMatchInlineSnapshot(`
|
||||
expect(
|
||||
createNodeFromPackageJson(
|
||||
'package.json',
|
||||
'/root',
|
||||
new PluginCache(),
|
||||
false
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"projects": {
|
||||
".": {
|
||||
@@ -99,7 +106,7 @@ describe('nx package.json workspaces plugin', () => {
|
||||
createNodeFromPackageJson(
|
||||
'packages/lib-a/package.json',
|
||||
'/root',
|
||||
{},
|
||||
new PluginCache(),
|
||||
false
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
@@ -153,7 +160,7 @@ describe('nx package.json workspaces plugin', () => {
|
||||
createNodeFromPackageJson(
|
||||
'packages/lib-b/package.json',
|
||||
'/root',
|
||||
{},
|
||||
new PluginCache(),
|
||||
false
|
||||
)
|
||||
).toMatchInlineSnapshot(`
|
||||
@@ -794,15 +801,19 @@ describe('nx package.json workspaces plugin', () => {
|
||||
);
|
||||
|
||||
expect(
|
||||
createNodeFromPackageJson('apps/myapp/package.json', '/root', {}, false)
|
||||
.projects['apps/myapp'].projectType
|
||||
createNodeFromPackageJson(
|
||||
'apps/myapp/package.json',
|
||||
'/root',
|
||||
new PluginCache(),
|
||||
false
|
||||
).projects['apps/myapp'].projectType
|
||||
).toEqual('application');
|
||||
|
||||
expect(
|
||||
createNodeFromPackageJson(
|
||||
'packages/mylib/package.json',
|
||||
'/root',
|
||||
{},
|
||||
new PluginCache(),
|
||||
false
|
||||
).projects['packages/mylib'].projectType
|
||||
).toEqual('library');
|
||||
@@ -826,9 +837,12 @@ describe('nx package.json workspaces plugin', () => {
|
||||
);
|
||||
|
||||
expect(
|
||||
createNodeFromPackageJson('package.json', '/root', {}, false).projects[
|
||||
'.'
|
||||
].projectType
|
||||
createNodeFromPackageJson(
|
||||
'package.json',
|
||||
'/root',
|
||||
new PluginCache(),
|
||||
false
|
||||
).projects['.'].projectType
|
||||
).toEqual('library');
|
||||
});
|
||||
|
||||
@@ -856,13 +870,17 @@ describe('nx package.json workspaces plugin', () => {
|
||||
createNodeFromPackageJson(
|
||||
'packages/mylib/package.json',
|
||||
'/root',
|
||||
{},
|
||||
new PluginCache(),
|
||||
false
|
||||
).projects['packages/mylib'].projectType
|
||||
).toEqual('library');
|
||||
expect(
|
||||
createNodeFromPackageJson('example/package.json', '/root', {}, false)
|
||||
.projects['example'].projectType
|
||||
createNodeFromPackageJson(
|
||||
'example/package.json',
|
||||
'/root',
|
||||
new PluginCache(),
|
||||
false
|
||||
).projects['example'].projectType
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ export function createNodeFromPackageJson(
|
||||
nxVersion,
|
||||
});
|
||||
|
||||
const cached = cache[hash];
|
||||
const cached = cache.get(hash);
|
||||
if (cached) {
|
||||
return {
|
||||
projects: {
|
||||
@@ -203,7 +203,7 @@ export function createNodeFromPackageJson(
|
||||
isInPackageManagerWorkspaces
|
||||
);
|
||||
|
||||
cache[hash] = project;
|
||||
cache.set(hash, project);
|
||||
return {
|
||||
projects: {
|
||||
[project.root]: project,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { existsSync, mkdirSync, renameSync } from 'node:fs';
|
||||
import { existsSync, mkdirSync, renameSync, rmSync } from 'node:fs';
|
||||
import { join } from 'path';
|
||||
import { performance } from 'perf_hooks';
|
||||
import { NxJsonConfiguration } from '../config/nx-json';
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
} from '../config/project-graph';
|
||||
import { ProjectConfiguration } from '../config/workspace-json-project-json';
|
||||
import { workspaceDataDirectory } from '../utils/cache-directory';
|
||||
import { logger } from '../utils/logger';
|
||||
import {
|
||||
directoryExists,
|
||||
fileExists,
|
||||
@@ -261,9 +262,12 @@ export function writeCache(
|
||||
}
|
||||
} while (!done && retry < 5);
|
||||
if (!done) {
|
||||
throw new Error(
|
||||
`Failed to write project graph cache to ${nxProjectGraph} and ${nxFileMap} after 5 attempts.`
|
||||
logger.warn(
|
||||
`Failed to write project graph cache to ${nxProjectGraph} and ${nxFileMap} after 5 attempts. Continuing without cache.`
|
||||
);
|
||||
tryRemoveFile(nxProjectGraph);
|
||||
tryRemoveFile(nxFileMap);
|
||||
tryRemoveFile(nxSourceMaps);
|
||||
}
|
||||
performance.mark('write cache:end');
|
||||
performance.measure('write cache', 'write cache:start', 'write cache:end');
|
||||
@@ -439,6 +443,16 @@ type PluginData = {
|
||||
options?: unknown;
|
||||
};
|
||||
|
||||
function tryRemoveFile(path: string): void {
|
||||
try {
|
||||
if (existsSync(path)) {
|
||||
rmSync(path);
|
||||
}
|
||||
} catch {
|
||||
// Best effort
|
||||
}
|
||||
}
|
||||
|
||||
function getNxJsonPluginsData(
|
||||
nxJson: NxJsonConfiguration,
|
||||
packageJsonDeps: Record<string, string>
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import {
|
||||
PluginCache,
|
||||
readPluginCache,
|
||||
safeWriteFileCache,
|
||||
safeWritePluginCache,
|
||||
} from './plugin-cache-utils';
|
||||
|
||||
describe('plugin-cache-utils', () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = mkdtempSync(join(tmpdir(), 'nx-cache-utils-test-'));
|
||||
});
|
||||
|
||||
describe('PluginCache', () => {
|
||||
it('should return value on get and track access', () => {
|
||||
const cache = new PluginCache<string>({ a: '1', b: '2' }, ['a', 'b']);
|
||||
expect(cache.get('a')).toBe('1');
|
||||
expect(cache.get('missing')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should store value on set and track access', () => {
|
||||
const cache = new PluginCache<string>({}, []);
|
||||
cache.set('x', 'hello');
|
||||
expect(cache.get('x')).toBe('hello');
|
||||
});
|
||||
|
||||
it('should check existence with has without tracking', () => {
|
||||
const cache = new PluginCache<string>({ a: '1' }, ['a']);
|
||||
expect(cache.has('a')).toBe(true);
|
||||
expect(cache.has('z')).toBe(false);
|
||||
});
|
||||
|
||||
it('should move accessed keys to end of access order', () => {
|
||||
const cache = new PluginCache<string>({ a: '1', b: '2', c: '3' }, [
|
||||
'a',
|
||||
'b',
|
||||
'c',
|
||||
]);
|
||||
cache.get('a'); // access oldest key
|
||||
|
||||
const serialized = cache.toSerializable();
|
||||
// 'a' should move to end, b and c stay in front
|
||||
expect(serialized.accessOrder).toEqual(['b', 'c', 'a']);
|
||||
});
|
||||
|
||||
it('should put set keys at end of access order', () => {
|
||||
const cache = new PluginCache<string>({ a: '1', b: '2' }, ['a', 'b']);
|
||||
cache.set('c', '3');
|
||||
|
||||
const serialized = cache.toSerializable();
|
||||
expect(serialized.accessOrder).toEqual(['a', 'b', 'c']);
|
||||
expect(serialized.entries['c']).toBe('3');
|
||||
});
|
||||
|
||||
it('should dedupe session log keeping last occurrence', () => {
|
||||
const cache = new PluginCache<string>({ a: '1', b: '2' }, ['a', 'b']);
|
||||
cache.get('a');
|
||||
cache.get('b');
|
||||
cache.get('a'); // a is most recent now
|
||||
|
||||
const serialized = cache.toSerializable();
|
||||
expect(serialized.accessOrder).toEqual(['b', 'a']);
|
||||
});
|
||||
|
||||
it('should cap entries at maxEntries, dropping oldest', () => {
|
||||
const entries: Record<string, string> = {};
|
||||
const order: string[] = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
entries[`key${i}`] = `val${i}`;
|
||||
order.push(`key${i}`);
|
||||
}
|
||||
const cache = new PluginCache<string>(entries, order);
|
||||
|
||||
const serialized = cache.toSerializable(5);
|
||||
expect(Object.keys(serialized.entries)).toHaveLength(5);
|
||||
// Should keep the last 5 (most recent by order)
|
||||
expect(serialized.entries['key5']).toBe('val5');
|
||||
expect(serialized.entries['key9']).toBe('val9');
|
||||
expect(serialized.entries['key0']).toBeUndefined();
|
||||
expect(serialized.accessOrder).toEqual([
|
||||
'key5',
|
||||
'key6',
|
||||
'key7',
|
||||
'key8',
|
||||
'key9',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not cap when under maxEntries', () => {
|
||||
const cache = new PluginCache<string>({ a: '1', b: '2' }, ['a', 'b']);
|
||||
const serialized = cache.toSerializable(500);
|
||||
expect(Object.keys(serialized.entries)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should handle new keys not in original accessOrder', () => {
|
||||
const cache = new PluginCache<string>({ a: '1' }, []);
|
||||
// 'a' exists but has no prior access order entry
|
||||
const serialized = cache.toSerializable();
|
||||
expect(serialized.accessOrder).toContain('a');
|
||||
});
|
||||
|
||||
it('should prioritize accessed keys over unaccessed when capping', () => {
|
||||
const cache = new PluginCache<string>({ old: 'x', fresh: 'y' }, [
|
||||
'old',
|
||||
'fresh',
|
||||
]);
|
||||
cache.get('fresh'); // mark as recently accessed
|
||||
|
||||
const serialized = cache.toSerializable(1);
|
||||
// Should keep 'fresh' (accessed) over 'old' (not accessed this session)
|
||||
expect(serialized.entries['fresh']).toBe('y');
|
||||
expect(serialized.entries['old']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('readPluginCache', () => {
|
||||
it('should read current format with entries and accessOrder', () => {
|
||||
const cachePath = join(tempDir, 'cache.json');
|
||||
writeFileSync(
|
||||
cachePath,
|
||||
JSON.stringify({
|
||||
entries: { a: '1', b: '2' },
|
||||
accessOrder: ['b', 'a'],
|
||||
})
|
||||
);
|
||||
|
||||
const cache = readPluginCache<string>(cachePath);
|
||||
expect(cache.get('a')).toBe('1');
|
||||
expect(cache.get('b')).toBe('2');
|
||||
});
|
||||
|
||||
it('should migrate legacy plain Record format', () => {
|
||||
const cachePath = join(tempDir, 'legacy.json');
|
||||
writeFileSync(cachePath, JSON.stringify({ a: '1', b: '2' }));
|
||||
|
||||
const cache = readPluginCache<string>(cachePath);
|
||||
expect(cache.get('a')).toBe('1');
|
||||
expect(cache.get('b')).toBe('2');
|
||||
});
|
||||
|
||||
it('should return empty cache if file does not exist', () => {
|
||||
const cache = readPluginCache<string>(join(tempDir, 'nope.json'));
|
||||
expect(cache.has('anything')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return empty cache if file is corrupted', () => {
|
||||
const cachePath = join(tempDir, 'bad.json');
|
||||
writeFileSync(cachePath, 'not json!!!');
|
||||
|
||||
const cache = readPluginCache<string>(cachePath);
|
||||
expect(cache.has('anything')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return empty cache when NX_CACHE_PROJECT_GRAPH is false', () => {
|
||||
const cachePath = join(tempDir, 'cache.json');
|
||||
writeFileSync(
|
||||
cachePath,
|
||||
JSON.stringify({ entries: { a: '1' }, accessOrder: ['a'] })
|
||||
);
|
||||
|
||||
const origEnv = process.env.NX_CACHE_PROJECT_GRAPH;
|
||||
process.env.NX_CACHE_PROJECT_GRAPH = 'false';
|
||||
try {
|
||||
const cache = readPluginCache<string>(cachePath);
|
||||
expect(cache.has('a')).toBe(false);
|
||||
} finally {
|
||||
if (origEnv === undefined) {
|
||||
delete process.env.NX_CACHE_PROJECT_GRAPH;
|
||||
} else {
|
||||
process.env.NX_CACHE_PROJECT_GRAPH = origEnv;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('safeWritePluginCache', () => {
|
||||
it('should write cache with entries and accessOrder', () => {
|
||||
const cachePath = join(tempDir, 'plugin-cache.json');
|
||||
const cache = new PluginCache<string>({ a: '1' }, ['a']);
|
||||
|
||||
safeWritePluginCache(cachePath, cache);
|
||||
|
||||
const written = JSON.parse(readFileSync(cachePath, 'utf-8'));
|
||||
expect(written.entries).toEqual({ a: '1' });
|
||||
expect(written.accessOrder).toEqual(['a']);
|
||||
});
|
||||
|
||||
it('should cap entries when maxEntries is set', () => {
|
||||
const cachePath = join(tempDir, 'plugin-cache.json');
|
||||
const entries: Record<string, string> = {};
|
||||
const order: string[] = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
entries[`key${i}`] = `val${i}`;
|
||||
order.push(`key${i}`);
|
||||
}
|
||||
const cache = new PluginCache<string>(entries, order);
|
||||
|
||||
safeWritePluginCache(cachePath, cache, { maxEntries: 3 });
|
||||
|
||||
const written = JSON.parse(readFileSync(cachePath, 'utf-8'));
|
||||
expect(Object.keys(written.entries)).toHaveLength(3);
|
||||
expect(written.entries['key7']).toBe('val7');
|
||||
expect(written.entries['key9']).toBe('val9');
|
||||
});
|
||||
|
||||
it('should not throw on write failure', () => {
|
||||
const dirAsFile = join(tempDir, 'im-a-dir');
|
||||
mkdirSync(dirAsFile);
|
||||
const cache = new PluginCache<string>({ a: '1' }, ['a']);
|
||||
|
||||
// Writing to a directory path should fail but not throw
|
||||
expect(() => safeWritePluginCache(dirAsFile, cache)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should write hash file after successful cache write', () => {
|
||||
const cachePath = join(tempDir, 'plugin-cache.json');
|
||||
const hashPath = join(tempDir, 'plugin-cache.hash');
|
||||
const cache = new PluginCache<string>({ a: '1' }, ['a']);
|
||||
|
||||
safeWritePluginCache(cachePath, cache, {
|
||||
hashPath,
|
||||
hash: 'abc123',
|
||||
});
|
||||
|
||||
expect(readFileSync(hashPath, 'utf-8')).toBe('abc123');
|
||||
});
|
||||
|
||||
it('should NOT write hash file if cache write fails', () => {
|
||||
const dirAsFile = join(tempDir, 'im-a-dir');
|
||||
mkdirSync(dirAsFile);
|
||||
const hashPath = join(tempDir, 'plugin-cache.hash');
|
||||
const cache = new PluginCache<string>({ a: '1' }, ['a']);
|
||||
|
||||
safeWritePluginCache(dirAsFile, cache, {
|
||||
hashPath,
|
||||
hash: 'abc123',
|
||||
});
|
||||
|
||||
expect(existsSync(hashPath)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('safeWriteFileCache', () => {
|
||||
it('should write data successfully', () => {
|
||||
const cachePath = join(tempDir, 'cache.json');
|
||||
safeWriteFileCache(cachePath, JSON.stringify({ foo: 'bar' }));
|
||||
|
||||
expect(JSON.parse(readFileSync(cachePath, 'utf-8'))).toEqual({
|
||||
foo: 'bar',
|
||||
});
|
||||
});
|
||||
|
||||
it('should create parent directories', () => {
|
||||
const cachePath = join(tempDir, 'nested', 'dir', 'cache.json');
|
||||
safeWriteFileCache(cachePath, 'data');
|
||||
|
||||
expect(readFileSync(cachePath, 'utf-8')).toBe('data');
|
||||
});
|
||||
|
||||
it('should not throw on write failure', () => {
|
||||
const dirAsFile = join(tempDir, 'im-a-dir');
|
||||
mkdirSync(dirAsFile);
|
||||
|
||||
expect(() => safeWriteFileCache(dirAsFile, 'data')).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,220 @@
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
unlinkSync,
|
||||
} from 'node:fs';
|
||||
import { dirname } from 'node:path';
|
||||
import { logger } from './logger';
|
||||
|
||||
/**
|
||||
* On-disk format for plugin caches with LRU metadata.
|
||||
*/
|
||||
interface PluginCacheData<T> {
|
||||
entries: Record<string, T>;
|
||||
accessOrder: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A plugin cache with explicit get/set that tracks access order for LRU eviction.
|
||||
*
|
||||
* Access tracking is append-only during the session (just an array push).
|
||||
* Dedup and capping happen once at write time via `toSerializable()`.
|
||||
*/
|
||||
export class PluginCache<T> {
|
||||
private entries: Record<string, T>;
|
||||
private accessOrder: string[];
|
||||
private sessionLog: string[] = [];
|
||||
|
||||
constructor(entries: Record<string, T> = {}, accessOrder: string[] = []) {
|
||||
this.entries = entries;
|
||||
this.accessOrder = accessOrder;
|
||||
}
|
||||
|
||||
get(key: string): T | undefined {
|
||||
if (key in this.entries) {
|
||||
this.sessionLog.push(key);
|
||||
return this.entries[key];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
set(key: string, value: T): void {
|
||||
this.entries[key] = value;
|
||||
this.sessionLog.push(key);
|
||||
}
|
||||
|
||||
has(key: string): boolean {
|
||||
return key in this.entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize for writing to disk.
|
||||
*
|
||||
* 1. Dedupes the session log (last occurrence = most recent)
|
||||
* 2. Keys not accessed this session keep their prior order at the front
|
||||
* 3. If maxEntries is set, drops oldest entries from the front
|
||||
*/
|
||||
toSerializable(maxEntries?: number): PluginCacheData<T> {
|
||||
const accessed = new Set<string>();
|
||||
const recentFirst: string[] = [];
|
||||
|
||||
// Walk session log backwards so last occurrence wins
|
||||
for (let i = this.sessionLog.length - 1; i >= 0; i--) {
|
||||
const key = this.sessionLog[i];
|
||||
if (!accessed.has(key) && key in this.entries) {
|
||||
accessed.add(key);
|
||||
recentFirst.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Build final order: unaccessed keys first (old order), then accessed keys (most recent last)
|
||||
const order: string[] = [];
|
||||
for (const key of this.accessOrder) {
|
||||
if (!accessed.has(key) && key in this.entries) {
|
||||
order.push(key);
|
||||
}
|
||||
}
|
||||
// Add any new keys not in the original accessOrder and not in session log
|
||||
for (const key of Object.keys(this.entries)) {
|
||||
if (!accessed.has(key) && !order.includes(key)) {
|
||||
order.push(key);
|
||||
}
|
||||
}
|
||||
// Append accessed keys, reversed so most recent is last
|
||||
for (let i = recentFirst.length - 1; i >= 0; i--) {
|
||||
order.push(recentFirst[i]);
|
||||
}
|
||||
|
||||
// Cap if needed
|
||||
if (maxEntries && order.length > maxEntries) {
|
||||
const toKeep = order.slice(order.length - maxEntries);
|
||||
const keepSet = new Set(toKeep);
|
||||
const capped: Record<string, T> = {};
|
||||
for (const k of toKeep) {
|
||||
capped[k] = this.entries[k];
|
||||
}
|
||||
return { entries: capped, accessOrder: toKeep };
|
||||
}
|
||||
|
||||
return { entries: { ...this.entries }, accessOrder: order };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a plugin cache from disk, returning a PluginCache instance.
|
||||
*
|
||||
* Backward compatible with old format: if file contains a plain
|
||||
* Record<string, T>, all keys start in access order as-is.
|
||||
*/
|
||||
export function readPluginCache<T>(cachePath: string): PluginCache<T> {
|
||||
try {
|
||||
if (
|
||||
process.env.NX_CACHE_PROJECT_GRAPH === 'false' ||
|
||||
!existsSync(cachePath)
|
||||
) {
|
||||
return new PluginCache<T>();
|
||||
}
|
||||
const raw = JSON.parse(readFileSync(cachePath, 'utf-8'));
|
||||
|
||||
// Current format: { entries, accessOrder }
|
||||
if (
|
||||
raw &&
|
||||
typeof raw === 'object' &&
|
||||
'entries' in raw &&
|
||||
'accessOrder' in raw &&
|
||||
Array.isArray(raw.accessOrder)
|
||||
) {
|
||||
return new PluginCache<T>(raw.entries, raw.accessOrder);
|
||||
}
|
||||
|
||||
// Legacy format: plain Record<string, T>
|
||||
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
||||
return new PluginCache<T>(raw, Object.keys(raw));
|
||||
}
|
||||
|
||||
return new PluginCache<T>();
|
||||
} catch {
|
||||
return new PluginCache<T>();
|
||||
}
|
||||
}
|
||||
|
||||
export interface SafeWriteOptions {
|
||||
/** Maximum number of entries to keep. Oldest (least recently used) are dropped. */
|
||||
maxEntries?: number;
|
||||
/** Path to a hash file that validates the cache */
|
||||
hashPath?: string;
|
||||
/** Hash value to write to the hash file */
|
||||
hash?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely writes a PluginCache to disk.
|
||||
*
|
||||
* - Caps entries if maxEntries is set
|
||||
* - On write failure: warns and removes corrupted file (never throws)
|
||||
* - Writes hash file only after successful cache write
|
||||
*/
|
||||
export function safeWritePluginCache<T>(
|
||||
cachePath: string,
|
||||
cache: PluginCache<T>,
|
||||
options?: SafeWriteOptions
|
||||
): void {
|
||||
try {
|
||||
mkdirSync(dirname(cachePath), { recursive: true });
|
||||
const data = cache.toSerializable(options?.maxEntries);
|
||||
writeFileSync(cachePath, JSON.stringify(data));
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
`Failed to write plugin cache at ${cachePath}: ${
|
||||
e instanceof Error ? e.message : 'unknown error'
|
||||
}. Continuing without cache.`
|
||||
);
|
||||
tryRemoveFile(cachePath);
|
||||
if (options?.hashPath) {
|
||||
tryRemoveFile(options.hashPath);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Hash file written only after successful cache write
|
||||
if (options?.hashPath && options?.hash) {
|
||||
try {
|
||||
mkdirSync(dirname(options.hashPath), { recursive: true });
|
||||
writeFileSync(options.hashPath, options.hash);
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
`Failed to write cache hash file at ${options.hashPath}: ${
|
||||
e instanceof Error ? e.message : 'unknown error'
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely writes already-stringified content to a cache file on disk.
|
||||
* On failure: warns and removes existing file (never throws).
|
||||
*/
|
||||
export function safeWriteFileCache(cachePath: string, content: string): void {
|
||||
try {
|
||||
mkdirSync(dirname(cachePath), { recursive: true });
|
||||
writeFileSync(cachePath, content);
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
`Failed to write cache at ${cachePath}: ${
|
||||
e instanceof Error ? e.message : 'unknown error'
|
||||
}. Removing existing cache file.`
|
||||
);
|
||||
tryRemoveFile(cachePath);
|
||||
}
|
||||
}
|
||||
|
||||
function tryRemoveFile(path: string): void {
|
||||
try {
|
||||
unlinkSync(path);
|
||||
} catch {
|
||||
// Best effort
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,8 @@ import {
|
||||
joinPathFragments,
|
||||
normalizePath,
|
||||
type ProjectConfiguration,
|
||||
readJsonFile,
|
||||
type TargetConfiguration,
|
||||
type TargetDependencyConfig,
|
||||
writeJsonFile,
|
||||
} from '@nx/devkit';
|
||||
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
|
||||
import { loadConfigFile } from '@nx/devkit/src/utils/config-utils';
|
||||
@@ -22,6 +20,11 @@ import { readdirSync } from 'node:fs';
|
||||
import { dirname, join, parse, posix, relative, resolve } from 'node:path';
|
||||
import { hashObject } from 'nx/src/hasher/file-hasher';
|
||||
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
|
||||
import {
|
||||
PluginCache,
|
||||
readPluginCache,
|
||||
safeWritePluginCache,
|
||||
} from 'nx/src/utils/plugin-cache-utils';
|
||||
import { getFilesInDirectoryUsingContext } from 'nx/src/utils/workspace-context';
|
||||
import { getReporterOutputs, type ReporterOutput } from '../utils/reporters';
|
||||
|
||||
@@ -40,25 +43,6 @@ interface NormalizedOptions {
|
||||
|
||||
type PlaywrightTargets = Pick<ProjectConfiguration, 'targets' | 'metadata'>;
|
||||
|
||||
function readTargetsCache(
|
||||
cachePath: string
|
||||
): Record<string, PlaywrightTargets> {
|
||||
try {
|
||||
return process.env.NX_CACHE_PROJECT_GRAPH !== 'false'
|
||||
? readJsonFile(cachePath)
|
||||
: {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writeTargetsToCache(
|
||||
cachePath: string,
|
||||
results: Record<string, PlaywrightTargets>
|
||||
) {
|
||||
writeJsonFile(cachePath, results);
|
||||
}
|
||||
|
||||
const playwrightConfigGlob = '**/playwright.config.{js,ts,cjs,cts,mjs,mts}';
|
||||
export const createNodes: CreateNodesV2<PlaywrightPluginOptions> = [
|
||||
playwrightConfigGlob,
|
||||
@@ -68,17 +52,17 @@ export const createNodes: CreateNodesV2<PlaywrightPluginOptions> = [
|
||||
workspaceDataDirectory,
|
||||
`playwright-${optionsHash}.hash`
|
||||
);
|
||||
const targetsCache = readTargetsCache(cachePath);
|
||||
const cache = readPluginCache<PlaywrightTargets>(cachePath);
|
||||
try {
|
||||
return await createNodesFromFiles(
|
||||
(configFile, options, context) =>
|
||||
createNodesInternal(configFile, options, context, targetsCache),
|
||||
createNodesInternal(configFile, options, context, cache),
|
||||
configFilePaths,
|
||||
options,
|
||||
context
|
||||
);
|
||||
} finally {
|
||||
writeTargetsToCache(cachePath, targetsCache);
|
||||
safeWritePluginCache(cachePath, cache);
|
||||
}
|
||||
},
|
||||
];
|
||||
@@ -89,7 +73,7 @@ async function createNodesInternal(
|
||||
configFilePath: string,
|
||||
options: PlaywrightPluginOptions,
|
||||
context: CreateNodesContextV2,
|
||||
targetsCache: Record<string, PlaywrightTargets>
|
||||
cache: PluginCache<PlaywrightTargets>
|
||||
) {
|
||||
const projectRoot = dirname(configFilePath);
|
||||
|
||||
@@ -114,13 +98,18 @@ async function createNodesInternal(
|
||||
[getLockFileName(detectPackageManager(context.workspaceRoot))]
|
||||
);
|
||||
|
||||
targetsCache[hash] ??= await buildPlaywrightTargets(
|
||||
configFilePath,
|
||||
projectRoot,
|
||||
normalizedOptions,
|
||||
context
|
||||
);
|
||||
const { targets, metadata } = targetsCache[hash];
|
||||
if (!cache.has(hash)) {
|
||||
cache.set(
|
||||
hash,
|
||||
await buildPlaywrightTargets(
|
||||
configFilePath,
|
||||
projectRoot,
|
||||
normalizedOptions,
|
||||
context
|
||||
)
|
||||
);
|
||||
}
|
||||
const { targets, metadata } = cache.get(hash);
|
||||
|
||||
return {
|
||||
projects: {
|
||||
|
||||
Reference in New Issue
Block a user