diff --git a/PublicAPI.md b/PublicAPI.md index b59cba2fe3..dd6d8f6151 100644 --- a/PublicAPI.md +++ b/PublicAPI.md @@ -479,7 +479,7 @@ tns.settingsService.setSettings({ userAgentName: "myUserAgent", profileDir: "cus `npm` module provides a way to interact with npm specifically the use of install, uninstall, search and view commands. ### install -Installs specified package. Note that you can use the third argument in order to pass different options to the installation like `ignore-scripts`, `save` or `save-exact` which work exactly like they would if you would execute npm from the command line and pass them as `--` flags. +Installs specified package. The third argument takes package-manager-agnostic options (`dev`, `exact`, `save`, `optional`, `silent`, `ignoreScripts`); the selected package manager maps them onto its own command line flags. * Auxiliary interfaces: ```TypeScript /** @@ -533,11 +533,11 @@ Uninstalls a specified package. /** * Uninstalls a dependency * @param {string} packageName The name of the dependency. - * @param {IDictionary} config Additional options that can be passed to manipulate uninstallation. + * @param {IPackageUninstallOptions} options Package-manager-agnostic uninstallation options (`save`). * @param {string} path The destination of the uninstallation. * @return {Promise} The output of the uninstallation. */ -uninstall(packageName: string, config?: IDictionary, path?: string): Promise; +uninstall(packageName: string, options?: IPackageUninstallOptions, path?: string): Promise; ``` * Usage: @@ -556,39 +556,57 @@ Searches for a package using keywords. ```TypeScript /** * Searches for a package. - * @param {string[]} filter Keywords with which to perform the search. - * @param {IDictionary} config Additional options that can be passed to manipulate search. - * @return {Promise} The output of the uninstallation. + * @param {string[]} filter Keywords with which to perform the search. + * @return {Promise} The raw search output. */ -search(filter: string[], config: IDictionary): Promise; +search(filter: string[]): Promise; ``` * Usage: ```JavaScript -tns.npm.search(["nativescript", "cloud"], { silent: true }).then(output => { +tns.npm.search(["nativescript", "cloud"]).then(output => { console.log(`Found: ${output}`); }, err => { console.log("An error occurred during searching", err); }); ``` +### getInstalledPackagePath +Locates a package the way the selected package manager laid it out on disk, so callers never have to assume a `node_modules` layout. + +* Definition: +```TypeScript +/** + * @param {string} packageName The name of the package. + * @param {string} fromDir The directory whose dependencies are searched, usually the project directory. + * @return {string} The absolute path of the package directory, or null when it is not installed. + */ +getInstalledPackagePath(packageName: string, fromDir: string): string; +``` + +* Usage: +```JavaScript +const pathToPackage = tns.packageManager.getInstalledPackagePath("@nativescript/core", "/tmp/myProject"); +console.log(pathToPackage ? `Installed at ${pathToPackage}` : "Not installed"); +``` + ### view Provides information about a given package. * Definition ```TypeScript /** - * Provides information about a given package. - * @param {string} packageName The name of the package. - * @param {IDictionary} config Additional options that can be passed to manipulate view. - * @return {Promise} Object, containing information about the package. + * Provides registry information about a package. + * @param {string} packageName The name of the package, optionally with a version. + * @param {string} field Optional single registry field (e.g. "versions" or "dist-tags") to return instead of the whole document. + * @return {Promise} The parsed registry data. */ -view(packageName: string, config: Object): Promise; +view(packageName: string, field?: string): Promise; ``` * Usage: ```JavaScript -tns.npm.view(["nativescript"], {}).then(result => { +tns.npm.view("nativescript").then(result => { console.log(`${result.name}'s latest version is ${result["dist-tags"].latest}`); }, err => { console.log("An error occurred during viewing", err); diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 154a325910..a1dfe80dc5 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -379,12 +379,12 @@ registerBuiltInCommand< typeof import("./commands/setup").setupCommandDefinition >("setup|*", () => require("./commands/setup").setupCommandDefinition); -injector.requirePublic("packageManager", "./package-manager"); -injector.requirePublic("npm", "./node-package-manager"); -injector.requirePublic("yarn", "./yarn-package-manager"); -injector.requirePublic("yarn2", "./yarn2-package-manager"); -injector.requirePublic("pnpm", "./pnpm-package-manager"); -injector.requirePublic("bun", "./bun-package-manager"); +injector.requirePublic("packageManager", "./package-managers/index"); +injector.requirePublic("npm", "./package-managers/npm"); +injector.requirePublic("yarn", "./package-managers/yarn"); +injector.requirePublic("yarn2", "./package-managers/yarn2"); +injector.requirePublic("pnpm", "./package-managers/pnpm"); +injector.requirePublic("bun", "./package-managers/bun"); registerBuiltInCommand< typeof import("./common/commands/package-manager-get").packageManagerGetCommandDefinition >( @@ -404,7 +404,7 @@ registerBuiltInCommand< injector.require( "packageInstallationManager", - "./package-installation-manager", + "./package-managers/package-installation-manager", ); injector.require("deviceLogProvider", "./common/mobile/device-log-provider"); diff --git a/lib/commands/install.ts b/lib/commands/install.ts index 69ab52f2b2..ba60986a2b 100644 --- a/lib/commands/install.ts +++ b/lib/commands/install.ts @@ -95,7 +95,7 @@ async function installModule( } await $packageManager.install(moduleName, projectDir, { - "save-dev": true, + dev: true, disableNpmInstall: context.options.disableNpmInstall, frameworkPath: context.options.frameworkPath, ignoreScripts: context.options.ignoreScripts, diff --git a/lib/commands/plugin/create-plugin.ts b/lib/commands/plugin/create-plugin.ts index 62ee77059e..a350d995a8 100644 --- a/lib/commands/plugin/create-plugin.ts +++ b/lib/commands/plugin/create-plugin.ts @@ -180,8 +180,7 @@ export class CreatePluginCommand extends Command({ const cwd = path.join(projectDir, "src"); try { spinner.start(); - const npmOptions: any = { silent: true }; - await this.$packageManager.install(cwd, cwd, npmOptions); + await this.$packageManager.install(cwd, cwd, { silent: true }); } finally { spinner.stop(); } diff --git a/lib/commands/preview.ts b/lib/commands/preview.ts index 0ef938950b..f4f45ca6bf 100644 --- a/lib/commands/preview.ts +++ b/lib/commands/preview.ts @@ -1,4 +1,3 @@ -import { resolvePackagePath } from "@rigor789/resolve-package-path"; import * as path from "path"; import { color } from "../color"; import { IChildProcess, IErrors } from "../common/declarations"; @@ -53,16 +52,17 @@ export class PreviewCommand extends Command({ `${PREVIEW_CLI_PACKAGE}@latest`, this.$projectData.projectDir, { - "save-dev": true, - "save-exact": true, - } as any, + dev: true, + exact: true, + }, ); } private getPreviewCLIPath(): string { - return resolvePackagePath(PREVIEW_CLI_PACKAGE, { - paths: [this.$projectData.projectDir], - }); + return this.$packageManager.getInstalledPackagePath( + PREVIEW_CLI_PACKAGE, + this.$projectData.projectDir, + ); } private async failMissingPreviewCLI(): Promise { diff --git a/lib/commands/test-init.ts b/lib/commands/test-init.ts index dc070f4e2d..e5d4e2dbf2 100644 --- a/lib/commands/test-init.ts +++ b/lib/commands/test-init.ts @@ -129,18 +129,21 @@ export class TestInitCommand extends Command({ await this.$packageManager.install(moduleToInstall, projectDir, { // Packages with native code must land in "dependencies" — the CLI // integrates plugin platform files (pods, aars) only from there. - ...(mod.saveInDependencies ? { save: true } : { "save-dev": true }), - "save-exact": true, - optional: false, + dev: !mod.saveInDependencies, + exact: true, disableNpmInstall: this.$options.disableNpmInstall, frameworkPath: this.$options.frameworkPath, ignoreScripts: this.$options.ignoreScripts, path: this.$options.path, }); - const modulePath = path.join(projectDir, "node_modules", mod.name); - const modulePackageJsonPath = path.join(modulePath, "package.json"); - const modulePackageJsonContent = this.$fs.readJson(modulePackageJsonPath); + const modulePath = this.$packageManager.getInstalledPackagePath( + mod.name, + projectDir, + ); + const modulePackageJsonContent = this.$fs.readJson( + path.join(modulePath, "package.json"), + ); const modulePeerDependencies = modulePackageJsonContent.peerDependencies || {}; const modulePeerDependenciesMeta = @@ -186,8 +189,8 @@ export class TestInitCommand extends Command({ `${peerDependency}@${dependencyVersion}`, projectDir, { - "save-dev": true, - "save-exact": true, + dev: true, + exact: true, disableNpmInstall: false, frameworkPath: this.$options.frameworkPath, ignoreScripts: this.$options.ignoreScripts, diff --git a/lib/common/definitions/json-file-settings-service.d.ts b/lib/common/definitions/json-file-settings-service.d.ts index 3c20e52e83..8fea9c1b63 100644 --- a/lib/common/definitions/json-file-settings-service.d.ts +++ b/lib/common/definitions/json-file-settings-service.d.ts @@ -13,6 +13,11 @@ interface IJsonFileSettingsService { settingName: string, cacheOpts?: ICacheTimeoutOpts ): Promise; + /** + * Reads a setting without taking the settings lock. Suitable for values that + * only change through explicit user commands, where a torn read is harmless. + */ + getSettingValueSync(settingName: string): T; saveSetting( key: string, value: T, diff --git a/lib/common/services/json-file-settings-service.ts b/lib/common/services/json-file-settings-service.ts index d58da1e8fa..81d0e38e43 100644 --- a/lib/common/services/json-file-settings-service.ts +++ b/lib/common/services/json-file-settings-service.ts @@ -56,6 +56,28 @@ export class JsonFileSettingsService implements IJsonFileSettingsService { ); } + public getSettingValueSync(settingName: string): T { + if (!this.jsonSettingsData && this.$fs.exists(this.jsonSettingsFilePath)) { + try { + this.jsonSettingsData = parseJson( + this.$fs.readText(this.jsonSettingsFilePath) + ); + } catch (err) { + this.$logger.trace( + `Error while trying to parse ${this.jsonSettingsFilePath}. Err is: ${err}` + ); + return null; + } + } + + if (this.jsonSettingsData && _.has(this.jsonSettingsData, settingName)) { + const data = this.jsonSettingsData[settingName]; + return data.modifiedByCacheMechanism ? data.value : data; + } + + return null; + } + public async saveSetting( key: string, value: T, diff --git a/lib/common/test/unit-tests/services/json-file-settings-service.ts b/lib/common/test/unit-tests/services/json-file-settings-service.ts index 9dac594142..e37673c6d1 100644 --- a/lib/common/test/unit-tests/services/json-file-settings-service.ts +++ b/lib/common/test/unit-tests/services/json-file-settings-service.ts @@ -65,6 +65,37 @@ describe("jsonFileSettingsService", () => { Date.now = originalDateNow; }); + describe("getSettingValueSync", () => { + it("returns the stored value without going through the lock", () => { + const testInjector = createTestInjector(); + dataInFile[jsonFileSettingsPath] = { prop1: "value1" }; + const lockService = testInjector.resolve("lockService"); + lockService.executeActionWithLock = () => { + throw new Error("lock must not be used for sync reads"); + }; + + const jsonFileSettingsService = + testInjector.resolve( + "jsonFileSettingsService", + { jsonFileSettingsPath } + ); + assert.equal(jsonFileSettingsService.getSettingValueSync("prop1"), "value1"); + assert.isNull(jsonFileSettingsService.getSettingValueSync("missing")); + }); + + it("returns null when the settings file does not exist", () => { + const testInjector = createTestInjector(); + const fs = testInjector.resolve("fs"); + fs.exists = () => false; + const jsonFileSettingsService = + testInjector.resolve( + "jsonFileSettingsService", + { jsonFileSettingsPath } + ); + assert.isNull(jsonFileSettingsService.getSettingValueSync("prop1")); + }); + }); + describe("getSettingValue", () => { it("returns correct data without cache", async () => { dataInFile = { [jsonFileSettingsPath]: { prop1: 1 } }; diff --git a/lib/constants.ts b/lib/constants.ts index 7420893fab..1c97df4171 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -126,13 +126,6 @@ export const TemplatesV2PackageJsonKeysToRemove: Array = [ "nativescript", ]; -export class SaveOptions { - static PRODUCTION = "save"; - static DEV = "save-dev"; - static OPTIONAL = "save-optional"; - static EXACT = "save-exact"; -} - export class ReleaseType { static MAJOR = "major"; static PREMAJOR = "premajor"; diff --git a/lib/contracts/package-manager.ts b/lib/contracts/package-manager.ts index cae1a8eed2..010fffbd46 100644 --- a/lib/contracts/package-manager.ts +++ b/lib/contracts/package-manager.ts @@ -1,7 +1,7 @@ import { Contract } from "../common/di/contract"; -import type { IDictionary } from "../common/declarations"; import type { - INodePackageManagerInstallOptions, + IPackageInstallOptions, + IPackageUninstallOptions, INpmInstallResultInfo, INpmPackageNameParts, INpmsResult, @@ -17,35 +17,35 @@ export abstract class PackageManager { * Installs dependency * @param {string} packageName The name of the dependency - can be a path, a url or a string. * @param {string} pathToSave The destination of the installation. - * @param {INodePackageManagerInstallOptions} config Additional options that can be passed to manipulate installation. + * @param {IPackageInstallOptions} options Package-manager-agnostic installation options. * @return {Promise} Information about installed package. */ abstract install( packageName: string, pathToSave: string, - config: INodePackageManagerInstallOptions, + options: IPackageInstallOptions, ): Promise; /** * Uninstalls a dependency * @param {string} packageName The name of the dependency. - * @param {IDictionary} config Additional options that can be passed to manipulate uninstallation. + * @param {IPackageUninstallOptions} options Package-manager-agnostic uninstallation options. * @param {string} path The destination of the uninstallation. * @return {Promise} The output of the uninstallation. */ abstract uninstall( packageName: string, - config?: IDictionary, + options?: IPackageUninstallOptions, path?: string, ): Promise; /** * Provides information about a given package. - * @param {string} packageName The name of the package. - * @param {IDictionary} config Additional options that can be passed to manipulate view. - * @return {Promise} Object, containing information about the package. + * @param {string} packageName The name of the package, optionally with a version. + * @param {string} field @optional A single registry field (e.g. "versions" or "dist-tags") to return instead of the whole document. + * @return {Promise} The parsed registry data, or null when it cannot be parsed. */ - abstract view(packageName: string, config: Object): Promise; + abstract view(packageName: string, field?: string): Promise; /** * Checks if the specified string is name of a packaged published in the NPM registry. @@ -74,14 +74,10 @@ export abstract class PackageManager { /** * Searches for a package. - * @param {string[]} filter Keywords with which to perform the search. - * @param {IDictionary} config Additional options that can be passed to manipulate search. - * @return {Promise} The output of the uninstallation. + * @param {string[]} filter Keywords with which to perform the search. + * @return {Promise} The raw search output. */ - abstract search( - filter: string[], - config: IDictionary, - ): Promise; + abstract search(filter: string[]): Promise; /** * Searches for npm packages in npms by keyword. @@ -103,6 +99,17 @@ export abstract class PackageManager { */ abstract getCachePath(): Promise; + /** + * Locates a package the way the package manager laid it out on disk. + * @param {string} packageName The name of the package. + * @param {string} fromDir The directory whose dependencies are searched, usually the project directory. + * @return {string} The absolute path of the package directory, or null when it is not installed. + */ + abstract getInstalledPackagePath( + packageName: string, + fromDir: string, + ): string; + /** * Gets the name of the package manager used for the current process. * It can be read from the user settings or by passing -- option. diff --git a/lib/controllers/prepare-controller.ts b/lib/controllers/prepare-controller.ts index 4cc149ba04..fc730ddeab 100644 --- a/lib/controllers/prepare-controller.ts +++ b/lib/controllers/prepare-controller.ts @@ -25,7 +25,11 @@ import { SupportedPlatform, TrackActionNames, } from "../constants"; -import { IOptions, IWatchIgnoreListService } from "../declarations"; +import { + IOptions, + IWatchIgnoreListService, + IPackageManager, +} from "../declarations"; import { INodeModulesDependenciesBuilder, IPlatformController, @@ -40,7 +44,6 @@ import { IProjectDataService, IProjectService, } from "../definitions/project"; -import { resolvePackageJSONPath } from "@rigor789/resolve-package-path"; interface IPlatformWatcherData { hasWebpackCompilerProcess: boolean; @@ -82,6 +85,7 @@ export class PrepareController private $markingModeService: IMarkingModeService, private $projectConfigService: IProjectConfigService, private $projectService: IProjectService, + private $packageManager: IPackageManager, ) { super(); } @@ -490,16 +494,14 @@ export class PrepareController SCOPED_ANDROID_RUNTIME_NAME; } // try reading from installed runtime first before reading from the npm registry... - const installedRuntimePackageJSONPath = resolvePackageJSONPath( + const installedRuntimePath = this.$packageManager.getInstalledPackagePath( runtimePackageName, - { - paths: [projectData.projectDir], - }, + projectData.projectDir, ); - if (installedRuntimePackageJSONPath) { + if (installedRuntimePath) { installedRuntimePackageJSON = this.$fs.readJson( - installedRuntimePackageJSONPath, + path.join(installedRuntimePath, "package.json"), ); } const packageData: any = { diff --git a/lib/declarations.d.ts b/lib/declarations.d.ts index 466f3830fe..a686771506 100644 --- a/lib/declarations.d.ts +++ b/lib/declarations.d.ts @@ -27,25 +27,25 @@ interface INodePackageManager { * Installs dependency * @param {string} packageName The name of the dependency - can be a path, a url or a string. * @param {string} pathToSave The destination of the installation. - * @param {INodePackageManagerInstallOptions} config Additional options that can be passed to manipulate installation. + * @param {IPackageInstallOptions} options Package-manager-agnostic installation options. * @return {Promise} Information about installed package. */ install( packageName: string, pathToSave: string, - config: INodePackageManagerInstallOptions, + options: IPackageInstallOptions, ): Promise; /** * Uninstalls a dependency * @param {string} packageName The name of the dependency. - * @param {IDictionary} config Additional options that can be passed to manipulate uninstallation. + * @param {IPackageUninstallOptions} options Package-manager-agnostic uninstallation options. * @param {string} path The destination of the uninstallation. * @return {Promise} The output of the uninstallation. */ uninstall( packageName: string, - config?: IDictionary, + options?: IPackageUninstallOptions, path?: string, ): Promise; @@ -55,7 +55,13 @@ interface INodePackageManager { * @param {IDictionary} config Additional options that can be passed to manipulate view. * @return {Promise} Object, containing information about the package. */ - view(packageName: string, config: Object): Promise; + /** + * Provides registry information about a package. + * @param {string} packageName The name of the package, optionally with a version. + * @param {string} field @optional A single registry field (e.g. "versions" or "dist-tags") to return instead of the whole document. + * @return {Promise} The parsed registry data, or null when it cannot be parsed. + */ + view(packageName: string, field?: string): Promise; /** * Checks if the specified string is name of a packaged published in the NPM registry. @@ -84,10 +90,7 @@ interface INodePackageManager { * @param {IDictionary} config Additional options that can be passed to manipulate search. * @return {Promise} The output of the uninstallation. */ - search( - filter: string[], - config: IDictionary, - ): Promise; + search(filter: string[]): Promise; /** * Searches for npm packages in npms by keyword. @@ -108,6 +111,14 @@ interface INodePackageManager { * @returns {string} The full path to npm cache directory */ getCachePath(): Promise; + + /** + * Locates a package the way the package manager laid it out on disk. + * @param {string} packageName The name of the package. + * @param {string} fromDir The directory whose dependencies are searched, usually the project directory. + * @return {string} The absolute path of the package directory, or null when it is not installed. + */ + getInstalledPackagePath(packageName: string, fromDir: string): string; } /** @deprecated Kept so existing annotations compile; use the {@link PackageManager} contract. */ @@ -167,18 +178,42 @@ interface IPackageInstallationManager { } /** - * Describes options that can be passed to manipulate package installation. + * Package-manager-agnostic installation options. Each package manager maps + * these onto its own command line flags; options a manager has no flag for + * are dropped rather than passed through. */ -interface INodePackageManagerInstallOptions - extends INpmInstallConfigurationOptions, IDictionary { - /** - * Destination of the installation. - * @type {string} - * @optional - */ +interface IPackageInstallOptions { + /** + * Record the package in package.json. Every supported package manager + * does this by default, so only `false` changes behaviour. + */ + save?: boolean; + /** Record the package under devDependencies. */ + dev?: boolean; + /** Record the package under optionalDependencies. */ + optional?: boolean; + /** Pin the exact resolved version instead of a semver range. */ + exact?: boolean; + /** Suppress the package manager's own output. */ + silent?: boolean; + /** Do not run lifecycle scripts. */ + ignoreScripts?: boolean; + /** Skip the installation entirely (the --disable-npm-install CLI flag). */ + disableNpmInstall?: boolean; + /** Local runtime location (the --frameworkPath CLI flag). */ + frameworkPath?: string; + /** Destination of the installation (the --path CLI flag). */ path?: string; } +/** + * Package-manager-agnostic uninstallation options. + */ +interface IPackageUninstallOptions { + /** Remove the package from package.json. */ + save?: boolean; +} + /** * Describes information about dependency packages. */ @@ -396,7 +431,8 @@ interface INpmInstallResultInfo { interface INpmInstallOptions { pathToSave?: string; version?: string; - dependencyType?: string; + /** Record the package under devDependencies. */ + dev?: boolean; } /** diff --git a/lib/base-package-manager.ts b/lib/package-managers/base-package-manager.ts similarity index 67% rename from lib/base-package-manager.ts rename to lib/package-managers/base-package-manager.ts index 5ee9a96abd..f44a7b85c4 100644 --- a/lib/base-package-manager.ts +++ b/lib/package-managers/base-package-manager.ts @@ -1,34 +1,46 @@ -import { isInteractive } from "./common/helpers"; +import { isInteractive } from "../common/helpers"; +import { resolvePackagePath } from "../helpers/package-path-helper"; import { INodePackageManager, - INodePackageManagerInstallOptions, + IPackageInstallOptions, + IPackageUninstallOptions, INpmInstallResultInfo, INpmsResult, INpmPackageNameParts, -} from "./declarations"; -import { - IDictionary, - IChildProcess, - IFileSystem, - IHostInfo, -} from "./common/declarations"; +} from "../declarations"; +import { IChildProcess, IFileSystem, IHostInfo } from "../common/declarations"; + +/** + * How one package manager spells each IPackageInstallOptions flag on its + * command line. A missing entry means the manager has no such flag and the + * option is dropped rather than passed through. + */ +export interface IPackageManagerFlags { + save?: string; + noSave?: string; + dev?: string; + optional?: string; + exact?: string; + silent?: string; + ignoreScripts?: string; +} export abstract class BasePackageManager implements INodePackageManager { + protected abstract readonly installFlags: IPackageManagerFlags; + protected abstract readonly uninstallFlags: IPackageManagerFlags; + public abstract install( packageName: string, pathToSave: string, - config: INodePackageManagerInstallOptions, + options: IPackageInstallOptions, ): Promise; public abstract uninstall( packageName: string, - config?: IDictionary, + options?: IPackageUninstallOptions, path?: string, ): Promise; - public abstract view(packageName: string, config: Object): Promise; - public abstract search( - filter: string[], - config: IDictionary, - ): Promise; + public abstract view(packageName: string, field?: string): Promise; + public abstract search(filter: string[]): Promise; public abstract searchNpms(keyword: string): Promise; public abstract getRegistryPackageData(packageName: string): Promise; public abstract getCachePath(): Promise; @@ -51,7 +63,7 @@ export abstract class BasePackageManager implements INodePackageManager { } try { - const viewResult = await this.view(packageName, { name: true }); + const viewResult = await this.view(packageName, "name"); // `npm view nonExistingPackageName` will return `nativescript` // if executed in the root dir of the CLI (npm 6.4.1) @@ -133,39 +145,39 @@ export abstract class BasePackageManager implements INodePackageManager { }; } - protected getFlagsString(config: any, asArray: boolean): any { - const array: Array = []; - for (const flag in config) { - if ( - flag === "global" && - this.packageManager !== "yarn" && - this.packageManager !== "yarn2" - ) { - array.push(`--${flag}`); - array.push(`${config[flag]}`); - } else if (config[flag]) { - if ( - flag === "dist-tags" || - flag === "versions" || - flag === "name" || - flag === "gradle" || - flag === "version_info" - ) { - if (this.packageManager === "yarn2") { - array.push(`--fields ${flag}`); - } else { - array.push(` ${flag}`); - } - continue; - } - array.push(`--${flag}`); - } - } - if (asArray) { - return array; - } + public getInstalledPackagePath(packageName: string, fromDir: string): string { + return resolvePackagePath(packageName, { paths: [fromDir] }) || null; + } - return array.join(" "); + protected getInstallFlags(options: IPackageInstallOptions): string[] { + return this.mapFlags(options, this.installFlags); + } + + protected getUninstallFlags(options: IPackageUninstallOptions): string[] { + return this.mapFlags(options, this.uninstallFlags); + } + + private mapFlags( + options: IPackageInstallOptions, + flags: IPackageManagerFlags, + ): string[] { + const result: string[] = []; + if (!options) { + return result; + } + const push = (flag?: string) => { + if (flag) { + result.push(flag); + } + }; + if (options.save === true) push(flags.save); + if (options.save === false) push(flags.noSave); + if (options.dev) push(flags.dev); + if (options.optional) push(flags.optional); + if (options.exact) push(flags.exact); + if (options.silent) push(flags.silent); + if (options.ignoreScripts) push(flags.ignoreScripts); + return result; } private isTgz(packageName: string): boolean { diff --git a/lib/bun-package-manager.ts b/lib/package-managers/bun.ts similarity index 78% rename from lib/bun-package-manager.ts rename to lib/package-managers/bun.ts index cfd5ffc057..65911014a5 100644 --- a/lib/bun-package-manager.ts +++ b/lib/package-managers/bun.ts @@ -1,23 +1,38 @@ import * as path from "path"; import { BasePackageManager } from "./base-package-manager"; -import { exported, cache } from "./common/decorators"; -import { CACACHE_DIRECTORY_NAME } from "./constants"; +import { exported, cache } from "../common/decorators"; +import { CACACHE_DIRECTORY_NAME } from "../constants"; import * as _ from "lodash"; import { - INodePackageManagerInstallOptions, + IPackageInstallOptions, + IPackageUninstallOptions, INpmInstallResultInfo, INpmsResult, -} from "./declarations"; +} from "../declarations"; import { IChildProcess, IErrors, IFileSystem, IHostInfo, Server, -} from "./common/declarations"; -import { injector } from "./common/yok"; +} from "../common/declarations"; +import { injector } from "../common/yok"; export class BunPackageManager extends BasePackageManager { + protected readonly installFlags = { + save: "--save", + noSave: "--no-save", + dev: "--dev", + optional: "--optional", + exact: "--exact", + silent: "--silent", + ignoreScripts: "--ignore-scripts", + }; + protected readonly uninstallFlags = { + save: "--save", + noSave: "--no-save", + }; + constructor( $childProcess: IChildProcess, private $errors: IErrors, @@ -34,19 +49,16 @@ export class BunPackageManager extends BasePackageManager { public async install( packageName: string, pathToSave: string, - config: INodePackageManagerInstallOptions + options: IPackageInstallOptions ): Promise { - if (config.disableNpmInstall) { + if (options.disableNpmInstall) { return; } - if (config.ignoreScripts) { - config["ignore-scripts"] = true; - } const packageJsonPath = path.join(pathToSave, "package.json"); const jsonContentBefore = this.$fs.readJson(packageJsonPath); - const flags = this.getFlagsString(config, true); + const flags = this.getInstallFlags(options); let params = ["install"]; const isInstallingAllDependencies = packageName === pathToSave; if (!isInstallingAllDependencies) { @@ -73,26 +85,22 @@ export class BunPackageManager extends BasePackageManager { @exported("bun") public async uninstall( packageName: string, - config?: any, + options?: IPackageUninstallOptions, cwd?: string ): Promise { - const flags = this.getFlagsString(config, false); + const flags = this.getUninstallFlags(options).join(" "); return this.$childProcess.exec(`bun remove ${packageName} ${flags}`, { cwd, }); } - // Bun does not have a `view` command; use npm. @exported("bun") - public async view(packageName: string, config: Object): Promise { - const wrappedConfig = _.extend({}, config, { json: true }); // always require view response as JSON - - const flags = this.getFlagsString(wrappedConfig, false); + // Bun does not have a `view` command; use npm. + public async view(packageName: string, field?: string): Promise { + const args = [packageName, field, "--json"].filter(Boolean).join(" "); let viewResult: any; try { - viewResult = await this.$childProcess.exec( - `npm view ${packageName} ${flags}` - ); + viewResult = await this.$childProcess.exec(`npm view ${args}`); } catch (e) { this.$errors.fail(e.message); } @@ -104,11 +112,10 @@ export class BunPackageManager extends BasePackageManager { } } - // Bun does not have a `search` command; use npm. @exported("bun") - public async search(filter: string[], config: any): Promise { - const flags = this.getFlagsString(config, false); - return this.$childProcess.exec(`npm search ${filter.join(" ")} ${flags}`); + // Bun does not have a `search` command; use npm. + public async search(filter: string[]): Promise { + return this.$childProcess.exec(`npm search ${filter.join(" ")}`); } public async searchNpms(keyword: string): Promise { diff --git a/lib/package-manager.ts b/lib/package-managers/index.ts similarity index 56% rename from lib/package-manager.ts rename to lib/package-managers/index.ts index df6d18aa92..180e855841 100644 --- a/lib/package-manager.ts +++ b/lib/package-managers/index.ts @@ -1,25 +1,23 @@ -import { cache, exported, invokeInit } from "./common/decorators"; -import { performanceLog } from "./common/decorators"; -import { PackageManagers } from "./constants"; +import { exported } from "../common/decorators"; +import { performanceLog } from "../common/decorators"; +import { PackageManagers } from "../constants"; import { IPackageManager, INodePackageManager, IOptions, - INodePackageManagerInstallOptions, + IPackageInstallOptions, + IPackageUninstallOptions, INpmInstallResultInfo, INpmsResult, INpmPackageNameParts, -} from "./declarations"; -import { - IErrors, - IUserSettingsService, - IDictionary, -} from "./common/declarations"; -import { injector } from "./common/yok"; -import { IProjectConfigService } from "./definitions/project"; +} from "../declarations"; +import { IErrors, IUserSettingsService } from "../common/declarations"; +import { injector } from "../common/yok"; +import { IProjectConfigService } from "../definitions/project"; + export class PackageManager implements IPackageManager { - private packageManager: INodePackageManager; - private _packageManagerName: string; + private selected: INodePackageManager; + private selectedName: string; constructor( private $errors: IErrors, @@ -31,89 +29,79 @@ export class PackageManager implements IPackageManager { private $bun: INodePackageManager, private $logger: ILogger, private $userSettingsService: IUserSettingsService, - private $projectConfigService: IProjectConfigService + private $projectConfigService: IProjectConfigService, ) {} - @cache() - protected async init(): Promise { - this.packageManager = await this._determinePackageManager(); - } - - @invokeInit() public async getPackageManagerName(): Promise { - return this._packageManagerName; + this.packageManager; + return this.selectedName; } @exported("packageManager") @performanceLog() - @invokeInit() public install( packageName: string, pathToSave: string, - config: INodePackageManagerInstallOptions + options: IPackageInstallOptions, ): Promise { - return this.packageManager.install(packageName, pathToSave, config); + return this.packageManager.install(packageName, pathToSave, options); } + @exported("packageManager") - @invokeInit() public uninstall( packageName: string, - config?: IDictionary, - path?: string + options?: IPackageUninstallOptions, + path?: string, ): Promise { - return this.packageManager.uninstall(packageName, config, path); + return this.packageManager.uninstall(packageName, options, path); } + @exported("packageManager") - @invokeInit() - public view(packageName: string, config: Object): Promise { - return this.packageManager.view(packageName, config); + public view(packageName: string, field?: string): Promise { + return this.packageManager.view(packageName, field); } + @exported("packageManager") - @invokeInit() - public search( - filter: string[], - config: IDictionary - ): Promise { - return this.packageManager.search(filter, config); + public search(filter: string[]): Promise { + return this.packageManager.search(filter); } - @invokeInit() public searchNpms(keyword: string): Promise { return this.packageManager.searchNpms(keyword); } - @invokeInit() - public async isRegistered(packageName: string): Promise { + public isRegistered(packageName: string): Promise { return this.packageManager.isRegistered(packageName); } - @invokeInit() - public async getPackageFullName( - packageNameParts: INpmPackageNameParts + public getPackageFullName( + packageNameParts: INpmPackageNameParts, ): Promise { return this.packageManager.getPackageFullName(packageNameParts); } - @invokeInit() - public async getPackageNameParts( - fullPackageName: string + public getPackageNameParts( + fullPackageName: string, ): Promise { return this.packageManager.getPackageNameParts(fullPackageName); } - @invokeInit() public getRegistryPackageData(packageName: string): Promise { return this.packageManager.getRegistryPackageData(packageName); } - @invokeInit() public getCachePath(): Promise { return this.packageManager.getCachePath(); } + @exported("packageManager") + public getInstalledPackagePath(packageName: string, fromDir: string): string { + return this.packageManager.getInstalledPackagePath(packageName, fromDir); + } + public async getTagVersion( packageName: string, - tag: string + tag: string, ): Promise { let version: string = null; if (!tag) { @@ -121,11 +109,11 @@ export class PackageManager implements IPackageManager { } try { - const result = await this.view(packageName, { "dist-tags": true }); + const result = await this.view(packageName, "dist-tags"); version = result[tag]; } catch (err) { this.$logger.trace( - `Error while getting tag version from view command: ${err}` + `Error while getting tag version from view command: ${err}`, ); const registryData = await this.getRegistryPackageData(packageName); version = registryData["dist-tags"][tag]; @@ -134,13 +122,21 @@ export class PackageManager implements IPackageManager { return version; } - private async _determinePackageManager(): Promise { - let pm = null; + private get packageManager(): INodePackageManager { + if (!this.selected) { + this.selected = this.determinePackageManager(); + } + + return this.selected; + } + + private determinePackageManager(): INodePackageManager { + let pm: string = null; try { - pm = await this.$userSettingsService.getSettingValue("packageManager"); + pm = this.$userSettingsService.getSettingValueSync("packageManager"); } catch (err) { this.$errors.fail( - `Unable to read package manager config from user settings ${err}` + `Unable to read package manager config from user settings ${err}`, ); } @@ -150,7 +146,7 @@ export class PackageManager implements IPackageManager { if (configPm) { this.$logger.trace( - `Determined packageManager to use from user config is: ${configPm}` + `Determined packageManager to use from user config is: ${configPm}`, ); pm = configPm; } @@ -158,25 +154,25 @@ export class PackageManager implements IPackageManager { // ignore error, but log info this.$logger.trace( "Tried to read cli.packageManager from project config and failed. Error is: ", - err + err, ); } if (pm === PackageManagers.yarn || this.$options.yarn) { - this._packageManagerName = PackageManagers.yarn; + this.selectedName = PackageManagers.yarn; return this.$yarn; } if (pm === PackageManagers.yarn2 || this.$options.yarn2) { - this._packageManagerName = PackageManagers.yarn2; + this.selectedName = PackageManagers.yarn2; return this.$yarn2; } else if (pm === PackageManagers.pnpm || this.$options.pnpm) { - this._packageManagerName = PackageManagers.pnpm; + this.selectedName = PackageManagers.pnpm; return this.$pnpm; } else if (pm === PackageManagers.bun) { - this._packageManagerName = PackageManagers.bun; + this.selectedName = PackageManagers.bun; return this.$bun; } else { - this._packageManagerName = PackageManagers.npm; + this.selectedName = PackageManagers.npm; return this.$npm; } } diff --git a/lib/node-package-manager.ts b/lib/package-managers/npm.ts similarity index 77% rename from lib/node-package-manager.ts rename to lib/package-managers/npm.ts index bf63cae523..9b55fe3336 100644 --- a/lib/node-package-manager.ts +++ b/lib/package-managers/npm.ts @@ -1,23 +1,38 @@ import { join, relative } from "path"; import { BasePackageManager } from "./base-package-manager"; -import { exported, cache } from "./common/decorators"; -import { CACACHE_DIRECTORY_NAME } from "./constants"; +import { exported, cache } from "../common/decorators"; +import { CACACHE_DIRECTORY_NAME } from "../constants"; import * as _ from "lodash"; import { - INodePackageManagerInstallOptions, + IPackageInstallOptions, + IPackageUninstallOptions, INpmInstallResultInfo, INpmsResult, -} from "./declarations"; +} from "../declarations"; import { IChildProcess, IErrors, IFileSystem, IHostInfo, Server, -} from "./common/declarations"; -import { injector } from "./common/yok"; +} from "../common/declarations"; +import { injector } from "../common/yok"; + +export class NpmPackageManager extends BasePackageManager { + protected readonly installFlags = { + save: "--save", + noSave: "--no-save", + dev: "--save-dev", + optional: "--save-optional", + exact: "--save-exact", + silent: "--silent", + ignoreScripts: "--ignore-scripts", + }; + protected readonly uninstallFlags = { + save: "--save", + noSave: "--no-save", + }; -export class NodePackageManager extends BasePackageManager { constructor( $childProcess: IChildProcess, private $errors: IErrors, @@ -34,19 +49,16 @@ export class NodePackageManager extends BasePackageManager { public async install( packageName: string, pathToSave: string, - config: INodePackageManagerInstallOptions + options: IPackageInstallOptions ): Promise { - if (config.disableNpmInstall) { + if (options.disableNpmInstall) { return; } - if (config.ignoreScripts) { - config["ignore-scripts"] = true; - } const packageJsonPath = join(pathToSave, "package.json"); const jsonContentBefore = this.$fs.readJson(packageJsonPath); - const flags = this.getFlagsString(config, true); + const flags = this.getInstallFlags(options); let params = ["install"]; const isInstallingAllDependencies = packageName === pathToSave; if (!isInstallingAllDependencies) { @@ -62,11 +74,11 @@ export class NodePackageManager extends BasePackageManager { const etcExistsPriorToInstallation = this.$fs.exists(etcDirectoryLocation); //TODO: plamen5kov: workaround is here for a reason (remove whole file later) - if (config.path) { + if (options.path) { let relativePathFromCwdToSource = ""; - if (config.frameworkPath) { + if (options.frameworkPath) { relativePathFromCwdToSource = relative( - config.frameworkPath, + options.frameworkPath, pathToSave ); if (this.$fs.exists(relativePathFromCwdToSource)) { @@ -103,31 +115,26 @@ export class NodePackageManager extends BasePackageManager { @exported("npm") public async uninstall( packageName: string, - config?: any, + options?: IPackageUninstallOptions, path?: string ): Promise { - const flags = this.getFlagsString(config, false); + const flags = this.getUninstallFlags(options).join(" "); return this.$childProcess.exec(`npm uninstall ${packageName} ${flags}`, { cwd: path, }); } @exported("npm") - public async search(filter: string[], config: any): Promise { - const flags = this.getFlagsString(config, false); - return this.$childProcess.exec(`npm search ${filter.join(" ")} ${flags}`); + public async search(filter: string[]): Promise { + return this.$childProcess.exec(`npm search ${filter.join(" ")}`); } @exported("npm") - public async view(packageName: string, config: Object): Promise { - const wrappedConfig = _.extend({}, config, { json: true }); // always require view response as JSON - - const flags = this.getFlagsString(wrappedConfig, false); + public async view(packageName: string, field?: string): Promise { + const args = [packageName, field, "--json"].filter(Boolean).join(" "); let viewResult: any; try { - viewResult = await this.$childProcess.exec( - `npm view ${packageName} ${flags}` - ); + viewResult = await this.$childProcess.exec(`npm view ${args}`); } catch (e) { this.$errors.fail(e.message); } @@ -172,4 +179,4 @@ export class NodePackageManager extends BasePackageManager { } } -injector.register("npm", NodePackageManager); +injector.register("npm", NpmPackageManager); diff --git a/lib/package-installation-manager.ts b/lib/package-managers/package-installation-manager.ts similarity index 86% rename from lib/package-installation-manager.ts rename to lib/package-managers/package-installation-manager.ts index 535ff2d942..807c4fc724 100644 --- a/lib/package-installation-manager.ts +++ b/lib/package-managers/package-installation-manager.ts @@ -1,20 +1,21 @@ import * as path from "path"; -import * as constants from "./constants"; +import * as constants from "../constants"; import { INpmInstallOptions, INpmInstallResultInfo, + IPackageInstallOptions, IPackageInstallationManager, IPackageManager, IStaticConfig, -} from "./declarations"; -import { IProjectDataService } from "./definitions/project"; +} from "../declarations"; +import { IProjectDataService } from "../definitions/project"; import { IChildProcess, IDictionary, IFileSystem, ISettingsService, -} from "./common/declarations"; -import { injector } from "./common/yok"; +} from "../common/declarations"; +import { injector } from "../common/yok"; import * as semver from "semver"; @@ -66,9 +67,7 @@ export class PackageInstallationManager implements IPackageInstallationManager { packageName: string, versionRange: string ): Promise { - const data = await this.$packageManager.view(packageName, { - versions: true, - }); + const data = await this.$packageManager.view(packageName, "versions"); let versions; @@ -152,13 +151,13 @@ export class PackageInstallationManager implements IPackageInstallationManager { try { const pathToSave = projectDir; const version = (opts && opts.version) || null; - const dependencyType = (opts && opts.dependencyType) || null; + const dev = !!(opts && opts.dev); return await this.installCore( packageToInstall, pathToSave, version, - dependencyType + dev ); } catch (error) { this.$logger.trace(error); @@ -189,14 +188,12 @@ export class PackageInstallationManager implements IPackageInstallationManager { inspectorNpmPackageName: string, projectDir: string ): Promise { - const inspectorPath = path.join( - projectDir, - constants.NODE_MODULES_FOLDER_NAME, - inspectorNpmPackageName - ); - // local installation takes precedence over cache - if (this.inspectorAlreadyInstalled(inspectorPath)) { + const inspectorPath = this.$packageManager.getInstalledPackagePath( + inspectorNpmPackageName, + projectDir + ); + if (inspectorPath) { return inspectorPath; } @@ -265,19 +262,11 @@ export class PackageInstallationManager implements IPackageInstallationManager { } } - private inspectorAlreadyInstalled(pathToInspector: string): Boolean { - if (this.$fs.exists(pathToInspector)) { - return true; - } - - return false; - } - private async installCore( packageName: string, pathToSave: string, version: string, - dependencyType: string + dev: boolean ): Promise { const possiblePackageName = path.resolve(packageName); if (this.$fs.exists(possiblePackageName)) { @@ -290,34 +279,29 @@ export class PackageInstallationManager implements IPackageInstallationManager { packageName, pathToSave, version, - dependencyType + dev ); - const installedPackageName = installResultInfo.name; - - const pathToInstalledPackage = path.join( - pathToSave, - "node_modules", - installedPackageName + return this.$packageManager.getInstalledPackagePath( + installResultInfo.name, + pathToSave ); - - return pathToInstalledPackage; } private async npmInstall( packageName: string, pathToSave: string, version: string, - dependencyType: string + dev: boolean ): Promise { this.$logger.info(`Installing ${packageName}`); packageName = packageName + (version ? `@${version}` : ""); - const npmOptions: any = { silent: true, "save-exact": true }; - - if (dependencyType) { - npmOptions[dependencyType] = true; - } + const npmOptions: IPackageInstallOptions = { + silent: true, + exact: true, + dev, + }; return await this.$packageManager.install( packageName, @@ -334,9 +318,7 @@ export class PackageInstallationManager implements IPackageInstallationManager { packageName: string, version: string ): Promise { - let data: any = await this.$packageManager.view(packageName, { - "dist-tags": true, - }); + let data: any = await this.$packageManager.view(packageName, "dist-tags"); data = data?.["dist-tags"] ?? data; this.$logger.trace("Using version %s. ", data[version]); diff --git a/lib/pnpm-package-manager.ts b/lib/package-managers/pnpm.ts similarity index 78% rename from lib/pnpm-package-manager.ts rename to lib/package-managers/pnpm.ts index 5970d50c2e..1321e50a9e 100644 --- a/lib/pnpm-package-manager.ts +++ b/lib/package-managers/pnpm.ts @@ -1,24 +1,33 @@ import * as path from "path"; import * as _ from "lodash"; import { BasePackageManager } from "./base-package-manager"; -import { exported } from "./common/decorators"; -import { CACACHE_DIRECTORY_NAME } from "./constants"; +import { exported } from "../common/decorators"; +import { CACACHE_DIRECTORY_NAME } from "../constants"; import { - INodePackageManagerInstallOptions, + IPackageInstallOptions, + IPackageUninstallOptions, INpmInstallResultInfo, INpmsResult, -} from "./declarations"; +} from "../declarations"; import { IChildProcess, IErrors, IFileSystem, IHostInfo, Server, - IDictionary, -} from "./common/declarations"; -import { injector } from "./common/yok"; +} from "../common/declarations"; +import { injector } from "../common/yok"; export class PnpmPackageManager extends BasePackageManager { + protected readonly installFlags = { + dev: "--save-dev", + optional: "--save-optional", + exact: "--save-exact", + silent: "--silent", + ignoreScripts: "--ignore-scripts", + }; + protected readonly uninstallFlags = {}; + constructor( $childProcess: IChildProcess, private $errors: IErrors, @@ -35,25 +44,16 @@ export class PnpmPackageManager extends BasePackageManager { public async install( packageName: string, pathToSave: string, - config: INodePackageManagerInstallOptions, + options: IPackageInstallOptions, ): Promise { - if (config.disableNpmInstall) { + if (options.disableNpmInstall) { return; } - delete config.dev; // temporary fix for unsupported yarn flag - if (config.ignoreScripts) { - config["ignore-scripts"] = true; - } - // CLI-internal options must never reach the command line: pnpm, unlike - // npm, hard-fails on unknown options. - delete config.ignoreScripts; - delete config.path; - delete config.frameworkPath; const packageJsonPath = path.join(pathToSave, "package.json"); const jsonContentBefore = this.$fs.readJson(packageJsonPath); - const flags = this.getFlagsString(config, true); + const flags = this.getInstallFlags(options); let params = ["i"]; if (!this.projectManagesOwnHoisting(pathToSave)) { // With pnpm's default isolated layout some imports won't be found, so @@ -87,27 +87,21 @@ export class PnpmPackageManager extends BasePackageManager { @exported("pnpm") public uninstall( packageName: string, - config?: IDictionary, + options?: IPackageUninstallOptions, cwd?: string, ): Promise { - // pnpm does not want save option in remove. It saves it by default - delete config["save"]; - const flags = this.getFlagsString(config, false); + const flags = this.getUninstallFlags(options).join(" "); return this.$childProcess.exec(`pnpm remove ${packageName} ${flags}`, { cwd, }); } @exported("pnpm") - public async view(packageName: string, config: Object): Promise { - const wrappedConfig = _.extend({}, config, { json: true }); - - const flags = this.getFlagsString(wrappedConfig, false); + public async view(packageName: string, field?: string): Promise { + const args = [packageName, field, "--json"].filter(Boolean).join(" "); let viewResult: any; try { - viewResult = await this.$childProcess.exec( - `pnpm info ${packageName} ${flags}`, - ); + viewResult = await this.$childProcess.exec(`pnpm info ${args}`); } catch (e) { this.$errors.fail(e.message); } @@ -120,12 +114,8 @@ export class PnpmPackageManager extends BasePackageManager { } @exported("pnpm") - public search( - filter: string[], - config: IDictionary, - ): Promise { - const flags = this.getFlagsString(config, false); - return this.$childProcess.exec(`pnpm search ${filter.join(" ")} ${flags}`); + public async search(filter: string[]): Promise { + return this.$childProcess.exec(`pnpm search ${filter.join(" ")}`); } public async searchNpms(keyword: string): Promise { diff --git a/lib/yarn-package-manager.ts b/lib/package-managers/yarn.ts similarity index 78% rename from lib/yarn-package-manager.ts rename to lib/package-managers/yarn.ts index d4d08ad7f0..aecffc77ca 100644 --- a/lib/yarn-package-manager.ts +++ b/lib/package-managers/yarn.ts @@ -1,23 +1,32 @@ import * as path from "path"; import * as _ from "lodash"; import { BasePackageManager } from "./base-package-manager"; -import { exported } from "./common/decorators"; +import { exported } from "../common/decorators"; import { - INodePackageManagerInstallOptions, + IPackageInstallOptions, + IPackageUninstallOptions, INpmInstallResultInfo, INpmsResult, -} from "./declarations"; +} from "../declarations"; import { IChildProcess, IErrors, IFileSystem, IHostInfo, Server, - IDictionary, -} from "./common/declarations"; -import { injector } from "./common/yok"; +} from "../common/declarations"; +import { injector } from "../common/yok"; export class YarnPackageManager extends BasePackageManager { + protected readonly installFlags = { + dev: "--dev", + optional: "--optional", + exact: "--exact", + silent: "--silent", + ignoreScripts: "--ignore-scripts", + }; + protected readonly uninstallFlags = {}; + constructor( $childProcess: IChildProcess, private $errors: IErrors, @@ -34,19 +43,16 @@ export class YarnPackageManager extends BasePackageManager { public async install( packageName: string, pathToSave: string, - config: INodePackageManagerInstallOptions + options: IPackageInstallOptions ): Promise { - if (config.disableNpmInstall) { + if (options.disableNpmInstall) { return; } - if (config.ignoreScripts) { - config["ignore-scripts"] = true; - } const packageJsonPath = path.join(pathToSave, "package.json"); const jsonContentBefore = this.$fs.readJson(packageJsonPath); - const flags = this.getFlagsString(config, true); + const flags = this.getInstallFlags(options); let params = []; const isInstallingAllDependencies = packageName === pathToSave; if (!isInstallingAllDependencies) { @@ -72,25 +78,21 @@ export class YarnPackageManager extends BasePackageManager { @exported("yarn") public uninstall( packageName: string, - config?: IDictionary, + options?: IPackageUninstallOptions, cwd?: string ): Promise { - const flags = this.getFlagsString(config, false); + const flags = this.getUninstallFlags(options).join(" "); return this.$childProcess.exec(`yarn remove ${packageName} ${flags}`, { cwd, }); } @exported("yarn") - public async view(packageName: string, config: Object): Promise { - const wrappedConfig = _.extend({}, config, { json: true }); - - const flags = this.getFlagsString(wrappedConfig, false); + public async view(packageName: string, field?: string): Promise { + const args = [packageName, field, "--json"].filter(Boolean).join(" "); let viewResult: any; try { - viewResult = await this.$childProcess.exec( - `yarn info ${packageName} ${flags}` - ); + viewResult = await this.$childProcess.exec(`yarn info ${args}`); } catch (e) { this.$errors.fail(e.message); } @@ -104,10 +106,7 @@ export class YarnPackageManager extends BasePackageManager { } @exported("yarn") - public search( - filter: string[], - config: IDictionary - ): Promise { + public search(filter: string[]): Promise { this.$errors.fail( "Method not implemented. Yarn does not support searching for packages in the registry." ); diff --git a/lib/yarn2-package-manager.ts b/lib/package-managers/yarn2.ts similarity index 76% rename from lib/yarn2-package-manager.ts rename to lib/package-managers/yarn2.ts index a8312abff3..4cbb9df362 100644 --- a/lib/yarn2-package-manager.ts +++ b/lib/package-managers/yarn2.ts @@ -1,23 +1,34 @@ import * as path from "path"; import * as _ from "lodash"; import { BasePackageManager } from "./base-package-manager"; -import { exported } from "./common/decorators"; +import { exported } from "../common/decorators"; import { - INodePackageManagerInstallOptions, + IPackageInstallOptions, + IPackageUninstallOptions, INpmInstallResultInfo, INpmsResult, -} from "./declarations"; +} from "../declarations"; import { IChildProcess, IErrors, IFileSystem, IHostInfo, Server, - IDictionary, -} from "./common/declarations"; -import { injector } from "./common/yok"; +} from "../common/declarations"; +import { injector } from "../common/yok"; export class Yarn2PackageManager extends BasePackageManager { + protected readonly installFlags = { + dev: "--dev", + optional: "--optional", + exact: "--exact", + silent: "--silent", + // yarn berry has no --ignore-scripts; skip-build is the mode that + // installs without running any build scripts. + ignoreScripts: "--mode=skip-build", + }; + protected readonly uninstallFlags = {}; + private $hostInfo_: IHostInfo; constructor( $childProcess: IChildProcess, @@ -46,23 +57,16 @@ export class Yarn2PackageManager extends BasePackageManager { public async install( packageName: string, pathToSave: string, - config: INodePackageManagerInstallOptions + options: IPackageInstallOptions ): Promise { - if (config.disableNpmInstall) { + if (options.disableNpmInstall) { return; } - if (config.ignoreScripts) { - config["ignore-scripts"] = true; - } const packageJsonPath = path.join(pathToSave, "package.json"); const jsonContentBefore = this.$fs.readJson(packageJsonPath); - // remove unsupported flags - // todo: refactor all package managers to map typed flags to the actual flags - const cleanedConfig = _.omit(config, ["save-dev", "save-exact"]); - - const flags = this.getFlagsString(cleanedConfig, true); + const flags = this.getInstallFlags(options); let params = []; const isInstallingAllDependencies = packageName === pathToSave; if (!isInstallingAllDependencies) { @@ -88,25 +92,23 @@ export class Yarn2PackageManager extends BasePackageManager { @exported("yarn2") public uninstall( packageName: string, - config?: IDictionary, + options?: IPackageUninstallOptions, cwd?: string ): Promise { - const flags = this.getFlagsString(config, false); + const flags = this.getUninstallFlags(options).join(" "); return this.$childProcess.exec(`yarn remove ${packageName} ${flags}`, { cwd, }); } @exported("yarn2") - public async view(packageName: string, config: Object): Promise { - const wrappedConfig = _.extend({}, config, { json: true }); - - const flags = this.getFlagsString(wrappedConfig, false); + public async view(packageName: string, field?: string): Promise { + const args = [packageName, field && `--fields ${field}`, "--json"] + .filter(Boolean) + .join(" "); let viewResult: any; try { - viewResult = await this.$childProcess.exec( - `yarn npm info ${packageName} ${flags}` - ); + viewResult = await this.$childProcess.exec(`yarn npm info ${args}`); } catch (e) { this.$errors.fail(e.message); } @@ -120,10 +122,7 @@ export class Yarn2PackageManager extends BasePackageManager { } @exported("yarn2") - public search( - filter: string[], - config: IDictionary - ): Promise { + public search(filter: string[]): Promise { this.$errors.fail( "Method not implemented. Yarn does not support searching for packages in the registry." ); diff --git a/lib/services/android-plugin-build-service.ts b/lib/services/android-plugin-build-service.ts index 88098d55b0..0d4021ac6e 100644 --- a/lib/services/android-plugin-build-service.ts +++ b/lib/services/android-plugin-build-service.ts @@ -36,7 +36,6 @@ import { IFilesHashService } from "../definitions/files-hash-service"; import { IInjector } from "../common/definitions/yok"; import { injector } from "../common/yok"; import * as _ from "lodash"; -import { resolvePackageJSONPath } from "@rigor789/resolve-package-path"; import { cwd } from "process"; export class AndroidPluginBuildService implements IAndroidPluginBuildService { @@ -492,9 +491,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { this.$projectData.nsConfig?.android?.runtimePackageName || SCOPED_ANDROID_RUNTIME_NAME; try { - let result = await this.$packageManager.view(packageName, { - "dist-tags": true, - }); + let result = await this.$packageManager.view(packageName, "dist-tags"); result = result?.["dist-tags"] ?? result; runtimeVersion = result.latest; } catch (err) { @@ -529,19 +526,17 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { this.$projectData.nsConfig?.android?.runtimePackageName || SCOPED_ANDROID_RUNTIME_NAME; // try reading from installed runtime first before reading from the npm registry... - const installedRuntimePackageJSONPath = resolvePackageJSONPath( + const installedRuntimePath = this.$packageManager.getInstalledPackagePath( packageName, - { - paths: [this.$projectData.projectDir], - }, + this.$projectData.projectDir, ); - if (!installedRuntimePackageJSONPath) { + if (!installedRuntimePath) { return null; } const installedRuntimePackageJSON: IRuntimePackageJSON = this.$fs.readJson( - installedRuntimePackageJSONPath, + path.join(installedRuntimePath, "package.json"), ); if (!installedRuntimePackageJSON) { @@ -590,7 +585,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { try { let output = await this.$packageManager.view( `${packageName}@${runtimeVersion}`, - { version_info: true }, + "version_info", ); output = output?.["version_info"] ?? output; @@ -605,7 +600,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { */ output = await this.$packageManager.view( `${packageName}@${runtimeVersion}`, - { gradle: true }, + "gradle", ); output = output?.["gradle"] ?? output; diff --git a/lib/services/bundler/bundler-compiler-service.ts b/lib/services/bundler/bundler-compiler-service.ts index ccf7df4bb7..2c05eb6183 100644 --- a/lib/services/bundler/bundler-compiler-service.ts +++ b/lib/services/bundler/bundler-compiler-service.ts @@ -35,10 +35,6 @@ import { import { ICleanupService } from "../../definitions/cleanup-service"; import { ViteHmrPortService } from "../../contracts/vite-hmr-port-service"; import { injector } from "../../common/yok"; -import { - resolvePackagePath, - resolvePackageJSONPath, -} from "../../helpers/package-path-helper"; // todo: move out of here interface IBundlerMessage { @@ -573,10 +569,12 @@ export class BundlerCompilerService additionalNodeArgs.unshift("--max_old_space_size=4096"); } + const bundlerExecutablePath = this.getBundlerExecutablePath(projectData); + const isModernBundler = this.isModernBundler(projectData); const args = [ ...additionalNodeArgs, - this.getBundlerExecutablePath(projectData), - isVite || this.isModernBundler(projectData) ? "build" : null, + bundlerExecutablePath, + isVite || isModernBundler ? "build" : null, `--config=${projectData.bundlerConfigPath}`, ...envParams, ].filter(Boolean); @@ -1168,19 +1166,20 @@ export class BundlerCompilerService private getBundlerExecutablePath(projectData: IProjectData): string { const bundler = this.getBundler(); + const resolve = (packageName: string) => + this.$packageManager.getInstalledPackagePath( + packageName, + projectData.projectDir, + ); if (bundler === "vite") { - const packagePath = resolvePackagePath(`vite`, { - paths: [projectData.projectDir], - }); + const packagePath = resolve("vite"); if (packagePath) { return path.resolve(packagePath, "bin", "vite.js"); } } else if (this.isModernBundler(projectData)) { - const packagePath = resolvePackagePath(this.getBundlerPackageName(), { - paths: [projectData.projectDir], - }); + const packagePath = resolve(this.getBundlerPackageName()); if (packagePath) { return path.resolve(packagePath, "dist", "bin", "index.js"); @@ -1200,9 +1199,7 @@ export class BundlerCompilerService ); } - const packagePath = resolvePackagePath("webpack", { - paths: [projectData.projectDir], - }); + const packagePath = resolve("webpack"); if (!packagePath) { return ""; @@ -1230,15 +1227,15 @@ export class BundlerCompilerService case "rspack": return true; default: - const packageJSONPath = resolvePackageJSONPath( + const packagePath = this.$packageManager.getInstalledPackagePath( this.getBundlerPackageName(), - { - paths: [projectData.projectDir], - }, + projectData.projectDir, ); - if (packageJSONPath) { - const packageData = this.$fs.readJson(packageJSONPath); + if (packagePath) { + const packageData = this.$fs.readJson( + path.join(packagePath, "package.json"), + ); const ver = semver.coerce(packageData.version); if (semver.satisfies(ver, ">= 5.0.0")) { diff --git a/lib/services/doctor-service.ts b/lib/services/doctor-service.ts index d57018efec..3c41698968 100644 --- a/lib/services/doctor-service.ts +++ b/lib/services/doctor-service.ts @@ -3,15 +3,11 @@ import * as path from "path"; import * as _ from "lodash"; import * as helpers from "../common/helpers"; import { cache } from "../common/decorators"; -import { - TrackActionNames, - NODE_MODULES_FOLDER_NAME, - TNS_CORE_MODULES_NAME, -} from "../constants"; +import { TrackActionNames, TNS_CORE_MODULES_NAME } from "../constants"; import { DoctorService } from "../contracts/doctor-service"; import { doctor, constants } from "@nativescript/doctor"; import { IProjectDataService } from "../definitions/project"; -import { IVersionsService, IOptions } from "../declarations"; +import { IVersionsService, IOptions, IPackageManager } from "../declarations"; import { IPlatformEnvironmentRequirements } from "../definitions/platform"; import { IAnalyticsService, @@ -73,6 +69,7 @@ export class DoctorServiceImpl implements DoctorService { private $terminalSpinnerService: ITerminalSpinnerService, private $versionsService: IVersionsService, private $settingsService: ISettingsService, + private $packageManager: IPackageManager, ) {} public async printWarnings(configOptions?: { @@ -278,6 +275,9 @@ export class DoctorServiceImpl implements DoctorService { ): { file: string; line: string }[] { const shortImportRegExp = this.getShortImportRegExp(projectDir); const shortImports: { file: string; line: string }[] = []; + if (!shortImportRegExp) { + return shortImports; + } for (const file of files) { const fileContent = this.$fs.readText(file); @@ -305,11 +305,13 @@ export class DoctorServiceImpl implements DoctorService { } private getShortImportRegExp(projectDir: string): RegExp { - const pathToTnsCoreModules = path.join( - projectDir, - NODE_MODULES_FOLDER_NAME, + const pathToTnsCoreModules = this.$packageManager.getInstalledPackagePath( TNS_CORE_MODULES_NAME, + projectDir, ); + if (!pathToTnsCoreModules) { + return null; + } const coreModulesSubDirs = this.$fs .readDirectory(pathToTnsCoreModules) .filter((entry) => diff --git a/lib/services/extensibility-service.ts b/lib/services/extensibility-service.ts index 9c2884c534..54de9ba694 100644 --- a/lib/services/extensibility-service.ts +++ b/lib/services/extensibility-service.ts @@ -4,7 +4,11 @@ import { cache } from "../common/decorators"; import * as constants from "../constants"; import { createRegExp, regExpEscape } from "../common/helpers"; import { reportDeprecation } from "../common/deprecation"; -import { INodePackageManager, INpmsSingleResultData } from "../declarations"; +import { + INodePackageManager, + INpmsSingleResultData, + IPackageInstallOptions, +} from "../declarations"; import { IDictionary, IFileSystem, @@ -119,9 +123,9 @@ export class ExtensibilityService implements IExtensibilityService { await this.assertPackageJsonExists(); - const npmOpts: any = { + const npmOpts: IPackageInstallOptions = { save: true, - ["save-exact"]: true, + exact: true, }; const localPath = path.resolve(extensionName); @@ -508,11 +512,12 @@ export class ExtensibilityService implements IExtensibilityService { extensionName: string, ): Promise { this.$logger.trace(`Asserting extension ${extensionName} is installed.`); - const installedExtensions = this.$fs.readDirectory( - path.join(this.pathToExtensions, constants.NODE_MODULES_FOLDER_NAME), + const installedPath = this.$packageManager.getInstalledPackagePath( + extensionName, + this.pathToExtensions, ); - if (installedExtensions.indexOf(extensionName) === -1) { + if (!installedPath) { this.$logger.trace( `Extension ${extensionName} is not installed, starting installation.`, ); diff --git a/lib/services/platform/add-platform-service.ts b/lib/services/platform/add-platform-service.ts index b93aba4ea7..28c45e45db 100644 --- a/lib/services/platform/add-platform-service.ts +++ b/lib/services/platform/add-platform-service.ts @@ -120,9 +120,8 @@ export class AddPlatformService implements IAddPlatformService { { silent: true, dev: true, - "save-dev": true, - "save-exact": true, - } as any + exact: true, + } ); if (!installedPackage.name) { diff --git a/lib/services/plugins-service.ts b/lib/services/plugins-service.ts index 43c843e59a..34880da5b2 100644 --- a/lib/services/plugins-service.ts +++ b/lib/services/plugins-service.ts @@ -17,7 +17,7 @@ import { } from "../definitions/platform"; import { IProjectDataService, IProjectData } from "../definitions/project"; import { - INodePackageManagerInstallOptions, + IPackageInstallOptions, INodePackageManager, IOptions, IDependencyData, @@ -32,10 +32,6 @@ import { IFilesHashService } from "../definitions/files-hash-service"; import * as _ from "lodash"; import { IInjector } from "../common/definitions/yok"; import { injector } from "../common/yok"; -import { - resolvePackagePath, - resolvePackageJSONPath, -} from "../helpers/package-path-helper"; import { color } from "../color"; export class PluginsService implements IPluginsService { @@ -59,7 +55,7 @@ export class PluginsService implements IPluginsService { return this.$injector.resolve("projectDataService"); } - private get npmInstallOptions(): INodePackageManagerInstallOptions { + private get npmInstallOptions(): IPackageInstallOptions { return _.merge( { disableNpmInstall: this.$options.disableNpmInstall, @@ -84,7 +80,7 @@ export class PluginsService implements IPluginsService { ) {} public async add(plugin: string, projectData: IProjectData): Promise { - await this.ensure(projectData); + await this.ensureAllDependenciesAreInstalled(projectData); const possiblePackageName = path.resolve(plugin); if ( possiblePackageName.indexOf(".tgz") !== -1 && @@ -298,24 +294,18 @@ export class PluginsService implements IPluginsService { _.keys(packageJsonContent.devDependencies), ); - const notInstalledDependencies = allDependencies - .map((dep) => { - this.$logger.trace(`Checking if ${dep} is installed...`); - const pathToPackage = resolvePackagePath(dep, { - paths: [projectData.projectDir], - }); - - if (pathToPackage) { - // return false if the dependency is installed - we'll filter out boolean values - // and end up with an array of dep names that are not installed if we end up - // inside the catch block. - return false; - } - + const notInstalledDependencies: string[] = []; + for (const dep of allDependencies) { + this.$logger.trace(`Checking if ${dep} is installed...`); + const pathToPackage = this.$packageManager.getInstalledPackagePath( + dep, + projectData.projectDir, + ); + if (!pathToPackage) { this.$logger.trace(`${dep} is not installed, or couldn't be found`); - return dep; - }) - .filter(Boolean); + notInstalledDependencies.push(dep); + } + } if (this.$options.force || notInstalledDependencies.length) { this.$logger.trace( @@ -635,9 +625,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple pluginData.version = cacheData.version; pluginData.fullPath = (cacheData).directory || - path.dirname( - this.getPackageJsonFilePathForModule(cacheData.name, projectDir), - ); + (cacheData).fullPath; pluginData.isPlugin = !!cacheData.nativescript; pluginData.pluginPlatformsFolderPath = (platform: string) => { if (this.$mobileHelper.isvisionOSPlatform(platform)) { @@ -702,10 +690,6 @@ This framework comes from ${dependencyName} plugin, which is installed multiple })); } - private getNodeModulesPath(projectDir: string): string { - return path.join(projectDir, "node_modules"); - } - private getPackageJsonFilePath(projectDir: string): string { return path.join(projectDir, "package.json"); } @@ -714,10 +698,11 @@ This framework comes from ${dependencyName} plugin, which is installed multiple moduleName: string, projectDir: string, ): string { - const pathToJsonFile = resolvePackageJSONPath(moduleName, { - paths: [projectDir], - }); - return pathToJsonFile; + const pathToModule = this.$packageManager.getInstalledPackagePath( + moduleName, + projectDir, + ); + return pathToModule && path.join(pathToModule, "package.json"); } private getDependencies(projectDir: string): string[] { @@ -754,22 +739,17 @@ This framework comes from ${dependencyName} plugin, which is installed multiple }; } - private async ensure(projectData: IProjectData): Promise { - await this.ensureAllDependenciesAreInstalled(projectData); - this.$fs.ensureDirectoryExists( - this.getNodeModulesPath(projectData.projectDir), - ); - } - private async getAllInstalledModules( projectData: IProjectData, ): Promise { - await this.ensure(projectData); + await this.ensureAllDependenciesAreInstalled(projectData); const nodeModules = this.getDependencies(projectData.projectDir); - return _.map(nodeModules, (nodeModuleName) => - this.getNodeModuleData(nodeModuleName, projectData.projectDir), - ).filter(Boolean); + return nodeModules + .map((nodeModuleName) => + this.getNodeModuleData(nodeModuleName, projectData.projectDir), + ) + .filter(Boolean); } private async executeNpmCommand( diff --git a/lib/services/project-data-service.ts b/lib/services/project-data-service.ts index 31e5a2a342..d795c9cf38 100644 --- a/lib/services/project-data-service.ts +++ b/lib/services/project-data-service.ts @@ -27,6 +27,7 @@ import { import { IAndroidResourcesMigrationService, IStaticConfig, + IPackageManager, } from "../declarations"; import { IBasePluginData, IPluginsService } from "../definitions/plugins"; import { IDictionary, IFileSystem, IProjectDir } from "../common/declarations"; @@ -34,7 +35,6 @@ import * as _ from "lodash"; import { IInjector } from "../common/definitions/yok"; import { injector } from "../common/yok"; import * as semver from "semver"; -import { resolvePackageJSONPath } from "../helpers/package-path-helper"; interface IProjectFileData { projectData: any; @@ -653,20 +653,17 @@ export class ProjectDataService implements IProjectDataService { // in case we are using a local tgz for the runtime or a range like ~8.0.0, ^8.0.0 etc. or a tag like JSC if (runtimePackage.version.includes("tgz") || isRange || isTag) { try { - const runtimePackageJsonPath = resolvePackageJSONPath( - runtimePackage.name, - { - paths: [projectDir], - }, - ); + const runtimePackagePath = this.$injector + .resolve("packageManager") + .getInstalledPackagePath(runtimePackage.name, projectDir); - if (!runtimePackageJsonPath) { + if (!runtimePackagePath) { // caught below - throw new Error("Runtime package.json not found."); + throw new Error("Runtime package not found."); } runtimePackage.version = this.$fs.readJson( - runtimePackageJsonPath, + path.join(runtimePackagePath, constants.PACKAGE_JSON_FILE_NAME), ).version; } catch (err) { if (isRange) { diff --git a/lib/services/test-execution-service.ts b/lib/services/test-execution-service.ts index 8ba4eda276..2733aea4c5 100644 --- a/lib/services/test-execution-service.ts +++ b/lib/services/test-execution-service.ts @@ -7,14 +7,13 @@ import { IProjectDataService, IProjectData, } from "../definitions/project"; -import { IConfiguration, IOptions } from "../declarations"; +import { IConfiguration, IOptions, IPackageManager } from "../declarations"; import { IPluginsService } from "../definitions/plugins"; import { Server, IFileSystem, IChildProcess } from "../common/declarations"; import { ErrorCodes } from "../common/enums"; import * as _ from "lodash"; import { injector } from "../common/yok"; import { ICommandParameter } from "../common/definitions/commands"; -import { resolvePackagePath } from "../helpers/package-path-helper"; interface IKarmaConfigOptions { debugBrk: boolean; @@ -36,6 +35,7 @@ export class TestExecutionService implements ITestExecutionService { private $pluginsService: IPluginsService, private $projectDataService: IProjectDataService, private $childProcess: IChildProcess, + private $packageManager: IPackageManager, ) {} public platform: string; @@ -144,9 +144,10 @@ export class TestExecutionService implements ITestExecutionService { } }); - const pathToKarma = resolvePackagePath("karma", { - paths: [projectData.projectDir], - }); + const pathToKarma = this.$packageManager.getInstalledPackagePath( + "karma", + projectData.projectDir, + ); canStartKarmaServer = canStartKarmaServer && !!pathToKarma; diff --git a/lib/services/user-settings-service.ts b/lib/services/user-settings-service.ts index 2ba6be9831..64ed5c3f9f 100644 --- a/lib/services/user-settings-service.ts +++ b/lib/services/user-settings-service.ts @@ -39,6 +39,10 @@ export class UserSettingsService implements IUserSettingsService { ); } + public getSettingValueSync(settingName: string): T { + return this.$jsonFileSettingsService.getSettingValueSync(settingName); + } + public saveSetting( key: string, value: T, diff --git a/lib/services/versions-service.ts b/lib/services/versions-service.ts index 3710ab52bc..19c9f2621d 100644 --- a/lib/services/versions-service.ts +++ b/lib/services/versions-service.ts @@ -2,7 +2,11 @@ import * as constants from "../constants"; import * as helpers from "../common/helpers"; import * as semver from "semver"; import * as path from "path"; -import { IVersionsService, IPackageInstallationManager } from "../declarations"; +import { + IVersionsService, + IPackageInstallationManager, + IPackageManager, +} from "../declarations"; import { IProjectData, IProjectDataService } from "../definitions/project"; import { IPluginsService, IBasePluginData } from "../definitions/plugins"; import { IFileSystem, IVersionInformation } from "../common/declarations"; @@ -28,6 +32,7 @@ class VersionsService implements IVersionsService { constructor( private $fs: IFileSystem, private $packageInstallationManager: IPackageInstallationManager, + private $packageManager: IPackageManager, private $injector: IInjector, private $logger: ILogger, private $staticConfig: Config.IStaticConfig, @@ -63,18 +68,13 @@ class VersionsService implements IVersionsService { const versionInformations: IVersionInformation[] = []; if (this.projectData) { - const nodeModulesPath = path.join( - this.projectData.projectDir, - constants.NODE_MODULES_FOLDER_NAME - ); - const scopedPackagePath = path.join( - nodeModulesPath, - constants.SCOPED_TNS_CORE_MODULES - ); - const tnsCoreModulesPath = path.join( - nodeModulesPath, - constants.TNS_CORE_MODULES_NAME - ); + const resolve = (packageName: string) => + this.$packageManager.getInstalledPackagePath( + packageName, + this.projectData.projectDir + ); + let scopedPackagePath = resolve(constants.SCOPED_TNS_CORE_MODULES); + let tnsCoreModulesPath = resolve(constants.TNS_CORE_MODULES_NAME); const dependsOnNonScopedPackage = !!this.projectData.dependencies[ constants.TNS_CORE_MODULES_NAME @@ -83,18 +83,19 @@ class VersionsService implements IVersionsService { constants.SCOPED_TNS_CORE_MODULES ]; - // ensure the dependencies are installed, so we can get their actual versions from node_modules + // ensure the dependencies are installed, so we can read their actual versions if ( - !this.$fs.exists(nodeModulesPath) || - (dependsOnNonScopedPackage && !this.$fs.exists(tnsCoreModulesPath)) || - (dependsOnScopedPackage && !this.$fs.exists(scopedPackagePath)) + (dependsOnNonScopedPackage && !tnsCoreModulesPath) || + (dependsOnScopedPackage && !scopedPackagePath) ) { await this.$pluginsService.ensureAllDependenciesAreInstalled( this.projectData ); + scopedPackagePath = resolve(constants.SCOPED_TNS_CORE_MODULES); + tnsCoreModulesPath = resolve(constants.TNS_CORE_MODULES_NAME); } - if (dependsOnNonScopedPackage && this.$fs.exists(tnsCoreModulesPath)) { + if (dependsOnNonScopedPackage && tnsCoreModulesPath) { const currentTnsCoreModulesVersion = this.$fs.readJson( path.join(tnsCoreModulesPath, constants.PACKAGE_JSON_FILE_NAME) ).version; @@ -102,7 +103,7 @@ class VersionsService implements IVersionsService { versionInformations.push(nativescriptCoreModulesInfo); } - if (dependsOnScopedPackage && this.$fs.exists(scopedPackagePath)) { + if (dependsOnScopedPackage && scopedPackagePath) { const scopedModulesInformation: IVersionInformation = { componentName: constants.SCOPED_TNS_CORE_MODULES, latestVersion: await this.$packageInstallationManager.getLatestVersion( diff --git a/lib/services/vitest-execution-service.ts b/lib/services/vitest-execution-service.ts index f4811780e8..80c09e8235 100644 --- a/lib/services/vitest-execution-service.ts +++ b/lib/services/vitest-execution-service.ts @@ -1,9 +1,8 @@ import * as path from "path"; import { IProjectData, IVitestExecutionService } from "../definitions/project"; -import { IOptions } from "../declarations"; +import { IOptions, IPackageManager } from "../declarations"; import { IChildProcess, IErrors, IFileSystem } from "../common/declarations"; import { injector } from "../common/yok"; -import { resolvePackagePath } from "../helpers/package-path-helper"; const VITEST_CONFIG_FILES = [ "vitest.config.mts", @@ -19,6 +18,7 @@ export class VitestExecutionService implements IVitestExecutionService { private $fs: IFileSystem, private $logger: ILogger, private $options: IOptions, + private $packageManager: IPackageManager, ) {} public isVitestProject(projectData: IProjectData): boolean { @@ -28,7 +28,7 @@ export class VitestExecutionService implements IVitestExecutionService { public canStartTestRun(projectData: IProjectData): boolean { return ( this.isVitestProject(projectData) && - !!resolvePackagePath("vitest", { paths: [projectData.projectDir] }) + !!this.getVitestPackagePath(projectData) ); } @@ -36,9 +36,7 @@ export class VitestExecutionService implements IVitestExecutionService { platform: string, projectData: IProjectData, ): Promise { - const vitestPackagePath = resolvePackagePath("vitest", { - paths: [projectData.projectDir], - }); + const vitestPackagePath = this.getVitestPackagePath(projectData); if (!vitestPackagePath) { this.$errors.fail( "Unable to find 'vitest' in the project. Run '$ ns test init --framework vitest' first.", @@ -90,6 +88,14 @@ export class VitestExecutionService implements IVitestExecutionService { } return null; } + + private getVitestPackagePath(projectData: IProjectData): string { + return this.$packageManager.getInstalledPackagePath( + "vitest", + projectData.projectDir, + ); + } + } injector.register("vitestExecutionService", VitestExecutionService); diff --git a/lib/tools/node-modules/node-modules-dependencies-builder.ts b/lib/tools/node-modules/node-modules-dependencies-builder.ts index 9474b7eb79..abed73c376 100644 --- a/lib/tools/node-modules/node-modules-dependencies-builder.ts +++ b/lib/tools/node-modules/node-modules-dependencies-builder.ts @@ -1,11 +1,10 @@ import * as path from "path"; import { PACKAGE_JSON_FILE_NAME } from "../../constants"; import { INodeModulesDependenciesBuilder } from "../../definitions/platform"; -import { IDependencyData } from "../../declarations"; +import { IDependencyData, IPackageManager } from "../../declarations"; import { IFileSystem } from "../../common/declarations"; import * as _ from "lodash"; import { injector } from "../../common/yok"; -import { resolvePackagePath } from "@rigor789/resolve-package-path"; interface IDependencyDescription { parent: IDependencyDescription; @@ -17,7 +16,10 @@ interface IDependencyDescription { export class NodeModulesDependenciesBuilder implements INodeModulesDependenciesBuilder { - public constructor(private $fs: IFileSystem) {} + public constructor( + private $fs: IFileSystem, + private $packageManager: IPackageManager, + ) {} public getProductionDependencies( projectPath: string, @@ -96,16 +98,18 @@ export class NodeModulesDependenciesBuilder const parentModulesPath = depDescription?.parentDir ?? depDescription?.parent?.parentDir; - let modulePath: string = resolvePackagePath(depDescription.name, { - paths: [parentModulesPath], - }); + let modulePath = this.$packageManager.getInstalledPackagePath( + depDescription.name, + parentModulesPath, + ); // perhaps traverse up the tree here? if (!modulePath) { // fallback to searching in the root path - modulePath = resolvePackagePath(depDescription.name, { - paths: [rootPath], - }); + modulePath = this.$packageManager.getInstalledPackagePath( + depDescription.name, + rootPath, + ); } // if we failed to find the module... diff --git a/test/bun-package-manager.ts b/test/bun-package-manager.ts index 758b620f54..46fdfeb16b 100644 --- a/test/bun-package-manager.ts +++ b/test/bun-package-manager.ts @@ -1,7 +1,7 @@ import { Yok } from "../lib/common/yok"; import * as stubs from "./stubs"; import { assert } from "chai"; -import { BunPackageManager } from "../lib/bun-package-manager"; +import { BunPackageManager } from "../lib/package-managers/bun"; import { IInjector } from "../lib/common/definitions/yok"; function createTestInjector(configuration: {} = {}): IInjector { diff --git a/test/controllers/add-platform-controller.ts b/test/controllers/add-platform-controller.ts index f7680f789f..934c84d26e 100644 --- a/test/controllers/add-platform-controller.ts +++ b/test/controllers/add-platform-controller.ts @@ -5,12 +5,12 @@ import { assert } from "chai"; import { format } from "util"; import * as _ from "lodash"; import { AddPlaformErrors } from "../../lib/constants"; -import { PackageManager } from "../../lib/package-manager"; -import { NodePackageManager } from "../../lib/node-package-manager"; -import { YarnPackageManager } from "../../lib/yarn-package-manager"; -import { Yarn2PackageManager } from "../../lib/yarn2-package-manager"; -import { PnpmPackageManager } from "../../lib/pnpm-package-manager"; -import { BunPackageManager } from "../../lib/bun-package-manager"; +import { PackageManager } from "../../lib/package-managers"; +import { NpmPackageManager } from "../../lib/package-managers/npm"; +import { YarnPackageManager } from "../../lib/package-managers/yarn"; +import { Yarn2PackageManager } from "../../lib/package-managers/yarn2"; +import { PnpmPackageManager } from "../../lib/package-managers/pnpm"; +import { BunPackageManager } from "../../lib/package-managers/bun"; import { MobileHelper } from "../../lib/common/mobile/mobile-helper"; let actualMessage: string = null; @@ -29,7 +29,7 @@ function createInjector(data?: { latestFrameworkVersion: string }) { trackEventActionInGoogleAnalytics: () => ({}), }); injector.register("packageManager", PackageManager); - injector.register("npm", NodePackageManager); + injector.register("npm", NpmPackageManager); injector.register("yarn", YarnPackageManager); injector.register("yarn2", Yarn2PackageManager); injector.register("pnpm", PnpmPackageManager); @@ -37,6 +37,7 @@ function createInjector(data?: { latestFrameworkVersion: string }) { injector.register("userSettingsService", { getSettingValue: async (settingName: string): Promise => undefined, + getSettingValueSync: (settingName: string): void => undefined, }); injector.register("tempService", TempServiceStub); injector.register("mobileHelper", MobileHelper); diff --git a/test/controllers/prepare-controller.ts b/test/controllers/prepare-controller.ts index e3982de1e4..553bcbe7bc 100644 --- a/test/controllers/prepare-controller.ts +++ b/test/controllers/prepare-controller.ts @@ -51,6 +51,9 @@ function createTestInjector(data: { hasNativeChanges: boolean }): IInjector { injector.register("mobileHelper", MobileHelper); injector.register("prepareController", PrepareController); + injector.register("packageManager", { + getInstalledPackagePath: (): string => null, + }); injector.register("nodeModulesDependenciesBuilder", { getProductionDependencies: () => [], diff --git a/test/extension-manifests.ts b/test/extension-manifests.ts index 9a213310d9..33bbe53d80 100644 --- a/test/extension-manifests.ts +++ b/test/extension-manifests.ts @@ -1,4 +1,5 @@ import { assert } from "chai"; +import { resolvePackagePath } from "../lib/helpers/package-path-helper"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; @@ -184,6 +185,8 @@ describe("extension manifests", () => { install: async (): Promise => { throw new Error("Extensions are expected to be installed already."); }, + getInstalledPackagePath: (packageName: string, fromDir: string): string => + resolvePackagePath(packageName, { paths: [fromDir] }) || null, uninstall: async (): Promise => undefined, searchNpms: async (): Promise => ({ results: [] }), getRegistryPackageData: async (): Promise => ({}), diff --git a/test/ios-project-service.ts b/test/ios-project-service.ts index e55c28f3f2..9d610a3d3f 100644 --- a/test/ios-project-service.ts +++ b/test/ios-project-service.ts @@ -24,9 +24,9 @@ import { IOSDeviceDiscovery } from "../lib/common/mobile/mobile-core/ios-device- import { AndroidDeviceDiscovery } from "../lib/common/mobile/mobile-core/android-device-discovery"; import { Utils } from "../lib/common/utils"; import { CocoaPodsService } from "../lib/services/cocoapods-service"; -import { PackageManager } from "../lib/package-manager"; -import { NodePackageManager } from "../lib/node-package-manager"; -import { YarnPackageManager } from "../lib/yarn-package-manager"; +import { PackageManager } from "../lib/package-managers"; +import { NpmPackageManager } from "../lib/package-managers/npm"; +import { YarnPackageManager } from "../lib/package-managers/yarn"; import { assert } from "chai"; import { SettingsService } from "../lib/common/test/unit-tests/stubs"; @@ -180,10 +180,11 @@ function createTestInjector( ); testInjector.register("userSettingsService", { getSettingValue: async (settingName: string): Promise => undefined, + getSettingValueSync: (settingName: string): void => undefined, }); testInjector.register("packageManager", PackageManager); testInjector.register("projectConfigService", ProjectConfigServiceStub); - testInjector.register("npm", NodePackageManager); + testInjector.register("npm", NpmPackageManager); testInjector.register("yarn", YarnPackageManager); testInjector.register("xcconfigService", XcconfigService); testInjector.register("settingsService", SettingsService); diff --git a/test/node-package-manager.ts b/test/node-package-manager.ts index 27efb270f3..36d1eb65a9 100644 --- a/test/node-package-manager.ts +++ b/test/node-package-manager.ts @@ -1,7 +1,7 @@ import { Yok } from "../lib/common/yok"; import * as stubs from "./stubs"; import { assert } from "chai"; -import { NodePackageManager } from "../lib/node-package-manager"; +import { NpmPackageManager } from "../lib/package-managers/npm"; import { IInjector } from "../lib/common/definitions/yok"; function createTestInjector(configuration: {} = {}): IInjector { @@ -12,7 +12,7 @@ function createTestInjector(configuration: {} = {}): IInjector { injector.register("childProcess", stubs.ChildProcessStub); injector.register("httpClient", {}); injector.register("fs", stubs.FileSystemStub); - injector.register("npm", NodePackageManager); + injector.register("npm", NpmPackageManager); injector.register("pacoteService", { manifest: () => Promise.resolve(), }); @@ -52,7 +52,7 @@ describe("node-package-manager", () => { ].forEach((testCase) => { it(testCase.name, async () => { const testInjector = createTestInjector(); - const npm = testInjector.resolve("npm"); + const npm = testInjector.resolve("npm"); const templateNameParts = await npm.getPackageNameParts( testCase.templateFullName ); @@ -87,7 +87,7 @@ describe("node-package-manager", () => { ].forEach((testCase) => { it(testCase.name, async () => { const testInjector = createTestInjector(); - const npm = testInjector.resolve("npm"); + const npm = testInjector.resolve("npm"); const templateFullName = await npm.getPackageFullName({ name: testCase.templateName, version: testCase.templateVersion, diff --git a/test/package-installation-manager.ts b/test/package-installation-manager.ts index ca2cbe409e..df8773c66e 100644 --- a/test/package-installation-manager.ts +++ b/test/package-installation-manager.ts @@ -4,13 +4,13 @@ import * as ErrorsLib from "../lib/common/errors"; import * as FsLib from "../lib/common/file-system"; import * as HostInfoLib from "../lib/common/host-info"; import * as LoggerLib from "../lib/common/logger/logger"; -import * as NpmLib from "../lib/node-package-manager"; -import * as YarnLib from "../lib/yarn-package-manager"; -import * as Yarn2Lib from "../lib/yarn2-package-manager"; -import * as PnpmLib from "../lib/pnpm-package-manager"; -import * as BunLib from "../lib/bun-package-manager"; -import * as PackageManagerLib from "../lib/package-manager"; -import * as PackageInstallationManagerLib from "../lib/package-installation-manager"; +import * as NpmLib from "../lib/package-managers/npm"; +import * as YarnLib from "../lib/package-managers/yarn"; +import * as Yarn2Lib from "../lib/package-managers/yarn2"; +import * as PnpmLib from "../lib/package-managers/pnpm"; +import * as BunLib from "../lib/package-managers/bun"; +import * as PackageManagerLib from "../lib/package-managers"; +import * as PackageInstallationManagerLib from "../lib/package-managers/package-installation-manager"; import * as OptionsLib from "../lib/options"; import * as StaticConfigLib from "../lib/config"; import * as yok from "../lib/common/yok"; @@ -45,8 +45,9 @@ function createTestInjector(): IInjector { }); testInjector.register("userSettingsService", { getSettingValue: async (settingName: string): Promise => undefined, + getSettingValueSync: (settingName: string): void => undefined, }); - testInjector.register("npm", NpmLib.NodePackageManager); + testInjector.register("npm", NpmLib.NpmPackageManager); testInjector.register("yarn", YarnLib.YarnPackageManager); testInjector.register("yarn2", Yarn2Lib.Yarn2PackageManager); testInjector.register("pnpm", PnpmLib.PnpmPackageManager); @@ -67,12 +68,12 @@ function mockNpm( latestVersion: string ) { testInjector.register("npm", { - view: async (packageName: string, config: any): Promise => { - if (config.versions) { + view: async (packageName: string, field?: string): Promise => { + if (field === "versions") { return versions; } - throw new Error(`Unable to find propertyName ${config}.`); + throw new Error(`Unable to find propertyName ${field}.`); }, }); } diff --git a/test/package-manager-flags.ts b/test/package-manager-flags.ts new file mode 100644 index 0000000000..23da5b55c2 --- /dev/null +++ b/test/package-manager-flags.ts @@ -0,0 +1,269 @@ +import * as path from "path"; +import { Yok } from "../lib/common/yok"; +import * as stubs from "./stubs"; +import { assert } from "chai"; +import { NpmPackageManager } from "../lib/package-managers/npm"; +import { YarnPackageManager } from "../lib/package-managers/yarn"; +import { Yarn2PackageManager } from "../lib/package-managers/yarn2"; +import { PnpmPackageManager } from "../lib/package-managers/pnpm"; +import { BunPackageManager } from "../lib/package-managers/bun"; +import { + INodePackageManager, + IPackageInstallOptions, +} from "../lib/declarations"; +import { IInjector } from "../lib/common/definitions/yok"; + +class RecordingChildProcessStub extends stubs.ChildProcessStub { + public spawnedArgs: string[][] = []; + public execCommands: string[] = []; + + public async exec( + command: string, + options?: any, + execOptions?: any, + ): Promise { + this.execCommands.push(command); + return super.exec(command, options, execOptions); + } + + public async spawnFromEvent( + command: string, + args: string[], + event: string, + options?: any, + spawnFromEventOptions?: any, + ): Promise { + this.spawnedArgs.push(args); + return super.spawnFromEvent( + command, + args, + event, + options, + spawnFromEventOptions, + ); + } +} + +class NoFilesFileSystemStub extends stubs.FileSystemStub { + exists(filePath: string): boolean { + return false; + } +} + +const managers: { name: string; ctor: any }[] = [ + { name: "npm", ctor: NpmPackageManager }, + { name: "yarn", ctor: YarnPackageManager }, + { name: "yarn2", ctor: Yarn2PackageManager }, + { name: "pnpm", ctor: PnpmPackageManager }, + { name: "bun", ctor: BunPackageManager }, +]; + +function createTestInjector(name: string, ctor: any): IInjector { + const injector = new Yok(); + injector.register("hostInfo", { isWindows: false }); + injector.register("errors", stubs.ErrorsStub); + injector.register("logger", stubs.LoggerStub); + injector.register("childProcess", RecordingChildProcessStub); + injector.register("httpClient", {}); + injector.register("fs", NoFilesFileSystemStub); + injector.register(name, ctor); + injector.register("pacoteService", { + manifest: () => Promise.resolve({ name: "left-pad", version: "1.3.0" }), + }); + + return injector; +} + +async function installArgs( + name: string, + ctor: any, + options: IPackageInstallOptions, +): Promise { + const injector = createTestInjector(name, ctor); + const manager = injector.resolve(name); + const childProcess = + injector.resolve("childProcess"); + await manager.install("left-pad", projectDir, options); + return childProcess.spawnedArgs[0]; +} + +async function uninstallCommand( + name: string, + ctor: any, + save: boolean, +): Promise { + const injector = createTestInjector(name, ctor); + const manager = injector.resolve(name); + const childProcess = + injector.resolve("childProcess"); + await manager.uninstall("left-pad", { save }, projectDir); + return childProcess.execCommands[0].trim(); +} + +const projectDir = path.join("/tmp", "some-project"); + +const allOptions: IPackageInstallOptions = { + save: true, + dev: true, + optional: true, + exact: true, + silent: true, + ignoreScripts: true, +}; + +describe("package manager flag mapping", () => { + const expectedInstallArgs: { [name: string]: string[] } = { + npm: [ + "install", + "left-pad", + "--save", + "--save-dev", + "--save-optional", + "--save-exact", + "--silent", + "--ignore-scripts", + ], + yarn: [ + "add", + "left-pad", + "--dev", + "--optional", + "--exact", + "--silent", + "--ignore-scripts", + ], + yarn2: [ + "add", + "left-pad", + "--dev", + "--optional", + "--exact", + "--silent", + "--mode=skip-build", + ], + pnpm: [ + "i", + "--shamefully-hoist", + "left-pad", + "--save-dev", + "--save-optional", + "--save-exact", + "--silent", + "--ignore-scripts", + ], + bun: [ + "install", + "left-pad", + "--save", + "--dev", + "--optional", + "--exact", + "--silent", + "--ignore-scripts", + ], + }; + + for (const { name, ctor } of managers) { + describe(name, () => { + it("maps every install option to its own flag", async () => { + const args = await installArgs(name, ctor, allOptions); + assert.deepEqual(args, expectedInstallArgs[name]); + }); + + it("passes no option flags when no options are set", async () => { + const args = await installArgs(name, ctor, {}); + const expected = expectedInstallArgs[name].filter( + (arg) => !arg.startsWith("--") || arg === "--shamefully-hoist", + ); + assert.deepEqual(args, expected); + }); + + it("never passes CLI-internal options to the command line", async () => { + const args = await installArgs(name, ctor, { + path: "/some/path", + frameworkPath: "/some/framework", + }); + for (const arg of args) { + assert.notMatch(arg, /path|framework/i); + } + }); + + it("skips the install when disableNpmInstall is set", async () => { + const injector = createTestInjector(name, ctor); + const manager = injector.resolve(name); + const childProcess = + injector.resolve("childProcess"); + await manager.install("left-pad", projectDir, { + disableNpmInstall: true, + }); + assert.lengthOf(childProcess.spawnedArgs, 0); + }); + }); + } + + describe("save: false", () => { + it("maps to --no-save where the package manager supports it", async () => { + assert.include( + await installArgs("npm", NpmPackageManager, { save: false }), + "--no-save", + ); + assert.include( + await installArgs("bun", BunPackageManager, { save: false }), + "--no-save", + ); + }); + + it("is dropped where the package manager has no such flag", async () => { + for (const { name, ctor } of managers.filter( + (m) => m.name !== "npm" && m.name !== "bun", + )) { + const args = await installArgs(name, ctor, { save: false }); + assert.notInclude(args, "--no-save", name); + } + }); + }); + + describe("getInstalledPackagePath", () => { + const repoRoot = path.join(__dirname, "..", ".."); + + for (const { name, ctor } of managers) { + it(`${name} resolves an installed package from the given directory`, async () => { + const manager = createTestInjector( + name, + ctor, + ).resolve(name); + const resolved = manager.getInstalledPackagePath("lodash", repoRoot); + assert.equal(resolved, path.join(repoRoot, "node_modules", "lodash")); + }); + + it(`${name} returns null for a package that is not installed`, async () => { + const manager = createTestInjector( + name, + ctor, + ).resolve(name); + assert.isNull( + manager.getInstalledPackagePath( + "definitely-not-installed-package", + repoRoot, + ), + ); + }); + } + }); + + describe("uninstall", () => { + const expected: { [name: string]: string } = { + npm: "npm uninstall left-pad --save", + yarn: "yarn remove left-pad", + yarn2: "yarn remove left-pad", + pnpm: "pnpm remove left-pad", + bun: "bun remove left-pad --save", + }; + + for (const { name, ctor } of managers) { + it(`${name} maps save onto its own remove command`, async () => { + assert.equal(await uninstallCommand(name, ctor, true), expected[name]); + }); + } + }); +}); diff --git a/test/plugins-service.ts b/test/plugins-service.ts index edf62e97b0..94b33dce79 100644 --- a/test/plugins-service.ts +++ b/test/plugins-service.ts @@ -1,12 +1,12 @@ import { Yok } from "../lib/common/yok"; import * as stubs from "./stubs"; -import { PackageManager } from "../lib/package-manager"; -import { PackageInstallationManager } from "../lib/package-installation-manager"; -import { NodePackageManager } from "../lib/node-package-manager"; -import { YarnPackageManager } from "../lib/yarn-package-manager"; -import { Yarn2PackageManager } from "../lib/yarn2-package-manager"; -import { PnpmPackageManager } from "../lib/pnpm-package-manager"; -import { BunPackageManager } from "../lib/bun-package-manager"; +import { PackageManager } from "../lib/package-managers"; +import { PackageInstallationManager } from "../lib/package-managers/package-installation-manager"; +import { NpmPackageManager } from "../lib/package-managers/npm"; +import { YarnPackageManager } from "../lib/package-managers/yarn"; +import { Yarn2PackageManager } from "../lib/package-managers/yarn2"; +import { PnpmPackageManager } from "../lib/package-managers/pnpm"; +import { BunPackageManager } from "../lib/package-managers/bun"; import { ProjectData } from "../lib/project-data"; import { ChildProcess } from "../lib/common/child-process"; import { Options } from "../lib/options"; @@ -69,13 +69,14 @@ function createTestInjector() { testInjector.register("messagesService", MessagesService); testInjector.register("userSettingsService", { getSettingValue: async (settingName: string): Promise => undefined, + getSettingValueSync: (settingName: string): void => undefined, }); testInjector.register("packageManager", PackageManager); testInjector.register( "projectConfigService", stubs.PackageInstallationManagerStub, ); - testInjector.register("npm", NodePackageManager); + testInjector.register("npm", NpmPackageManager); testInjector.register("yarn", YarnPackageManager); testInjector.register("yarn2", Yarn2PackageManager); testInjector.register("pnpm", PnpmPackageManager); diff --git a/test/pnpm-package-manager.ts b/test/pnpm-package-manager.ts index 3c825f877a..16f8b02ac8 100644 --- a/test/pnpm-package-manager.ts +++ b/test/pnpm-package-manager.ts @@ -3,7 +3,7 @@ import { Yok } from "../lib/common/yok"; import * as stubs from "./stubs"; import { assert } from "chai"; import { setIsInteractive } from "../lib/common/helpers"; -import { PnpmPackageManager } from "../lib/pnpm-package-manager"; +import { PnpmPackageManager } from "../lib/package-managers/pnpm"; import { IInjector } from "../lib/common/definitions/yok"; class RecordingChildProcessStub extends stubs.ChildProcessStub { @@ -146,7 +146,7 @@ describe("pnpm-package-manager", () => { fs.textFiles[npmrcPath] = `registry=https://example.com\n${layoutKey}=*types*\n`; - await pnpm.install(projectDir, projectDir, {} as any); + await pnpm.install(projectDir, projectDir, {}); assert.deepEqual(childProcess.spawnedArgs[0], ["i"]); }); @@ -180,7 +180,7 @@ describe("pnpm-package-manager", () => { ignoreScripts: true, path: "/some/path", frameworkPath: "/some/framework", - } as any); + }); const args = childProcess.spawnedArgs[0]; assert.include(args, "--ignore-scripts"); @@ -197,7 +197,7 @@ describe("pnpm-package-manager", () => { setIsInteractive(() => false); try { - await pnpm.install(projectDir, projectDir, {} as any); + await pnpm.install(projectDir, projectDir, {}); } finally { setIsInteractive(undefined); } @@ -217,13 +217,12 @@ describe("pnpm-package-manager", () => { const childProcess = testInjector.resolve("childProcess"); - await pnpm.install("left-pad", projectDir, { save: true } as any); + await pnpm.install("left-pad", projectDir, { save: true }); assert.deepEqual(childProcess.spawnedArgs[0], [ "i", "--shamefully-hoist", "left-pad", - "--save", ]); }); }); diff --git a/test/project-templates-service.ts b/test/project-templates-service.ts index 10abc12520..6635526130 100644 --- a/test/project-templates-service.ts +++ b/test/project-templates-service.ts @@ -6,7 +6,7 @@ import * as path from "path"; import * as constants from "../lib/constants"; import { INpmInstallResultInfo, - INodePackageManagerInstallOptions, + IPackageInstallOptions, INpmPackageNameParts, INpmInstallOptions, } from "../lib/declarations"; @@ -38,7 +38,7 @@ function createTestInjector( public async install( packageName: string, pathToSave: string, - config: INodePackageManagerInstallOptions + config: IPackageInstallOptions ): Promise { if (configuration.shouldNpmInstallThrow) { throw new Error("NPM install throws error."); diff --git a/test/services/android-plugin-build-service.ts b/test/services/android-plugin-build-service.ts index c2107cf4c1..8e5a8c3748 100644 --- a/test/services/android-plugin-build-service.ts +++ b/test/services/android-plugin-build-service.ts @@ -10,6 +10,7 @@ import * as FsLib from "../../lib/common/file-system"; import * as path from "path"; import * as stubs from "../stubs"; import { mkdtempSync } from "fs"; +import { resolvePackagePath } from "../../lib/helpers/package-path-helper"; import { tmpdir } from "os"; import { IFileSystem, @@ -128,6 +129,8 @@ describe("androidPluginBuildService", () => { addProjectRuntime?: boolean; }): any { return { + getInstalledPackagePath: (packageName: string, fromDir: string): string => + resolvePackagePath(packageName, { paths: [fromDir] }) || null, getRegistryPackageData: async (packageName: string): Promise => { const result: any = []; result["dist-tags"] = { latest: "4.1.2" }; @@ -149,9 +152,9 @@ describe("androidPluginBuildService", () => { return result; }, - view: async (packageName: string, config: any): Promise => { + view: async (packageName: string, field?: string): Promise => { let result: any = null; - if (config && config.gradle) { + if (field === "gradle") { const packageNameParts = packageName.split("@"); const packageVersion = packageNameParts[packageNameParts.length - 1]; switch (packageVersion) { @@ -170,7 +173,7 @@ describe("androidPluginBuildService", () => { } } - if (config && config["dist-tags"]) { + if (field === "dist-tags") { result = { latest: "4.1.2", }; diff --git a/test/services/bundler/bundler-compiler-service.ts b/test/services/bundler/bundler-compiler-service.ts index 1ec3a6b057..94b1c84986 100644 --- a/test/services/bundler/bundler-compiler-service.ts +++ b/test/services/bundler/bundler-compiler-service.ts @@ -58,6 +58,7 @@ function createTestInjector( const testInjector = new Yok(); testInjector.register("packageManager", { getPackageManagerName: async () => packageManager, + getInstalledPackagePath: (): string => null, }); testInjector.register("bundlerCompilerService", BundlerCompilerService); testInjector.register("childProcess", {}); diff --git a/test/services/doctor-service.ts b/test/services/doctor-service.ts index 862480c02c..2adfc0eb2f 100644 --- a/test/services/doctor-service.ts +++ b/test/services/doctor-service.ts @@ -6,7 +6,7 @@ import * as path from "path"; import * as sinon from "sinon"; import * as _ from "lodash"; import { IProjectDataService } from "../../lib/definitions/project"; -import { IVersionsService } from "../../lib/declarations"; +import { IVersionsService, IPackageManager } from "../../lib/declarations"; import { ICheckEnvironmentRequirementsInput, ICheckEnvironmentRequirementsOutput, @@ -45,6 +45,7 @@ class DoctorServiceInheritor extends DoctorService { $terminalSpinnerService: ITerminalSpinnerService, $versionsService: IVersionsService, $settingsService: ISettingsService, + $packageManager: IPackageManager, ) { super( $analyticsService, @@ -57,6 +58,7 @@ class DoctorServiceInheritor extends DoctorService { $terminalSpinnerService, $versionsService, $settingsService, + $packageManager, ); } @@ -93,6 +95,12 @@ describe("doctorService", () => { }, }); testInjector.register("versionsService", {}); + testInjector.register("packageManager", { + getInstalledPackagePath: (packageName: string, fromDir: string): string => + packageName === "tns-core-modules" + ? path.join(fromDir, "node_modules", packageName) + : null, + }); testInjector.register("settingsService", { getProfileDir: (): string => "", }); @@ -349,7 +357,7 @@ const Observable = require("tns-core-modules-widgets/data/observable").Observabl }, ]; - it("getDeprecatedShortImportsInFiles returns correct results", () => { + it("getDeprecatedShortImportsInFiles returns correct results", async () => { const testInjector = createTestInjector(); const doctorService = testInjector.resolve("doctorService"); @@ -365,15 +373,32 @@ const Observable = require("tns-core-modules-widgets/data/observable").Observabl } }; - testData.forEach(({ filesContents, expectedShortImports }) => { + for (const { filesContents, expectedShortImports } of testData) { fs.readText = (filePath) => filesContents[filePath]; - const shortImports = doctorService.getDeprecatedShortImportsInFiles( - _.keys(filesContents), - "projectDir", - ); + const shortImports = + doctorService.getDeprecatedShortImportsInFiles( + _.keys(filesContents), + "projectDir", + ); assert.deepStrictEqual(shortImports, expectedShortImports); - }); + } + }); + + it("getDeprecatedShortImportsInFiles returns no results when tns-core-modules is not installed", async () => { + const testInjector = createTestInjector(); + const packageManager = testInjector.resolve("packageManager"); + packageManager.getInstalledPackagePath = (): string => null; + const doctorService = + testInjector.resolve("doctorService"); + const fs = testInjector.resolve("fs"); + fs.readText = () => 'const application = require("application");'; + + const shortImports = doctorService.getDeprecatedShortImportsInFiles( + ["file1"], + "projectDir", + ); + assert.deepStrictEqual(shortImports, []); }); }); diff --git a/test/services/extensibility-service.ts b/test/services/extensibility-service.ts index a6b1970ee5..132acff774 100644 --- a/test/services/extensibility-service.ts +++ b/test/services/extensibility-service.ts @@ -2,12 +2,12 @@ import { ExtensibilityService } from "../../lib/services/extensibility-service"; import { Yok } from "../../lib/common/yok"; import * as stubs from "../stubs"; import { assert } from "chai"; -import { NodePackageManager } from "../../lib/node-package-manager"; -import { PackageManager } from "../../lib/package-manager"; -import { YarnPackageManager } from "../../lib/yarn-package-manager"; -import { Yarn2PackageManager } from "../../lib/yarn2-package-manager"; -import { PnpmPackageManager } from "../../lib/pnpm-package-manager"; -import { BunPackageManager } from "../../lib/bun-package-manager"; +import { NpmPackageManager } from "../../lib/package-managers/npm"; +import { PackageManager } from "../../lib/package-managers"; +import { YarnPackageManager } from "../../lib/package-managers/yarn"; +import { Yarn2PackageManager } from "../../lib/package-managers/yarn2"; +import { PnpmPackageManager } from "../../lib/package-managers/pnpm"; +import { BunPackageManager } from "../../lib/package-managers/bun"; import * as constants from "../../lib/constants"; import { ChildProcess } from "../../lib/common/child-process"; import { CommandsDelimiters } from "../../lib/common/constants"; @@ -74,8 +74,9 @@ describe("extensibilityService", () => { }); testInjector.register("userSettingsService", { getSettingValue: async (settingName: string): Promise => undefined, + getSettingValueSync: (settingName: string): void => undefined, }); - testInjector.register("npm", NodePackageManager); + testInjector.register("npm", NpmPackageManager); testInjector.register("yarn", YarnPackageManager); testInjector.register("yarn2", Yarn2PackageManager); testInjector.register("pnpm", PnpmPackageManager); @@ -87,6 +88,17 @@ describe("extensibilityService", () => { return testInjector; }; + const stubInstalledExtensions = ( + testInjector: IInjector, + resolve: (extensionName: string, fromDir: string) => string, + ): void => { + const packageManager = testInjector.resolve("packageManager"); + packageManager.getInstalledPackagePath = ( + packageName: string, + fromDir: string, + ): string => resolve(packageName, fromDir); + }; + const getExpectedInstallationPathForExtension = ( testInjector: IInjector, extensionName: string, @@ -245,15 +257,16 @@ describe("extensibilityService", () => { ); }); - it("passes save and save-exact options to npm install", async () => { + it("passes save and exact options to the package manager", async () => { const extensionName = "extension1"; const argsPassedToNpmInstall = await getArgsPassedToNpmInstallDuringInstallExtensionCall( extensionName, ); - const expectedNpmConfg: any = { save: true }; - expectedNpmConfg["save-exact"] = true; - assert.deepStrictEqual(argsPassedToNpmInstall.config, expectedNpmConfg); + assert.deepStrictEqual(argsPassedToNpmInstall.config, { + save: true, + exact: true, + }); }); it("passes full path to extensions dir for installation", async () => { @@ -319,14 +332,9 @@ describe("extensibilityService", () => { const fs: IFileSystem = testInjector.resolve("fs"); const extensionNames = ["extension1", "extension2", "extension3"]; fs.exists = (pathToCheck: string): boolean => true; - fs.readDirectory = (dir: string): string[] => { - assert.deepStrictEqual( - path.basename(dir), - constants.NODE_MODULES_FOLDER_NAME, - ); - // Simulates extensions are installed in node_modules - return extensionNames; - }; + stubInstalledExtensions(testInjector, (name, fromDir) => + path.join(fromDir, constants.NODE_MODULES_FOLDER_NAME, name), + ); mockFsReadJson(testInjector, extensionNames); @@ -358,20 +366,15 @@ describe("extensibilityService", () => { fs.exists = (pathToCheck: string): boolean => path.basename(pathToCheck) !== extensionNames[0]; - let isFirstReadDirExecution = true; - fs.readDirectory = (dir: string): string[] => { - assert.deepStrictEqual( - path.basename(dir), - constants.NODE_MODULES_FOLDER_NAME, - ); - // Simulates extensions are installed in node_modules - if (isFirstReadDirExecution) { - isFirstReadDirExecution = false; - return extensionNames.filter((ext) => ext !== "extension1"); - } else { - return extensionNames; + // extension1 is missing until the service installs it + let isExtension1Installed = false; + stubInstalledExtensions(testInjector, (name, fromDir) => { + if (name === "extension1" && !isExtension1Installed) { + isExtension1Installed = true; + return null; } - }; + return path.join(fromDir, constants.NODE_MODULES_FOLDER_NAME, name); + }); mockFsReadJson(testInjector, extensionNames); @@ -414,14 +417,9 @@ describe("extensibilityService", () => { const fs: IFileSystem = testInjector.resolve("fs"); const extensionNames = ["extension1", "extension2", "extension3"]; fs.exists = (pathToCheck: string): boolean => true; - fs.readDirectory = (dir: string): string[] => { - assert.deepStrictEqual( - path.basename(dir), - constants.NODE_MODULES_FOLDER_NAME, - ); - // Simulates extensions are installed in node_modules - return extensionNames; - }; + stubInstalledExtensions(testInjector, (name, fromDir) => + path.join(fromDir, constants.NODE_MODULES_FOLDER_NAME, name), + ); mockFsReadJson(testInjector, extensionNames); @@ -468,7 +466,7 @@ describe("extensibilityService", () => { } }); - it("rejects all promises when unable to read node_modules dir (simulate EPERM error)", async () => { + it("rejects all promises when the package manager cannot locate extensions (simulate EPERM error)", async () => { const testInjector = getTestInjector(); const extensionNames = ["extension1", "extension2", "extension3"]; const fs: IFileSystem = testInjector.resolve("fs"); @@ -479,14 +477,10 @@ describe("extensibilityService", () => { mockFsReadJson(testInjector, extensionNames); let isReadDirCalled = false; - fs.readDirectory = (dir: string): string[] => { + stubInstalledExtensions(testInjector, () => { isReadDirCalled = true; - assert.deepStrictEqual( - path.basename(dir), - constants.NODE_MODULES_FOLDER_NAME, - ); throw new Error(expectedErrorMessage); - }; + }); const extensibilityService: IExtensibilityService = testInjector.resolve(ExtensibilityService); @@ -531,14 +525,10 @@ describe("extensibilityService", () => { mockFsReadJson(testInjector, extensionNames); let isReadDirCalled = false; - fs.readDirectory = (dir: string): string[] => { + stubInstalledExtensions(testInjector, () => { isReadDirCalled = true; - assert.deepStrictEqual( - path.basename(dir), - constants.NODE_MODULES_FOLDER_NAME, - ); - return []; - }; + return null; + }); let isNpmInstallCalled = false; const npm: INodePackageManager = testInjector.resolve("npm"); diff --git a/test/services/test-execution-service.ts b/test/services/test-execution-service.ts index ad6ad5c62a..bd85b8b3f6 100644 --- a/test/services/test-execution-service.ts +++ b/test/services/test-execution-service.ts @@ -8,10 +8,18 @@ import { IDictionary } from "../../lib/common/declarations"; const karmaPluginName = "karma"; const unitTestsPluginName = "@nativescript/unit-test-runner"; -function getTestExecutionService(): ITestExecutionService { +function getTestExecutionService( + installedPackages: string[], +): ITestExecutionService { const injector = new InjectorStub(); injector.register("testExecutionService", TestExecutionService); injector.register("runController", {}); + injector.register("packageManager", { + getInstalledPackagePath: (packageName: string, fromDir: string): string => + installedPackages.indexOf(packageName) !== -1 + ? `${fromDir}/node_modules/${packageName}` + : null, + }); return injector.resolve("testExecutionService"); } @@ -28,8 +36,7 @@ function getDependenciesObj(deps: string[]): IDictionary { describe("testExecutionService", () => { const testCases = [ { - name: - "should return false when the project has no dependencies and dev dependencies", + name: "should return false when the project has no dependencies and dev dependencies", expectedCanStartKarmaServer: false, projectData: { dependencies: {}, devDependencies: {} }, }, @@ -50,8 +57,7 @@ describe("testExecutionService", () => { }, }, { - name: - "should return true when the project has the required plugins as dependencies", + name: "should return true when the project has the required plugins as dependencies", expectedCanStartKarmaServer: true, projectData: { dependencies: getDependenciesObj([ @@ -62,8 +68,7 @@ describe("testExecutionService", () => { }, }, { - name: - "should return true when the project has the required plugins as dev dependencies", + name: "should return true when the project has the required plugins as dev dependencies", expectedCanStartKarmaServer: true, projectData: { dependencies: {}, @@ -74,8 +79,7 @@ describe("testExecutionService", () => { }, }, { - name: - "should return true when the project has the required plugins as dev and normal dependencies", + name: "should return true when the project has the required plugins as dev and normal dependencies", expectedCanStartKarmaServer: true, projectData: { dependencies: getDependenciesObj([karmaPluginName]), @@ -87,35 +91,15 @@ describe("testExecutionService", () => { describe("canStartKarmaServer", () => { _.each(testCases, (testCase: any) => { it(`${testCase.name}`, async () => { - const testExecutionService = getTestExecutionService(); - - // todo: cleanup monkey-patch with a friendlier syntax (util?) - // MOCK require.resolve - const Module = require("module"); - const originalResolveFilename = Module._resolveFilename; - - Module._resolveFilename = function (...args: any) { - if ( - args[0].startsWith(karmaPluginName) && - (testCase.projectData.dependencies[karmaPluginName] || - testCase.projectData.devDependencies[karmaPluginName]) - ) { - // override with a "random" built-in module to - // ensure the module can be resolved - args[0] = "fs"; - } + const installedPackages = _.keys({ + ...testCase.projectData.dependencies, + ...testCase.projectData.devDependencies, + }); + const testExecutionService = getTestExecutionService(installedPackages); - return originalResolveFilename.apply(this, args); - }; - // END MOCK - - const canStartKarmaServer = await testExecutionService.canStartKarmaServer( - testCase.projectData - ); + const canStartKarmaServer = + await testExecutionService.canStartKarmaServer(testCase.projectData); assert.equal(canStartKarmaServer, testCase.expectedCanStartKarmaServer); - - // restore mock - Module._resolveFilename = originalResolveFilename; }); }); }); diff --git a/test/stubs.ts b/test/stubs.ts index 8011d1c0be..6be3af35fc 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -17,7 +17,7 @@ import { INpmInstallOptions, INodePackageManager, INpmInstallResultInfo, - INodePackageManagerInstallOptions, + IPackageInstallOptions, INpmPackageNameParts, INpmsResult, IAndroidToolsInfoData, @@ -457,10 +457,14 @@ export class PackageInstallationManagerStub implements IPackageInstallationManag export class NodePackageManagerStub implements INodePackageManager { constructor() {} + public getInstalledPackagePath(packageName: string, fromDir: string): string { + return null; + } + public async install( packageName: string, pathToSave: string, - config: INodePackageManagerInstallOptions, + options: IPackageInstallOptions, ): Promise { return { name: packageName, @@ -476,11 +480,11 @@ export class NodePackageManagerStub implements INodePackageManager { return ""; } - public async search(filter: string[], config: any): Promise { + public async search(filter: string[]): Promise { return ""; } - public async view(packageName: string, config: Object): Promise { + public async view(packageName: string, field?: string): Promise { return {}; } diff --git a/test/tools/node-modules/node-modules-dependencies-builder.ts b/test/tools/node-modules/node-modules-dependencies-builder.ts index 8ea64163ef..8f6898ddc7 100644 --- a/test/tools/node-modules/node-modules-dependencies-builder.ts +++ b/test/tools/node-modules/node-modules-dependencies-builder.ts @@ -14,6 +14,7 @@ import { import * as os from "os"; import * as fs from "fs"; import { FileSystem } from "../../../lib/common/file-system"; +import { resolvePackagePath } from "../../../lib/helpers/package-path-helper"; interface IDependencyInfo { name: string; @@ -39,6 +40,10 @@ describe("nodeModulesDependenciesBuilder", () => { const getTestInjector = (): IInjector => { const testInjector = new Yok(); testInjector.register("fs", FileSystem); + testInjector.register("packageManager", { + getInstalledPackagePath: (packageName: string, fromDir: string): string => + resolvePackagePath(packageName, { paths: [fromDir] }) || null, + }); return testInjector; };