From b06f51505941dbbbe5a06dd35017d64459b1d2b5 Mon Sep 17 00:00:00 2001 From: Jason Jean Date: Wed, 18 Sep 2024 13:41:24 -0400 Subject: [PATCH] feat(core): add integration with nx powerpack (#27972) ## Current Behavior There is no Nx Powerpack product. ## Expected Behavior Integration with Nx Powerpack is added. Nx Powerpack is optional, stay tuned for more information which will be released soon. ## Related Issue(s) Fixes # --------- Co-authored-by: Craigory Coppola Co-authored-by: JamesHenry --- docs/generated/cli/activate-powerpack.md | 25 +++++++ docs/generated/manifests/menus.json | 8 +++ docs/generated/manifests/nx-api.json | 11 +++ docs/generated/manifests/tags.json | 7 ++ docs/generated/packages-metadata.json | 11 +++ .../nx/documents/activate-powerpack.md | 25 +++++++ docs/map.json | 6 ++ docs/shared/reference/sitemap.md | 1 + .../activate-powerpack/activate-powerpack.ts | 39 +++++++++++ .../activate-powerpack/command-object.ts | 38 +++++++++++ .../nx/src/command-line/add/command-object.ts | 5 +- packages/nx/src/command-line/list/list.ts | 2 + packages/nx/src/command-line/nx-commands.ts | 20 ++++++ packages/nx/src/command-line/report/report.ts | 52 ++++++++++++++ packages/nx/src/native/cache/cache.rs | 67 +++++++++++++++---- packages/nx/src/native/db/mod.rs | 7 +- packages/nx/src/native/index.d.ts | 3 +- packages/nx/src/native/tests/cache.spec.ts | 4 +- .../nx/src/native/tests/task_history.spec.ts | 4 +- packages/nx/src/tasks-runner/cache.ts | 65 ++++++++++++++++-- packages/nx/src/tasks-runner/run-command.ts | 3 + packages/nx/src/utils/db-connection.ts | 31 +++++++-- packages/nx/src/utils/plugins/output.ts | 7 ++ packages/nx/src/utils/powerpack.ts | 44 ++++++++++++ 24 files changed, 454 insertions(+), 31 deletions(-) create mode 100644 docs/generated/cli/activate-powerpack.md create mode 100644 docs/generated/packages/nx/documents/activate-powerpack.md create mode 100644 packages/nx/src/command-line/activate-powerpack/activate-powerpack.ts create mode 100644 packages/nx/src/command-line/activate-powerpack/command-object.ts create mode 100644 packages/nx/src/utils/powerpack.ts diff --git a/docs/generated/cli/activate-powerpack.md b/docs/generated/cli/activate-powerpack.md new file mode 100644 index 0000000000000..6dcfab199869a --- /dev/null +++ b/docs/generated/cli/activate-powerpack.md @@ -0,0 +1,25 @@ +--- +title: 'activate-powerpack - CLI command' +description: 'Activate a Nx Powerpack license.' +--- + +# activate-powerpack + +Activate a Nx Powerpack license. + +## Usage + +```shell +nx activate-powerpack +``` + +Install `nx` globally to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpm nx`. + +## Options + +| Option | Type | Description | +| ----------- | ------- | ---------------------------------------------------------------------- | +| `--help` | boolean | Show help. | +| `--license` | string | This is a License Key for Nx Powerpack. | +| `--verbose` | boolean | Prints additional information about the commands (e.g., stack traces). | +| `--version` | boolean | Show version number. | diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json index 6209a08abebb7..3f2adc8ccc0f7 100644 --- a/docs/generated/manifests/menus.json +++ b/docs/generated/manifests/menus.json @@ -8753,6 +8753,14 @@ "children": [], "disableCollapsible": false }, + { + "name": "activate-powerpack", + "path": "/nx-api/nx/documents/activate-powerpack", + "id": "activate-powerpack", + "isExternal": false, + "children": [], + "disableCollapsible": false + }, { "name": "reset", "path": "/nx-api/nx/documents/reset", diff --git a/docs/generated/manifests/nx-api.json b/docs/generated/manifests/nx-api.json index ef366a035478a..c89db1b4ca0ba 100644 --- a/docs/generated/manifests/nx-api.json +++ b/docs/generated/manifests/nx-api.json @@ -1880,6 +1880,17 @@ "tags": ["cache-task-results", "distribute-task-execution"], "originalFilePath": "generated/cli/connect" }, + "/nx-api/nx/documents/activate-powerpack": { + "id": "activate-powerpack", + "name": "activate-powerpack", + "description": "The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.", + "file": "generated/packages/nx/documents/activate-powerpack", + "itemList": [], + "isExternal": false, + "path": "/nx-api/nx/documents/activate-powerpack", + "tags": ["cache-task-results"], + "originalFilePath": "generated/cli/activate-powerpack" + }, "/nx-api/nx/documents/reset": { "id": "reset", "name": "reset", diff --git a/docs/generated/manifests/tags.json b/docs/generated/manifests/tags.json index 7d45149b36845..2650223bc4bb6 100644 --- a/docs/generated/manifests/tags.json +++ b/docs/generated/manifests/tags.json @@ -235,6 +235,13 @@ "name": "connect-to-nx-cloud", "path": "/nx-api/nx/documents/connect-to-nx-cloud" }, + { + "description": "The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.", + "file": "generated/packages/nx/documents/activate-powerpack", + "id": "activate-powerpack", + "name": "activate-powerpack", + "path": "/nx-api/nx/documents/activate-powerpack" + }, { "description": "The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.", "file": "generated/packages/nx/documents/reset", diff --git a/docs/generated/packages-metadata.json b/docs/generated/packages-metadata.json index 06f3052829a5d..a40e913a246fc 100644 --- a/docs/generated/packages-metadata.json +++ b/docs/generated/packages-metadata.json @@ -1859,6 +1859,17 @@ "tags": ["cache-task-results", "distribute-task-execution"], "originalFilePath": "generated/cli/connect" }, + { + "id": "activate-powerpack", + "name": "activate-powerpack", + "description": "The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.", + "file": "generated/packages/nx/documents/activate-powerpack", + "itemList": [], + "isExternal": false, + "path": "nx/documents/activate-powerpack", + "tags": ["cache-task-results"], + "originalFilePath": "generated/cli/activate-powerpack" + }, { "id": "reset", "name": "reset", diff --git a/docs/generated/packages/nx/documents/activate-powerpack.md b/docs/generated/packages/nx/documents/activate-powerpack.md new file mode 100644 index 0000000000000..6dcfab199869a --- /dev/null +++ b/docs/generated/packages/nx/documents/activate-powerpack.md @@ -0,0 +1,25 @@ +--- +title: 'activate-powerpack - CLI command' +description: 'Activate a Nx Powerpack license.' +--- + +# activate-powerpack + +Activate a Nx Powerpack license. + +## Usage + +```shell +nx activate-powerpack +``` + +Install `nx` globally to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpm nx`. + +## Options + +| Option | Type | Description | +| ----------- | ------- | ---------------------------------------------------------------------- | +| `--help` | boolean | Show help. | +| `--license` | string | This is a License Key for Nx Powerpack. | +| `--verbose` | boolean | Prints additional information about the commands (e.g., stack traces). | +| `--version` | boolean | Show version number. | diff --git a/docs/map.json b/docs/map.json index 4fe5012ac0ca6..55fbd5bf03ed0 100644 --- a/docs/map.json +++ b/docs/map.json @@ -2095,6 +2095,12 @@ "tags": ["cache-task-results", "distribute-task-execution"], "file": "generated/cli/connect" }, + { + "name": "activate-powerpack", + "tags": ["cache-task-results"], + "id": "activate-powerpack", + "file": "generated/cli/activate-powerpack" + }, { "name": "reset", "id": "reset", diff --git a/docs/shared/reference/sitemap.md b/docs/shared/reference/sitemap.md index 2059beb0ec394..89d21169bb49a 100644 --- a/docs/shared/reference/sitemap.md +++ b/docs/shared/reference/sitemap.md @@ -556,6 +556,7 @@ - [report](/nx-api/nx/documents/report) - [list](/nx-api/nx/documents/list) - [connect-to-nx-cloud](/nx-api/nx/documents/connect-to-nx-cloud) + - [activate-powerpack](/nx-api/nx/documents/activate-powerpack) - [reset](/nx-api/nx/documents/reset) - [repair](/nx-api/nx/documents/repair) - [sync](/nx-api/nx/documents/sync) diff --git a/packages/nx/src/command-line/activate-powerpack/activate-powerpack.ts b/packages/nx/src/command-line/activate-powerpack/activate-powerpack.ts new file mode 100644 index 0000000000000..856091d0f7352 --- /dev/null +++ b/packages/nx/src/command-line/activate-powerpack/activate-powerpack.ts @@ -0,0 +1,39 @@ +import { workspaceRoot } from '../../utils/workspace-root'; +import { ActivatePowerpackOptions } from './command-object'; +import { prompt } from 'enquirer'; +import { execSync } from 'child_process'; +import { getPackageManagerCommand } from '../../utils/package-manager'; + +export async function handleActivatePowerpack( + options: ActivatePowerpackOptions +) { + const license = + options.license ?? + (await prompt({ + type: 'input', + name: 'license', + message: 'Enter your License Key', + })); + const { activatePowerpack } = await requirePowerpack(); + activatePowerpack(workspaceRoot, license); +} + +async function requirePowerpack(): Promise { + // @ts-ignore + return import('@nx/powerpack-license').catch(async (e) => { + if ('code' in e && e.code === 'MODULE_NOT_FOUND') { + try { + execSync( + `${getPackageManagerCommand().addDev} @nx/powerpack-license@latest` + ); + + // @ts-ignore + return await import('@nx/powerpack-license'); + } catch (e) { + throw new Error( + 'Failed to install @nx/powerpack-license. Please install @nx/powerpack-license and try again.' + ); + } + } + }); +} diff --git a/packages/nx/src/command-line/activate-powerpack/command-object.ts b/packages/nx/src/command-line/activate-powerpack/command-object.ts new file mode 100644 index 0000000000000..ee6132e4379c0 --- /dev/null +++ b/packages/nx/src/command-line/activate-powerpack/command-object.ts @@ -0,0 +1,38 @@ +import { CommandModule } from 'yargs'; +import { withVerbose } from '../yargs-utils/shared-options'; +import { handleErrors } from '../../utils/handle-errors'; + +export interface ActivatePowerpackOptions { + license: string; + verbose: boolean; +} + +export const yargsActivatePowerpackCommand: CommandModule< + {}, + ActivatePowerpackOptions +> = { + command: 'activate-powerpack ', + describe: 'Activate a Nx Powerpack license.', + builder: (yargs) => + withVerbose(yargs) + .parserConfiguration({ + 'strip-dashed': true, + 'unknown-options-as-args': true, + }) + .positional('license', { + type: 'string', + description: 'This is a License Key for Nx Powerpack.', + }) + .example( + '$0 activate-powerpack ', + 'Activate a Nx Powerpack license' + ), + handler: async (args) => { + const exitCode = await handleErrors(args.verbose as boolean, async () => { + return (await import('./activate-powerpack')).handleActivatePowerpack( + args + ); + }); + process.exit(exitCode); + }, +}; diff --git a/packages/nx/src/command-line/add/command-object.ts b/packages/nx/src/command-line/add/command-object.ts index 2fbe5fabae6db..e729c53529c90 100644 --- a/packages/nx/src/command-line/add/command-object.ts +++ b/packages/nx/src/command-line/add/command-object.ts @@ -8,10 +8,7 @@ export interface AddOptions { __overrides_unparsed__: string[]; } -export const yargsAddCommand: CommandModule< - Record, - AddOptions -> = { +export const yargsAddCommand: CommandModule<{}, AddOptions> = { command: 'add ', describe: 'Install a plugin and initialize it.', builder: (yargs) => diff --git a/packages/nx/src/command-line/list/list.ts b/packages/nx/src/command-line/list/list.ts index e3315ada49fcd..038bbd9f4e1bc 100644 --- a/packages/nx/src/command-line/list/list.ts +++ b/packages/nx/src/command-line/list/list.ts @@ -12,6 +12,7 @@ import { listPlugins, } from '../../utils/plugins'; import { workspaceRoot } from '../../utils/workspace-root'; +import { listPowerpackPlugins } from '../../utils/plugins/output'; export interface ListArgs { /** The name of an installed plugin to query */ @@ -46,6 +47,7 @@ export async function listHandler(args: ListArgs): Promise { } listPlugins(installedPlugins, 'Installed plugins:'); listAlsoAvailableCorePlugins(installedPlugins); + listPowerpackPlugins(); output.note({ title: 'Community Plugins', diff --git a/packages/nx/src/command-line/nx-commands.ts b/packages/nx/src/command-line/nx-commands.ts index f9524f2e37dc5..0feeb611864b1 100644 --- a/packages/nx/src/command-line/nx-commands.ts +++ b/packages/nx/src/command-line/nx-commands.ts @@ -1,6 +1,7 @@ import * as chalk from 'chalk'; import * as yargs from 'yargs'; +import { yargsActivatePowerpackCommand } from './activate-powerpack/command-object'; import { yargsAffectedBuildCommand, yargsAffectedCommand, @@ -63,6 +64,7 @@ export const commandsObject = yargs .parserConfiguration(parserConfiguration) .usage(chalk.bold('Smart Monorepos ยท Fast CI')) .demandCommand(1, '') + .command(yargsActivatePowerpackCommand) .command(yargsAddCommand) .command(yargsAffectedBuildCommand) .command(yargsAffectedCommand) @@ -98,9 +100,27 @@ export const commandsObject = yargs .command(yargsNxInfixCommand) .command(yargsLoginCommand) .command(yargsLogoutCommand) + .command(resolveConformanceCommandObject()) .scriptName('nx') .help() // NOTE: we handle --version in nx.ts, this just tells yargs that the option exists // so that it shows up in help. The default yargs implementation of --version is not // hit, as the implementation in nx.ts is hit first and calls process.exit(0). .version(); + +function resolveConformanceCommandObject() { + try { + const { yargsConformanceCommand } = require('@nx/powerpack-conformance'); + return yargsConformanceCommand; + } catch (e) { + return { + command: 'conformance', + // Hide from --help output in the common case of not having the plugin installed + describe: false, + handler: () => { + // TODO: Add messaging to help with learning more about powerpack and conformance + process.exit(1); + }, + }; + } +} diff --git a/packages/nx/src/command-line/report/report.ts b/packages/nx/src/command-line/report/report.ts index 257f87600595c..61ef530b1d815 100644 --- a/packages/nx/src/command-line/report/report.ts +++ b/packages/nx/src/command-line/report/report.ts @@ -23,6 +23,7 @@ import { getNxRequirePaths } from '../../utils/installation-directory'; import { NxJsonConfiguration, readNxJson } from '../../config/nx-json'; import { ProjectGraph } from '../../config/project-graph'; import { ProjectGraphError } from '../../project-graph/error-types'; +import { getPowerpackLicenseInformation } from '../../utils/powerpack'; const nxPackageJson = readJsonFile( join(__dirname, '../../../package.json') @@ -39,6 +40,7 @@ export const packagesWeCareAbout = [ export const patternsWeIgnoreInCommunityReport: Array = [ ...packagesWeCareAbout, + new RegExp('@nx/powerpack*'), '@schematics/angular', new RegExp('@angular/*'), '@nestjs/schematics', @@ -58,7 +60,9 @@ export async function reportHandler() { const { pm, pmVersion, + powerpackLicense, localPlugins, + powerpackPlugins, communityPlugins, registeredPlugins, packageVersionsWeCareAbout, @@ -88,6 +92,36 @@ export async function reportHandler() { ); }); + if (powerpackLicense) { + bodyLines.push(LINE_SEPARATOR); + bodyLines.push(chalk.green('Nx Powerpack')); + + bodyLines.push( + `Licensed to ${powerpackLicense.organizationName} for ${ + powerpackLicense.seatCount + } user${powerpackLicense.seatCount > 1 ? 's' : ''} in ${ + powerpackLicense.workspaceCount + } workspace${ + powerpackLicense.workspaceCount > 1 ? 's' : '' + } until ${new Date(powerpackLicense.expiresAt).toLocaleDateString()}` + ); + bodyLines.push(''); + + padding = + Math.max( + ...powerpackPlugins.map( + (powerpackPlugin) => powerpackPlugin.name.length + ) + ) + 1; + for (const powerpackPlugin of powerpackPlugins) { + bodyLines.push( + `${chalk.green(powerpackPlugin.name.padEnd(padding))} : ${chalk.bold( + powerpackPlugin.version + )}` + ); + } + } + if (registeredPlugins.length) { bodyLines.push(LINE_SEPARATOR); bodyLines.push('Registered Plugins:'); @@ -147,6 +181,9 @@ export async function reportHandler() { export interface ReportData { pm: PackageManager; pmVersion: string; + // TODO(@FrozenPandaz): Provide the right type here. + powerpackLicense: any | null; + powerpackPlugins: PackageJson[]; localPlugins: string[]; communityPlugins: PackageJson[]; registeredPlugins: string[]; @@ -174,6 +211,7 @@ export async function getReportData(): Promise { const nxJson = readNxJson(); const localPlugins = await findLocalPlugins(graph, nxJson); + const powerpackPlugins = findInstalledPowerpackPlugins(); const communityPlugins = findInstalledCommunityPlugins(); const registeredPlugins = findRegisteredPluginsBeingUsed(nxJson); @@ -193,8 +231,15 @@ export async function getReportData(): Promise { const native = isNativeAvailable(); + let powerpackLicense = null; + try { + powerpackLicense = await getPowerpackLicenseInformation(); + } catch {} + return { pm, + powerpackLicense, + powerpackPlugins, pmVersion, localPlugins, communityPlugins, @@ -294,6 +339,13 @@ export function findMisalignedPackagesForPackage( : undefined; } +export function findInstalledPowerpackPlugins(): PackageJson[] { + const installedPlugins = findInstalledPlugins(); + return installedPlugins.filter((dep) => + new RegExp('@nx/powerpack*').test(dep.name) + ); +} + export function findInstalledCommunityPlugins(): PackageJson[] { const installedPlugins = findInstalledPlugins(); return installedPlugins.filter( diff --git a/packages/nx/src/native/cache/cache.rs b/packages/nx/src/native/cache/cache.rs index b1057b40f5513..e300f76b2dac4 100644 --- a/packages/nx/src/native/cache/cache.rs +++ b/packages/nx/src/native/cache/cache.rs @@ -4,12 +4,12 @@ use std::time::Instant; use fs_extra::remove_items; use napi::bindgen_prelude::*; +use regex::Regex; use rusqlite::{params, Connection, OptionalExtension}; use tracing::trace; use crate::native::cache::expand_outputs::_expand_outputs; use crate::native::cache::file_ops::_copy; -use crate::native::machine_id::get_machine_id; use crate::native::utils::Normalize; #[napi(object)] @@ -36,8 +36,7 @@ impl NxCache { cache_path: String, db_connection: External, ) -> anyhow::Result { - let machine_id = get_machine_id(); - let cache_path = PathBuf::from(&cache_path).join(machine_id); + let cache_path = PathBuf::from(&cache_path); create_dir_all(&cache_path)?; create_dir_all(cache_path.join("terminalOutputs"))?; @@ -143,7 +142,11 @@ impl NxCache { } #[napi] - pub fn apply_remote_cache_results(&self, hash: String, result: CachedResult) -> anyhow::Result<()> { + pub fn apply_remote_cache_results( + &self, + hash: String, + result: CachedResult, + ) -> anyhow::Result<()> { let terminal_output = result.terminal_output; write(self.get_task_outputs_path(hash.clone()), terminal_output)?; @@ -153,14 +156,13 @@ impl NxCache { } fn get_task_outputs_path_internal(&self, hash: &str) -> PathBuf { - self.cache_path - .join("terminalOutputs") - .join(hash) + self.cache_path.join("terminalOutputs").join(hash) } #[napi] pub fn get_task_outputs_path(&self, hash: String) -> String { - self.get_task_outputs_path_internal(&hash).to_normalized_string() + self.get_task_outputs_path_internal(&hash) + .to_normalized_string() } fn record_to_cache(&self, hash: String, code: i16) -> anyhow::Result<()> { @@ -192,11 +194,12 @@ impl NxCache { .as_slice(), )?; - trace!("Copying Files from Cache {:?} -> {:?}", &outputs_path, &self.workspace_root); - _copy( - outputs_path, - &self.workspace_root, - )?; + trace!( + "Copying Files from Cache {:?} -> {:?}", + &outputs_path, + &self.workspace_root + ); + _copy(outputs_path, &self.workspace_root)?; Ok(()) } @@ -224,4 +227,42 @@ impl NxCache { Ok(()) } + + #[napi] + pub fn check_cache_fs_in_sync(&self) -> anyhow::Result { + // Checks that the number of cache records in the database + // matches the number of cache directories on the filesystem. + // If they don't match, it means that the cache is out of sync. + let cache_records_exist = self.db.query_row( + "SELECT EXISTS (SELECT 1 FROM cache_outputs)", + [], + |row| { + let exists: bool = row.get(0)?; + Ok(exists) + }, + )?; + + if !cache_records_exist { + let hash_regex = Regex::new(r"^\d+$").expect("Hash regex is invalid"); + let fs_entries = std::fs::read_dir(&self.cache_path) + .map_err(anyhow::Error::from)?; + + for entry in fs_entries { + let entry = entry?; + let is_dir = entry.file_type()?.is_dir(); + + if (is_dir) { + if let Some(file_name) = entry.file_name().to_str() { + if hash_regex.is_match(file_name) { + return Ok(false); + } + } + } + } + + Ok(true) + } else { + Ok(true) + } + } } diff --git a/packages/nx/src/native/db/mod.rs b/packages/nx/src/native/db/mod.rs index e580e01138a7f..0c82c01c41b6e 100644 --- a/packages/nx/src/native/db/mod.rs +++ b/packages/nx/src/native/db/mod.rs @@ -10,10 +10,13 @@ use crate::native::machine_id::get_machine_id; pub fn connect_to_nx_db( cache_dir: String, nx_version: String, + db_name: Option, ) -> anyhow::Result> { - let machine_id = get_machine_id(); let cache_dir_buf = PathBuf::from(cache_dir); - let db_path = cache_dir_buf.join(format!("{}.db", machine_id)); + let db_path = cache_dir_buf.join(format!( + "{}.db", + db_name.unwrap_or_else(get_machine_id) + )); create_dir_all(cache_dir_buf)?; let c = create_connection(&db_path)?; diff --git a/packages/nx/src/native/index.d.ts b/packages/nx/src/native/index.d.ts index e231b73fa3aca..2839c5a94ee05 100644 --- a/packages/nx/src/native/index.d.ts +++ b/packages/nx/src/native/index.d.ts @@ -35,6 +35,7 @@ export declare class NxCache { getTaskOutputsPath(hash: string): string copyFilesFromCache(cachedResult: CachedResult, outputs: Array): void removeOldCacheRecords(): void + checkCacheFsInSync(): boolean } export declare class NxTaskHistory { @@ -96,7 +97,7 @@ export interface CachedResult { outputsPath: string } -export declare export function connectToNxDb(cacheDir: string, nxVersion: string): ExternalObject +export declare export function connectToNxDb(cacheDir: string, nxVersion: string, dbName?: string | undefined | null): ExternalObject export declare export function copy(src: string, dest: string): void diff --git a/packages/nx/src/native/tests/cache.spec.ts b/packages/nx/src/native/tests/cache.spec.ts index 6a0fa974de212..068f7b72b6f12 100644 --- a/packages/nx/src/native/tests/cache.spec.ts +++ b/packages/nx/src/native/tests/cache.spec.ts @@ -16,7 +16,9 @@ describe('Cache', () => { force: true, }); - const dbConnection = getDbConnection(join(__dirname, 'temp-db')); + const dbConnection = getDbConnection({ + directory: join(__dirname, 'temp-db'), + }); taskDetails = new TaskDetails(dbConnection); diff --git a/packages/nx/src/native/tests/task_history.spec.ts b/packages/nx/src/native/tests/task_history.spec.ts index 2d09de7862a9a..2e891de940459 100644 --- a/packages/nx/src/native/tests/task_history.spec.ts +++ b/packages/nx/src/native/tests/task_history.spec.ts @@ -17,7 +17,9 @@ describe('NxTaskHistory', () => { force: true, }); - const dbConnection = getDbConnection(join(__dirname, 'temp-db')); + const dbConnection = getDbConnection({ + directory: join(__dirname, 'temp-db'), + }); taskHistory = new NxTaskHistory(dbConnection); taskDetails = new TaskDetails(dbConnection); diff --git a/packages/nx/src/tasks-runner/cache.ts b/packages/nx/src/tasks-runner/cache.ts index 03b7be083a0fe..14a82904ec4bd 100644 --- a/packages/nx/src/tasks-runner/cache.ts +++ b/packages/nx/src/tasks-runner/cache.ts @@ -17,6 +17,7 @@ import { isNxCloudUsed } from '../utils/nx-cloud-utils'; import { readNxJson } from '../config/nx-json'; import { verifyOrUpdateNxCloudClient } from '../nx-cloud/update-manager'; import { getCloudOptions } from '../nx-cloud/utilities/get-cloud-options'; +import { isCI } from '../utils/is-ci'; export type CachedResult = { terminalOutput: string; @@ -40,15 +41,20 @@ export function getCache(options: DefaultTasksRunnerOptions) { export class DbCache { private cache = new NxCache(workspaceRoot, cacheDir, getDbConnection()); + private remoteCache: RemoteCacheV2 | null; private remoteCachePromise: Promise; - async setup() { + constructor(private readonly options: { nxCloudRemoteCache: RemoteCache }) {} + + async init() { + // This should be cheap because we've already loaded this.remoteCache = await this.getRemoteCache(); + if (!this.remoteCache) { + this.assertCacheIsValid(); + } } - constructor(private readonly options: { nxCloudRemoteCache: RemoteCache }) {} - async get(task: Task): Promise { const res = this.cache.get(task.hash); @@ -58,7 +64,6 @@ export class DbCache { remote: false, }; } - await this.setup(); if (this.remoteCache) { // didn't find it locally but we have a remote cache // attempt remote cache @@ -92,10 +97,10 @@ export class DbCache { outputs: string[], code: number ) { + await this.assertCacheIsValid(); return tryAndRetry(async () => { this.cache.put(task.hash, terminalOutput, outputs, code); - await this.setup(); if (this.remoteCache) { await this.remoteCache.store( task.hash, @@ -142,9 +147,59 @@ export class DbCache { return await RemoteCacheV2.fromCacheV1(this.options.nxCloudRemoteCache); } } else { + return ( + (await this.getPowerpackS3Cache()) ?? + (await this.getPowerpackSharedCache()) ?? + null + ); + } + } + + private async getPowerpackS3Cache(): Promise { + try { + const { getRemoteCache } = await import( + this.resolvePackage('@nx/powerpack-s3-cache') + ); + return getRemoteCache(); + } catch { return null; } } + + private async getPowerpackSharedCache(): Promise { + try { + const { getRemoteCache } = await import( + this.resolvePackage('@nx/powerpack-shared-cache') + ); + return getRemoteCache(); + } catch { + return null; + } + } + + private resolvePackage(pkg: string) { + return require.resolve(pkg, { + paths: [process.cwd(), workspaceRoot, __dirname], + }); + } + + private assertCacheIsValid() { + // User has customized the cache directory - this could be because they + // are using a shared cache in the custom directory. The db cache is not + // stored in the cache directory, and is keyed by machine ID so they would + // hit issues. If we detect this, we can create a fallback db cache in the + // custom directory, and check if the entries are there when the main db + // cache misses. + if (isCI() && !this.cache.checkCacheFsInSync()) { + const warning = [ + `Nx found unrecognized artifacts in the cache directory and will not be able to use them.`, + `Nx can only restore artifacts it has metadata about.`, + `Read about this warning and how to address it here: https://nx.dev/troubleshooting/unknown-local-cache`, + ``, + ].join('\n'); + console.warn(warning); + } + } } /** diff --git a/packages/nx/src/tasks-runner/run-command.ts b/packages/nx/src/tasks-runner/run-command.ts index 8656ee105538d..2447b189bb10e 100644 --- a/packages/nx/src/tasks-runner/run-command.ts +++ b/packages/nx/src/tasks-runner/run-command.ts @@ -51,6 +51,7 @@ import { TasksRunner, TaskStatus } from './tasks-runner'; import { shouldStreamOutput } from './utils'; import chalk = require('chalk'); import type { Observable } from 'rxjs'; +import { printPowerpackLicense } from '../utils/powerpack'; async function getTerminalOutputLifeCycle( initiatingProject: string, @@ -241,6 +242,8 @@ export async function runCommandForTasks( await renderIsDone; + await printPowerpackLicense(); + return taskResults; } diff --git a/packages/nx/src/utils/db-connection.ts b/packages/nx/src/utils/db-connection.ts index 323e2d35aa042..66034177d6bf5 100644 --- a/packages/nx/src/utils/db-connection.ts +++ b/packages/nx/src/utils/db-connection.ts @@ -2,9 +2,32 @@ import { connectToNxDb, ExternalObject } from '../native'; import { workspaceDataDirectory } from './cache-directory'; import { version as NX_VERSION } from '../../package.json'; -let dbConnection: ExternalObject; +const dbConnectionMap = new Map>(); -export function getDbConnection(directory = workspaceDataDirectory) { - dbConnection ??= connectToNxDb(directory, NX_VERSION); - return dbConnection; +export function getDbConnection( + opts: { + directory?: string; + dbName?: string; + } = {} +) { + opts.directory ??= workspaceDataDirectory; + const key = `${opts.directory}:${opts.dbName ?? 'default'}`; + const connection = getEntryOrSet(dbConnectionMap, key, () => + connectToNxDb(opts.directory, NX_VERSION, opts.dbName) + ); + return connection; +} + +function getEntryOrSet( + map: Map, + key: TKey, + defaultValue: () => TVal +) { + const existing = map.get(key); + if (existing) { + return existing; + } + const val = defaultValue(); + map.set(key, val); + return val; } diff --git a/packages/nx/src/utils/plugins/output.ts b/packages/nx/src/utils/plugins/output.ts index 928fdbda82d25..181c6197bf0db 100644 --- a/packages/nx/src/utils/plugins/output.ts +++ b/packages/nx/src/utils/plugins/output.ts @@ -61,6 +61,13 @@ export function listAlsoAvailableCorePlugins( } } +export function listPowerpackPlugins(): void { + const powerpackLink = 'https://nx.dev/plugin-registry'; + output.log({ + title: `Available Powerpack Plugins: ${powerpackLink}`, + }); +} + export async function listPluginCapabilities( pluginName: string, projects: Record diff --git a/packages/nx/src/utils/powerpack.ts b/packages/nx/src/utils/powerpack.ts new file mode 100644 index 0000000000000..f6251dbda5605 --- /dev/null +++ b/packages/nx/src/utils/powerpack.ts @@ -0,0 +1,44 @@ +import { logger } from './logger'; +import { getPackageManagerCommand } from './package-manager'; +import { workspaceRoot } from './workspace-root'; + +export async function printPowerpackLicense() { + try { + const { organizationName, seatCount, workspaceCount } = + await getPowerpackLicenseInformation(); + + logger.log( + `Nx Powerpack Licensed to ${organizationName} for ${seatCount} user${ + seatCount > 1 ? '' : 's' + } in ${workspaceCount} workspace${workspaceCount > 1 ? '' : 's'}` + ); + } catch {} +} + +export async function getPowerpackLicenseInformation() { + try { + const { getPowerpackLicenseInformation } = (await import( + // @ts-ignore + '@nx/powerpack-license' + // TODO(@FrozenPandaz): Provide the right type here. + )) as any; + // )) as typeof import('@nx/powerpack-license'); + return getPowerpackLicenseInformation(workspaceRoot); + } catch (e) { + if ('code' in e && e.code === 'ERR_MODULE_NOT_FOUND') { + throw new NxPowerpackNotInstalledError(e); + } + throw e; + } +} + +export class NxPowerpackNotInstalledError extends Error { + constructor(e: Error) { + super( + `The "@nx/powerpack-license" package is needed to use Nx Powerpack enabled features. Please install the @nx/powerpack-license with ${ + getPackageManagerCommand().addDev + } @nx/powerpack-license`, + { cause: e } + ); + } +}