From e58a9e8bb9f44a00157775d6cd67842cbfa6f581 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 14 Sep 2026 17:24:11 -0300 Subject: [PATCH 01/17] refactor(commands): infer command services from setup A command's services type is now read off its setup function with ReturnType instead of being declared beside it and kept in sync by hand. --- defining-commands.md | 62 ++++++++++++++----- lib/commands/add-platform.ts | 12 ++-- lib/commands/apple-login.ts | 13 ++-- lib/commands/appstore-list.ts | 17 ++--- lib/commands/appstore-upload.ts | 20 ++---- lib/commands/build.ts | 14 +---- lib/commands/clean.ts | 16 +---- lib/commands/command-base.ts | 23 +++---- lib/commands/config.ts | 12 ++-- lib/commands/create-project.ts | 12 ++-- lib/commands/debug.ts | 30 +++------ lib/commands/embedding/embed.ts | 12 +--- .../extensibility/install-extension.ts | 11 ++-- lib/commands/extensibility/list-extensions.ts | 11 ++-- .../extensibility/uninstall-extension.ts | 11 ++-- lib/commands/fonts.ts | 11 +--- lib/commands/generate-assets.ts | 14 ++--- lib/commands/hooks/common.ts | 14 ++--- lib/commands/install.ts | 17 +---- lib/commands/list-platforms.ts | 12 ++-- lib/commands/migrate.ts | 12 +--- lib/commands/native-add.ts | 12 ++-- lib/commands/open.ts | 28 +++------ lib/commands/platform-clean.ts | 15 ++--- lib/commands/plugin/add-plugin.ts | 12 ++-- lib/commands/plugin/build-plugin.ts | 17 ++--- lib/commands/plugin/create-plugin.ts | 17 ++--- lib/commands/plugin/list-plugins.ts | 12 ++-- lib/commands/plugin/remove-plugin.ts | 13 ++-- lib/commands/plugin/update-plugin.ts | 12 ++-- lib/commands/post-install.ts | 16 ++--- lib/commands/prepare.ts | 11 +--- lib/commands/preview.ts | 12 +--- lib/commands/remove-platform.ts | 13 ++-- lib/commands/resources/resources-update.ts | 12 ++-- lib/commands/test-init.ts | 17 +---- lib/commands/test.ts | 22 +------ lib/commands/typings.ts | 16 +---- lib/commands/update-platform.ts | 15 ++--- lib/commands/update.ts | 14 +---- lib/common/commands/analytics.ts | 15 ++--- lib/common/commands/autocompletion.ts | 11 ++-- .../commands/device/device-log-stream.ts | 14 ++--- lib/common/commands/device/get-file.ts | 10 +-- .../commands/device/list-applications.ts | 11 ++-- lib/common/commands/device/list-devices.ts | 20 ++---- lib/common/commands/device/list-files.ts | 12 ++-- lib/common/commands/device/put-file.ts | 10 +-- lib/common/commands/device/run-application.ts | 12 ++-- .../commands/device/stop-application.ts | 10 +-- .../commands/device/uninstall-application.ts | 10 +-- lib/common/commands/doctor.ts | 12 +--- lib/common/commands/help.ts | 9 +-- lib/common/commands/package-manager-get.ts | 11 ++-- lib/common/commands/package-manager-set.ts | 12 ++-- lib/common/commands/preuninstall.ts | 14 ++--- lib/common/commands/proxy/proxy-base.ts | 12 ++-- lib/common/commands/proxy/proxy-set.ts | 12 +--- 58 files changed, 310 insertions(+), 549 deletions(-) diff --git a/defining-commands.md b/defining-commands.md index 49ca625488..2eeda4792e 100644 --- a/defining-commands.md +++ b/defining-commands.md @@ -169,11 +169,11 @@ options: { So a redeclaration of the same name with the same type is silent. What the CLI still warns about at registration is a redeclaration that changes what the -spelling *means*: +spelling _means_: - a declared option whose name matches a CLI-wide one but whose type differs — `verbose: stringOption()` against the CLI's boolean `--verbose`; -- an alias that belongs to a *different* CLI-wide option — `output: +- an alias that belongs to a _different_ CLI-wide option — `output: stringOption({ alias: "p" })` steals `--path`'s shorthand. Restating an option's own shorthand (`path: stringOption({ alias: "p" })`) is fine. @@ -449,6 +449,40 @@ top of `run`; nothing else changes. "Once per invocation" means once across `canExecute`, `run` and `postRun` together — whichever of them the CLI reaches first triggers it, and the rest reuse the value. +When several commands share a setup, or a helper outside the definition takes +the services as a parameter, lift it into a named function and derive the type +from it instead of writing the shape out by hand: + +```ts +export function setupWidgetAddCommand() { + const projectData = inject(ProjectData); + projectData.initializeProjectData(); + return { projectData, widgets: inject(WidgetService) }; +} +export type IWidgetAddCommandServices = ReturnType< + typeof setupWidgetAddCommand +>; + +export function canAddWidget(services: IWidgetAddCommandServices): boolean { + return !!services.projectData.projectDir; +} + +export default defineCommand({ + name: "widget|add", + arguments: "any", + setup: setupWidgetAddCommand, + canExecute: (ctx, services) => canAddWidget(services), + async run(ctx, { widgets }) { + await widgets.add(ctx.args); + }, +}); +``` + +Leave the setup function's return type off: the alias reads what the body +infers, so annotating the function with the alias makes the pair circular. Read +a setup curried over a parameter — `setupX(platform)` returning the setup +itself — through its inner function, `ReturnType>`. + `run`'s return value, and `postRun` ----------------------------------- @@ -504,7 +538,7 @@ all — or the definition itself, which it defines on your behalf, so registerin a command is one call. Either way the definition is validated before it reaches the registry. It claims every name the definition declares, through the `CommandRegistry` the target injector provides, and returns a -`DeferredCommandResult` — see *The owner is ambient* below. The command instance +`DeferredCommandResult` — see _The owner is ambient_ below. The command instance is built by a factory on first resolution and cached. Pass providers as the second argument to scope the command to a child injector @@ -523,7 +557,7 @@ That is how one definition serves several commands that differ only in data — the platform each one targets — instead of one command subclassing another. **Which injector it registers against is not a parameter.** It is the injector -of the current injection context — see *The owner is ambient* below — and the +of the current injection context — see _The owner is ambient_ below — and the CLI's own injector outside one. To register against some other injector, run the call in its context: @@ -722,16 +756,16 @@ A definition is compiled into an ordinary `ICommand`, so nothing downstream — the registry, the router, hooks, help, analytics — knows the difference. The mapping is: -| Definition | `ICommand` | -| --------------------------------- | --------------------------------------------------- | -| `options` | `dashedOptions` | -| `run` | `execute`, wrapped in an injection context | -| `arguments`, `canExecute` | `canExecute`: policy enforced, then the refinement | -| `setup` | — run inside `canExecute`/`execute`, memoised | -| `postRun` | `postCommandAction`, with `run`'s return value | -| `allowUnknownOptions` | `skipOptionsValidation` | -| — | `allowedParameters`, always `[]` | -| `disableAnalytics`, `enableHooks` | passed through unchanged | +| Definition | `ICommand` | +| --------------------------------- | -------------------------------------------------- | +| `options` | `dashedOptions` | +| `run` | `execute`, wrapped in an injection context | +| `arguments`, `canExecute` | `canExecute`: policy enforced, then the refinement | +| `setup` | — run inside `canExecute`/`execute`, memoised | +| `postRun` | `postCommandAction`, with `run`'s return value | +| `allowUnknownOptions` | `skipOptionsValidation` | +| — | `allowedParameters`, always `[]` | +| `disableAnalytics`, `enableHooks` | passed through unchanged | The compiled command always exposes `canExecute`, because `CommandsService` stops consulting `allowedParameters` as soon as a command has one — the adapter diff --git a/lib/commands/add-platform.ts b/lib/commands/add-platform.ts index 79faa9f01e..8ecbd166b1 100644 --- a/lib/commands/add-platform.ts +++ b/lib/commands/add-platform.ts @@ -1,7 +1,6 @@ import { canExecuteCommandBase, injectPlatformCommandServices, - IPlatformCommandServices, } from "./command-base"; import { IPlatformCommandHelper } from "../declarations"; import { IErrors } from "../common/declarations"; @@ -21,12 +20,7 @@ export type AddPlatformCommandContext = CommandContext< typeof addPlatformCommandOptions >; -export interface IAddPlatformCommandServices extends IPlatformCommandServices { - $errors: IErrors; - $platformCommandHelper: IPlatformCommandHelper; -} - -export function setupAddPlatformCommand(): IAddPlatformCommandServices { +export function setupAddPlatformCommand() { const services = { ...injectPlatformCommandServices(), $errors: inject("errors"), @@ -39,6 +33,10 @@ export function setupAddPlatformCommand(): IAddPlatformCommandServices { return services; } +export type IAddPlatformCommandServices = ReturnType< + typeof setupAddPlatformCommand +>; + export async function canExecuteAddPlatformCommand( context: AddPlatformCommandContext, services: IAddPlatformCommandServices, diff --git a/lib/commands/apple-login.ts b/lib/commands/apple-login.ts index fc0a523fda..5f87cea262 100644 --- a/lib/commands/apple-login.ts +++ b/lib/commands/apple-login.ts @@ -5,14 +5,7 @@ import { IApplePortalSessionService } from "../services/apple-portal/definitions export type AppleLoginCommandContext = CommandContext; -export interface IAppleLoginCommandServices { - $applePortalSessionService: IApplePortalSessionService; - $errors: IErrors; - $logger: ILogger; - $prompter: IPrompter; -} - -export function setupAppleLoginCommand(): IAppleLoginCommandServices { +export function setupAppleLoginCommand() { return { $applePortalSessionService: inject( "applePortalSessionService", @@ -23,6 +16,10 @@ export function setupAppleLoginCommand(): IAppleLoginCommandServices { }; } +export type IAppleLoginCommandServices = ReturnType< + typeof setupAppleLoginCommand +>; + export async function runAppleLoginCommand( context: AppleLoginCommandContext, services: IAppleLoginCommandServices, diff --git a/lib/commands/appstore-list.ts b/lib/commands/appstore-list.ts index 2417008c28..17103adfa7 100644 --- a/lib/commands/appstore-list.ts +++ b/lib/commands/appstore-list.ts @@ -22,18 +22,7 @@ export type ListiOSAppsCommandContext = CommandContext< typeof listiOSAppsCommandOptions >; -export interface IListiOSAppsCommandServices { - $applePortalApplicationService: IApplePortalApplicationService; - $applePortalSessionService: IApplePortalSessionService; - $devicePlatformsConstants: Mobile.IDevicePlatformsConstants; - $errors: IErrors; - $logger: ILogger; - $platformValidationService: IPlatformValidationService; - $projectData: IProjectData; - $prompter: IPrompter; -} - -export function setupListiOSAppsCommand(): IListiOSAppsCommandServices { +export function setupListiOSAppsCommand() { const services = { $applePortalApplicationService: inject( "applePortalApplicationService", @@ -57,6 +46,10 @@ export function setupListiOSAppsCommand(): IListiOSAppsCommandServices { return services; } +export type IListiOSAppsCommandServices = ReturnType< + typeof setupListiOSAppsCommand +>; + export async function runListiOSAppsCommand( context: ListiOSAppsCommandContext, services: IListiOSAppsCommandServices, diff --git a/lib/commands/appstore-upload.ts b/lib/commands/appstore-upload.ts index 81eadc6281..7814e9cb2f 100644 --- a/lib/commands/appstore-upload.ts +++ b/lib/commands/appstore-upload.ts @@ -32,21 +32,7 @@ export type PublishIOSCommandContext = CommandContext< typeof publishIOSCommandOptions >; -export interface IPublishIOSCommandServices { - $applePortalSessionService: IApplePortalSessionService; - $buildController: BuildController; - $devicePlatformsConstants: Mobile.IDevicePlatformsConstants; - $errors: IErrors; - $hostInfo: IHostInfo; - $itmsTransporterService: IITMSTransporterService; - $logger: ILogger; - $options: IOptions; - $platformValidationService: IPlatformValidationService; - $projectData: IProjectData; - $prompter: IPrompter; -} - -export function setupPublishIOSCommand(): IPublishIOSCommandServices { +export function setupPublishIOSCommand() { const services = { $applePortalSessionService: inject( "applePortalSessionService", @@ -73,6 +59,10 @@ export function setupPublishIOSCommand(): IPublishIOSCommandServices { return services; } +export type IPublishIOSCommandServices = ReturnType< + typeof setupPublishIOSCommand +>; + export function canExecutePublishIOSCommand( context: PublishIOSCommandContext, services: IPublishIOSCommandServices, diff --git a/lib/commands/build.ts b/lib/commands/build.ts index 8552f26e93..29ae6f753e 100644 --- a/lib/commands/build.ts +++ b/lib/commands/build.ts @@ -5,7 +5,6 @@ import { import { canExecuteCommandBase, injectPlatformCommandServices, - IPlatformCommandServices, validatePlatformOptions, } from "./command-base"; import { hasValidAndroidSigning } from "../common/helpers"; @@ -40,17 +39,6 @@ const buildCommandOptions = { keyStoreAliasPassword: stringOption(), } satisfies CommandOptionsSchema; -interface IBuildCommandServices extends IPlatformCommandServices { - platform: string; - isAndroid: boolean; - $errors: IErrors; - $logger: ILogger; - $buildController: IBuildController; - $buildDataService: IBuildDataService; - $migrateController: IMigrateController; - $androidBundleValidatorHelper: IAndroidBundleValidatorHelper; -} - const defineBuildCommand = ( name: TName, buildPlatform: BuildPlatform, @@ -60,7 +48,7 @@ const defineBuildCommand = ( description: "Builds the project for the selected target platform.", options: buildCommandOptions, arguments: "none", - setup(): IBuildCommandServices { + setup() { const devicePlatformsConstants = inject( "devicePlatformsConstants", ); diff --git a/lib/commands/clean.ts b/lib/commands/clean.ts index b193930020..28ccee4be9 100644 --- a/lib/commands/clean.ts +++ b/lib/commands/clean.ts @@ -88,19 +88,7 @@ const cleanCommandOptions = { export type CleanCommandContext = CommandContext; -export interface ICleanCommandServices { - $childProcess: IChildProcess; - $logger: ILogger; - $projectCleanupService: IProjectCleanupService; - $projectConfigService: IProjectConfigService; - $projectData: IProjectData; - $projectService: IProjectService; - $prompter: IPrompter; - $staticConfig: IStaticConfig; - $terminalSpinnerService: ITerminalSpinnerService; -} - -export function setupCleanCommand(): ICleanCommandServices { +export function setupCleanCommand() { return { $childProcess: inject("childProcess"), $logger: inject("logger"), @@ -120,6 +108,8 @@ export function setupCleanCommand(): ICleanCommandServices { }; } +export type ICleanCommandServices = ReturnType; + async function getNSProjectPathsInDirectory( services: ICleanCommandServices, dir = process.cwd(), diff --git a/lib/commands/command-base.ts b/lib/commands/command-base.ts index 748c54dcdd..48d0d2a9a6 100644 --- a/lib/commands/command-base.ts +++ b/lib/commands/command-base.ts @@ -9,20 +9,8 @@ import { import { ArgumentSpec } from "../common/define-command"; import { inject, Injector } from "../common/di"; -/** - * What the platform-validation helpers below need. A command definition's - * `setup` returns this shape (see `injectPlatformCommandServices`), so its - * result can be handed straight to them. - */ -export interface IPlatformCommandServices { - $options: IOptions; - $platformsDataService: IPlatformsDataService; - $platformValidationService: IPlatformValidationService; - $projectData: IProjectData; -} - /** Callable from `setup` and from `canExecute` before their first `await`. */ -export function injectPlatformCommandServices(): IPlatformCommandServices { +export function injectPlatformCommandServices() { return { $options: inject("options"), $platformsDataService: inject( @@ -35,6 +23,15 @@ export function injectPlatformCommandServices(): IPlatformCommandServices { }; } +/** + * What the platform-validation helpers below need. A command definition's + * `setup` returns this shape (see `injectPlatformCommandServices`), so its + * result can be handed straight to them. + */ +export type IPlatformCommandServices = ReturnType< + typeof injectPlatformCommandServices +>; + /** * The declarative form of `$platformCommandParameter`. Initializing the * project data is what makes the platform check possible, so it stays part of diff --git a/lib/commands/config.ts b/lib/commands/config.ts index b3d21b8eef..41792dce04 100644 --- a/lib/commands/config.ts +++ b/lib/commands/config.ts @@ -5,13 +5,7 @@ import { CommandContext, defineCommand } from "../common/define-command"; import { inject } from "../common/di"; import { color } from "../color"; -export interface IConfigCommandServices { - $projectConfigService: IProjectConfigService; - $logger: ILogger; - $errors: IErrors; -} - -export function injectConfigCommandServices(): IConfigCommandServices { +export function injectConfigCommandServices() { return { $projectConfigService: inject( "projectConfigService", @@ -21,6 +15,10 @@ export function injectConfigCommandServices(): IConfigCommandServices { }; } +export type IConfigCommandServices = ReturnType< + typeof injectConfigCommandServices +>; + function getValueString(value: SupportedConfigValues, depth = 0): string { const indent = () => " ".repeat(depth); if (typeof value === "object") { diff --git a/lib/commands/create-project.ts b/lib/commands/create-project.ts index f3a72dca02..01067dcf05 100644 --- a/lib/commands/create-project.ts +++ b/lib/commands/create-project.ts @@ -53,13 +53,7 @@ export type CreateProjectCommandContext = CommandContext< typeof createProjectCommandOptions >; -export interface ICreateProjectCommandServices { - $projectService: IProjectService; - $logger: ILogger; - $prompter: IPrompter; -} - -export function setupCreateProjectCommand(): ICreateProjectCommandServices { +export function setupCreateProjectCommand() { return { $projectService: inject("projectService"), $logger: inject("logger"), @@ -67,6 +61,10 @@ export function setupCreateProjectCommand(): ICreateProjectCommandServices { }; } +export type ICreateProjectCommandServices = ReturnType< + typeof setupCreateProjectCommand +>; + interface ITemplateChoice { key?: string; value: string; diff --git a/lib/commands/debug.ts b/lib/commands/debug.ts index 9da5a8aeb1..0cfe058dc4 100644 --- a/lib/commands/debug.ts +++ b/lib/commands/debug.ts @@ -28,7 +28,6 @@ import { import { canExecuteCommandBase, injectPlatformCommandServices, - IPlatformCommandServices, } from "./command-base"; import * as _ from "lodash"; @@ -53,21 +52,7 @@ const debugCommandOptions = { export type DebugCommandContext = CommandContext; -export interface IDebugCommandServices extends IPlatformCommandServices { - platform: string; - $cleanupService: ICleanupService; - $debugController: IDebugController; - $debugDataService: IDebugDataService; - $devicePlatformsConstants: Mobile.IDevicePlatformsConstants; - $devicesService: Mobile.IDevicesService; - $errors: IErrors; - $liveSyncCommandHelper: ILiveSyncCommandHelper; - $migrateController: IMigrateController; -} - -export function setupDebugCommand( - debugPlatform: DebugPlatform, -): IDebugCommandServices { +export function setupDebugCommand(debugPlatform: DebugPlatform) { const $devicePlatformsConstants = inject( "devicePlatformsConstants", ); @@ -88,6 +73,8 @@ export function setupDebugCommand( }; } +export type IDebugCommandServices = ReturnType; + export async function canExecuteDebugCommand( context: DebugCommandContext, services: IDebugCommandServices, @@ -213,13 +200,8 @@ export async function runDebugCommand( } } -interface IDebugApplePlatformCommandServices extends IDebugCommandServices { - $sysInfo: ISysInfo; -} - const setupDebugApplePlatformCommand = - (debugPlatform: "iOS" | "visionOS") => - (): IDebugApplePlatformCommandServices => { + (debugPlatform: "iOS" | "visionOS") => () => { const services = { ...setupDebugCommand(debugPlatform), $sysInfo: inject("sysInfo"), @@ -238,6 +220,10 @@ const setupDebugApplePlatformCommand = return services; }; +type IDebugApplePlatformCommandServices = ReturnType< + ReturnType +>; + function isValidTimeoutOption(timeout: string): boolean { if (!timeout) { return true; diff --git a/lib/commands/embedding/embed.ts b/lib/commands/embedding/embed.ts index d4489ddfaa..e0a245a0c3 100644 --- a/lib/commands/embedding/embed.ts +++ b/lib/commands/embedding/embed.ts @@ -7,19 +7,11 @@ import { IProjectConfigService } from "../../definitions/project"; import { platformArgument } from "../command-base"; import { canExecutePrepareCommand, - IPrepareCommandServices, prepareCommandOptions, runPrepareCommand, setupPrepareCommand, } from "../prepare"; -interface IEmbedCommandServices extends IPrepareCommandServices { - $fs: IFileSystem; - $logger: ILogger; - hostProjectPath: string; - hostProjectModuleName: string; -} - function resolveHostProjectPath( projectDir: string, hostProjectPath: string, @@ -41,14 +33,14 @@ export const embedCommandDefinition = defineCommand({ { name: "hostProjectPath" }, { name: "hostProjectModuleName" }, ], - setup(context): IEmbedCommandServices { + setup(context) { const services = setupPrepareCommand(); const $projectConfigService = inject( "projectConfigService", ); const platform = (context.args[0] || "").toLowerCase(); // embed.., falling back to embed. - const configValue = (key: string) => + const configValue = (key: string): string => $projectConfigService.getValue( `embed.${platform}.${key}`, $projectConfigService.getValue(`embed.${key}`), diff --git a/lib/commands/extensibility/install-extension.ts b/lib/commands/extensibility/install-extension.ts index d69eca8202..6133e4e5de 100644 --- a/lib/commands/extensibility/install-extension.ts +++ b/lib/commands/extensibility/install-extension.ts @@ -2,12 +2,7 @@ import { defineCommand } from "../../common/define-command"; import { inject } from "../../common/di"; import { IExtensibilityService } from "../../common/definitions/extensibility"; -export interface IInstallExtensionCommandServices { - $extensibilityService: IExtensibilityService; - $logger: ILogger; -} - -export function setupInstallExtensionCommand(): IInstallExtensionCommandServices { +export function setupInstallExtensionCommand() { return { $extensibilityService: inject( "extensibilityService", @@ -16,6 +11,10 @@ export function setupInstallExtensionCommand(): IInstallExtensionCommandServices }; } +export type IInstallExtensionCommandServices = ReturnType< + typeof setupInstallExtensionCommand +>; + export const installExtensionCommandDefinition = defineCommand({ name: "extension|install", description: "Installs the specified extension.", diff --git a/lib/commands/extensibility/list-extensions.ts b/lib/commands/extensibility/list-extensions.ts index 1ae4fb8383..9dec66c7c1 100644 --- a/lib/commands/extensibility/list-extensions.ts +++ b/lib/commands/extensibility/list-extensions.ts @@ -4,12 +4,7 @@ import { inject } from "../../common/di"; import { IExtensibilityService } from "../../common/definitions/extensibility"; import * as helpers from "../../common/helpers"; -export interface IListExtensionsCommandServices { - $extensibilityService: IExtensibilityService; - $logger: ILogger; -} - -export function setupListExtensionsCommand(): IListExtensionsCommandServices { +export function setupListExtensionsCommand() { return { $extensibilityService: inject( "extensibilityService", @@ -18,6 +13,10 @@ export function setupListExtensionsCommand(): IListExtensionsCommandServices { }; } +export type IListExtensionsCommandServices = ReturnType< + typeof setupListExtensionsCommand +>; + export const listExtensionsCommandDefinition = defineCommand({ name: "extension|*list", description: "Lists all installed extensions.", diff --git a/lib/commands/extensibility/uninstall-extension.ts b/lib/commands/extensibility/uninstall-extension.ts index 44b26b915e..1701865848 100644 --- a/lib/commands/extensibility/uninstall-extension.ts +++ b/lib/commands/extensibility/uninstall-extension.ts @@ -2,12 +2,7 @@ import { defineCommand } from "../../common/define-command"; import { inject } from "../../common/di"; import { IExtensibilityService } from "../../common/definitions/extensibility"; -export interface IUninstallExtensionCommandServices { - $extensibilityService: IExtensibilityService; - $logger: ILogger; -} - -export function setupUninstallExtensionCommand(): IUninstallExtensionCommandServices { +export function setupUninstallExtensionCommand() { return { $extensibilityService: inject( "extensibilityService", @@ -16,6 +11,10 @@ export function setupUninstallExtensionCommand(): IUninstallExtensionCommandServ }; } +export type IUninstallExtensionCommandServices = ReturnType< + typeof setupUninstallExtensionCommand +>; + export const uninstallExtensionCommandDefinition = defineCommand({ name: "extension|uninstall", description: "Uninstalls the specified extension.", diff --git a/lib/commands/fonts.ts b/lib/commands/fonts.ts index 7d4a13e4ba..edfed4dc55 100644 --- a/lib/commands/fonts.ts +++ b/lib/commands/fonts.ts @@ -7,14 +7,7 @@ import * as fontFinder from "font-finder"; import { createTable } from "../common/helpers"; import * as path from "path"; -export interface IFontsCommandServices { - $projectData: IProjectData; - $fs: IFileSystem; - $logger: ILogger; - $projectConfigService: IProjectConfigService; -} - -export function setupFontsCommand(): IFontsCommandServices { +export function setupFontsCommand() { const services = { $projectData: inject("projectData"), $fs: inject("fs"), @@ -28,6 +21,8 @@ export function setupFontsCommand(): IFontsCommandServices { return services; } +export type IFontsCommandServices = ReturnType; + export const fontsCommandDefinition = defineCommand({ name: "fonts", description: "Lists the custom fonts the project bundles.", diff --git a/lib/commands/generate-assets.ts b/lib/commands/generate-assets.ts index a299ab1e4f..559f5911db 100644 --- a/lib/commands/generate-assets.ts +++ b/lib/commands/generate-assets.ts @@ -34,15 +34,7 @@ export type GenerateAssetsCommandContext = CommandContext< typeof generateAssetsCommandOptions >; -export interface IGenerateAssetsCommandServices { - assets: GeneratedAssets; - $assetsGenerationService: IAssetsGenerationService; - $projectData: IProjectData; -} - -export function setupGenerateAssetsCommand( - assets: GeneratedAssets, -): IGenerateAssetsCommandServices { +export function setupGenerateAssetsCommand(assets: GeneratedAssets) { const services = { assets, $assetsGenerationService: inject( @@ -55,6 +47,10 @@ export function setupGenerateAssetsCommand( return services; } +export type IGenerateAssetsCommandServices = ReturnType< + typeof setupGenerateAssetsCommand +>; + export function runGenerateAssetsCommand( context: GenerateAssetsCommandContext, services: IGenerateAssetsCommandServices, diff --git a/lib/commands/hooks/common.ts b/lib/commands/hooks/common.ts index a51bf8dac1..8cfef75424 100644 --- a/lib/commands/hooks/common.ts +++ b/lib/commands/hooks/common.ts @@ -16,16 +16,8 @@ export interface OutputPlugin { hooks: OutputHook[]; } -export interface IHooksCommandServices { - $pluginsService: IPluginsService; - $projectData: IProjectData; - $errors: IErrors; - $fs: IFileSystem; - $logger: ILogger; -} - /** Callable from `setup` and from `canExecute` before their first `await`. */ -export function injectHooksCommandServices(): IHooksCommandServices { +export function injectHooksCommandServices() { const services = { $pluginsService: inject("pluginsService"), $projectData: inject("projectData"), @@ -38,6 +30,10 @@ export function injectHooksCommandServices(): IHooksCommandServices { return services; } +export type IHooksCommandServices = ReturnType< + typeof injectHooksCommandServices +>; + export function getPluginsWithHooks(plugins: IPluginData[]): IPluginData[] { const pluginsWithHooks: IPluginData[] = []; for (const plugin of plugins) { diff --git a/lib/commands/install.ts b/lib/commands/install.ts index 788a3e00b1..eacd029b4d 100644 --- a/lib/commands/install.ts +++ b/lib/commands/install.ts @@ -29,20 +29,7 @@ export type InstallCommandContext = CommandContext< typeof installCommandOptions >; -export interface IInstallCommandServices { - $options: IOptions; - $mobileHelper: Mobile.IMobileHelper; - $platformsDataService: IPlatformsDataService; - $platformCommandHelper: IPlatformCommandHelper; - $projectData: IProjectData; - $projectDataService: IProjectDataService; - $pluginsService: IPluginsService; - $logger: ILogger; - $fs: IFileSystem; - $packageManager: INodePackageManager; -} - -export function setupInstallCommand(): IInstallCommandServices { +export function setupInstallCommand() { const services = { $options: inject("options"), $mobileHelper: inject("mobileHelper"), @@ -64,6 +51,8 @@ export function setupInstallCommand(): IInstallCommandServices { return services; } +export type IInstallCommandServices = ReturnType; + async function installProjectDependencies( context: InstallCommandContext, services: IInstallCommandServices, diff --git a/lib/commands/list-platforms.ts b/lib/commands/list-platforms.ts index 2250ddb5bd..789aa25b0b 100644 --- a/lib/commands/list-platforms.ts +++ b/lib/commands/list-platforms.ts @@ -4,13 +4,7 @@ import { IPlatformCommandHelper } from "../declarations"; import { defineCommand } from "../common/define-command"; import { inject } from "../common/di"; -export interface IListPlatformsCommandServices { - $platformCommandHelper: IPlatformCommandHelper; - $projectData: IProjectData; - $logger: ILogger; -} - -export function setupListPlatformsCommand(): IListPlatformsCommandServices { +export function setupListPlatformsCommand() { const services = { $platformCommandHelper: inject( "platformCommandHelper", @@ -23,6 +17,10 @@ export function setupListPlatformsCommand(): IListPlatformsCommandServices { return services; } +export type IListPlatformsCommandServices = ReturnType< + typeof setupListPlatformsCommand +>; + export const listPlatformsCommandDefinition = defineCommand({ name: "platform|*list", description: "Lists all platforms that the project currently targets.", diff --git a/lib/commands/migrate.ts b/lib/commands/migrate.ts index 909511e397..e3345fcd46 100644 --- a/lib/commands/migrate.ts +++ b/lib/commands/migrate.ts @@ -3,15 +3,7 @@ import { IMigrateController, IMigrationData } from "../definitions/migrate"; import { defineCommand } from "../common/define-command"; import { inject } from "../common/di"; -export interface IMigrateCommandServices { - $devicePlatformsConstants: Mobile.IDevicePlatformsConstants; - $migrateController: IMigrateController; - $staticConfig: Config.IStaticConfig; - $projectData: IProjectData; - $logger: ILogger; -} - -export function setupMigrateCommand(): IMigrateCommandServices { +export function setupMigrateCommand() { const services = { $devicePlatformsConstants: inject( "devicePlatformsConstants", @@ -26,6 +18,8 @@ export function setupMigrateCommand(): IMigrateCommandServices { return services; } +export type IMigrateCommandServices = ReturnType; + export const migrateCommandDefinition = defineCommand({ name: "migrate", description: diff --git a/lib/commands/native-add.ts b/lib/commands/native-add.ts index 879c169ecb..3567ed31b4 100644 --- a/lib/commands/native-add.ts +++ b/lib/commands/native-add.ts @@ -14,17 +14,11 @@ import { IProjectData } from "../definitions/project"; */ type NativeAddLanguage = "java" | "kotlin" | "swift" | "objective-c"; -export interface INativeAddCommandServices { - $projectData: IProjectData; - $logger: ILogger; - $errors: IErrors; -} - interface INativeAddLanguageCommandServices extends INativeAddCommandServices { language: NativeAddLanguage; } -export function setupNativeAddCommand(): INativeAddCommandServices { +export function setupNativeAddCommand() { const services = { $projectData: inject("projectData"), $logger: inject("logger"), @@ -35,6 +29,10 @@ export function setupNativeAddCommand(): INativeAddCommandServices { return services; } +export type INativeAddCommandServices = ReturnType< + typeof setupNativeAddCommand +>; + function failWithUsage(services: INativeAddCommandServices): void { services.$errors.failWithHelp( "Usage: ns native add [swift|objective-c|java|kotlin] [class name]", diff --git a/lib/commands/open.ts b/lib/commands/open.ts index fe57eabf14..1bf8696964 100644 --- a/lib/commands/open.ts +++ b/lib/commands/open.ts @@ -14,23 +14,7 @@ import { IOptions } from "../declarations"; import { IProjectData } from "../definitions/project"; import type { IOSProjectService } from "../services/ios-project-service"; -export interface IOpenXcodeProjectServices { - $iOSProjectService: IOSProjectService; - $logger: ILogger; - $childProcess: IChildProcess; - $projectData: IProjectData; - $xcodeSelectService: IXcodeSelectService; - $xcodebuildArgsService: IXcodebuildArgsService; -} - -export interface IOpenAndroidStudioServices { - $logger: ILogger; - $liveSyncCommandHelper: ILiveSyncCommandHelper; - $childProcess: IChildProcess; - $projectData: IProjectData; -} - -export function injectOpenXcodeProjectServices(): IOpenXcodeProjectServices { +export function injectOpenXcodeProjectServices() { return { $iOSProjectService: inject("iOSProjectService"), $logger: inject("logger"), @@ -43,7 +27,11 @@ export function injectOpenXcodeProjectServices(): IOpenXcodeProjectServices { }; } -export function injectOpenAndroidStudioServices(): IOpenAndroidStudioServices { +export type IOpenXcodeProjectServices = ReturnType< + typeof injectOpenXcodeProjectServices +>; + +export function injectOpenAndroidStudioServices() { return { $logger: inject("logger"), $liveSyncCommandHelper: inject( @@ -54,6 +42,10 @@ export function injectOpenAndroidStudioServices(): IOpenAndroidStudioServices { }; } +export type IOpenAndroidStudioServices = ReturnType< + typeof injectOpenAndroidStudioServices +>; + export function getAndroidStudioPath(): string | null { const os = currentPlatform(); diff --git a/lib/commands/platform-clean.ts b/lib/commands/platform-clean.ts index 887dc4bdd1..521985c7ce 100644 --- a/lib/commands/platform-clean.ts +++ b/lib/commands/platform-clean.ts @@ -23,16 +23,7 @@ export type PlatformCleanCommandContext = CommandContext< typeof platformCleanCommandOptions >; -export interface IPlatformCleanCommandServices { - $errors: IErrors; - $options: IOptions; - $platformCommandHelper: IPlatformCommandHelper; - $platformValidationService: IPlatformValidationService; - $platformEnvironmentRequirements: IPlatformEnvironmentRequirements; - $projectData: IProjectData; -} - -export function setupPlatformCleanCommand(): IPlatformCleanCommandServices { +export function setupPlatformCleanCommand() { const services = { $errors: inject("errors"), $options: inject("options"), @@ -52,6 +43,10 @@ export function setupPlatformCleanCommand(): IPlatformCleanCommandServices { return services; } +export type IPlatformCleanCommandServices = ReturnType< + typeof setupPlatformCleanCommand +>; + export async function canExecutePlatformCleanCommand( context: PlatformCleanCommandContext, services: IPlatformCleanCommandServices, diff --git a/lib/commands/plugin/add-plugin.ts b/lib/commands/plugin/add-plugin.ts index fda3acf033..66287c59d7 100644 --- a/lib/commands/plugin/add-plugin.ts +++ b/lib/commands/plugin/add-plugin.ts @@ -5,13 +5,7 @@ import { IErrors } from "../../common/declarations"; import { CommandContext, defineCommand } from "../../common/define-command"; import { inject } from "../../common/di"; -export interface IAddPluginCommandServices { - $pluginsService: IPluginsService; - $projectData: IProjectData; - $errors: IErrors; -} - -export function setupAddPluginCommand(): IAddPluginCommandServices { +export function setupAddPluginCommand() { const services = { $pluginsService: inject("pluginsService"), $projectData: inject("projectData"), @@ -22,6 +16,10 @@ export function setupAddPluginCommand(): IAddPluginCommandServices { return services; } +export type IAddPluginCommandServices = ReturnType< + typeof setupAddPluginCommand +>; + export async function canExecuteAddPluginCommand( context: CommandContext, services: IAddPluginCommandServices, diff --git a/lib/commands/plugin/build-plugin.ts b/lib/commands/plugin/build-plugin.ts index 0e5f94c9d1..56eb6327b8 100644 --- a/lib/commands/plugin/build-plugin.ts +++ b/lib/commands/plugin/build-plugin.ts @@ -25,18 +25,7 @@ export type BuildPluginCommandContext = CommandContext< typeof buildPluginCommandOptions >; -export interface IBuildPluginCommandServices { - pluginProjectPath: string; - $androidPluginBuildService: IAndroidPluginBuildService; - $errors: IErrors; - $logger: ILogger; - $fs: IFileSystem; - $tempService: ITempService; -} - -export function setupBuildPluginCommand( - context: BuildPluginCommandContext, -): IBuildPluginCommandServices { +export function setupBuildPluginCommand(context: BuildPluginCommandContext) { return { pluginProjectPath: path.resolve(context.options.path || "."), $androidPluginBuildService: inject( @@ -49,6 +38,10 @@ export function setupBuildPluginCommand( }; } +export type IBuildPluginCommandServices = ReturnType< + typeof setupBuildPluginCommand +>; + export async function canExecuteBuildPluginCommand( context: BuildPluginCommandContext, services: IBuildPluginCommandServices, diff --git a/lib/commands/plugin/create-plugin.ts b/lib/commands/plugin/create-plugin.ts index 71bb45340a..d164ab3cc2 100644 --- a/lib/commands/plugin/create-plugin.ts +++ b/lib/commands/plugin/create-plugin.ts @@ -35,18 +35,7 @@ export type CreatePluginCommandContext = CommandContext< typeof createPluginCommandOptions >; -export interface ICreatePluginCommandServices { - $errors: IErrors; - $terminalSpinnerService: ITerminalSpinnerService; - $logger: ILogger; - $pacoteService: IPacoteService; - $fs: IFileSystem; - $childProcess: IChildProcess; - $prompter: IPrompter; - $packageManager: INodePackageManager; -} - -export function setupCreatePluginCommand(): ICreatePluginCommandServices { +export function setupCreatePluginCommand() { return { $errors: inject("errors"), $terminalSpinnerService: inject( @@ -61,6 +50,10 @@ export function setupCreatePluginCommand(): ICreatePluginCommandServices { }; } +export type ICreatePluginCommandServices = ReturnType< + typeof setupCreatePluginCommand +>; + function ensurePackageDir( services: ICreatePluginCommandServices, projectDir: string, diff --git a/lib/commands/plugin/list-plugins.ts b/lib/commands/plugin/list-plugins.ts index 771a3f595e..cd589964ed 100644 --- a/lib/commands/plugin/list-plugins.ts +++ b/lib/commands/plugin/list-plugins.ts @@ -9,13 +9,7 @@ import { defineCommand } from "../../common/define-command"; import { inject } from "../../common/di"; import { color } from "../../color"; -export interface IListPluginsCommandServices { - $pluginsService: IPluginsService; - $projectData: IProjectData; - $logger: ILogger; -} - -export function setupListPluginsCommand(): IListPluginsCommandServices { +export function setupListPluginsCommand() { const services = { $pluginsService: inject("pluginsService"), $projectData: inject("projectData"), @@ -26,6 +20,10 @@ export function setupListPluginsCommand(): IListPluginsCommandServices { return services; } +export type IListPluginsCommandServices = ReturnType< + typeof setupListPluginsCommand +>; + function createTableCells(items: IBasePluginData[]): string[][] { return items.map((item) => [item.name, item.version]); } diff --git a/lib/commands/plugin/remove-plugin.ts b/lib/commands/plugin/remove-plugin.ts index 4feb50305b..8cb0683c8e 100644 --- a/lib/commands/plugin/remove-plugin.ts +++ b/lib/commands/plugin/remove-plugin.ts @@ -5,14 +5,7 @@ import { IErrors } from "../../common/declarations"; import { CommandContext, defineCommand } from "../../common/define-command"; import { inject } from "../../common/di"; -export interface IRemovePluginCommandServices { - $pluginsService: IPluginsService; - $errors: IErrors; - $logger: ILogger; - $projectData: IProjectData; -} - -export function setupRemovePluginCommand(): IRemovePluginCommandServices { +export function setupRemovePluginCommand() { const services = { $pluginsService: inject("pluginsService"), $errors: inject("errors"), @@ -24,6 +17,10 @@ export function setupRemovePluginCommand(): IRemovePluginCommandServices { return services; } +export type IRemovePluginCommandServices = ReturnType< + typeof setupRemovePluginCommand +>; + export async function canExecuteRemovePluginCommand( context: CommandContext, services: IRemovePluginCommandServices, diff --git a/lib/commands/plugin/update-plugin.ts b/lib/commands/plugin/update-plugin.ts index 1180850f2f..c2672960bb 100644 --- a/lib/commands/plugin/update-plugin.ts +++ b/lib/commands/plugin/update-plugin.ts @@ -5,13 +5,7 @@ import { IErrors } from "../../common/declarations"; import { CommandContext, defineCommand } from "../../common/define-command"; import { inject } from "../../common/di"; -export interface IUpdatePluginCommandServices { - $pluginsService: IPluginsService; - $projectData: IProjectData; - $errors: IErrors; -} - -export function setupUpdatePluginCommand(): IUpdatePluginCommandServices { +export function setupUpdatePluginCommand() { const services = { $pluginsService: inject("pluginsService"), $projectData: inject("projectData"), @@ -22,6 +16,10 @@ export function setupUpdatePluginCommand(): IUpdatePluginCommandServices { return services; } +export type IUpdatePluginCommandServices = ReturnType< + typeof setupUpdatePluginCommand +>; + export async function canExecuteUpdatePluginCommand( context: CommandContext, services: IUpdatePluginCommandServices, diff --git a/lib/commands/post-install.ts b/lib/commands/post-install.ts index bcfdab2fd4..c7cea7d480 100644 --- a/lib/commands/post-install.ts +++ b/lib/commands/post-install.ts @@ -10,17 +10,7 @@ import { CommandContext, defineCommand } from "../common/define-command"; import { inject } from "../common/di"; import { doesCurrentNpmCommandMatch } from "../common/helpers"; -export interface IPostInstallCliCommandServices { - $fs: IFileSystem; - $commandsService: ICommandsService; - $helpService: IHelpService; - $settingsService: ISettingsService; - $analyticsService: IAnalyticsService; - $logger: ILogger; - $hostInfo: IHostInfo; -} - -export function setupPostInstallCliCommand(): IPostInstallCliCommandServices { +export function setupPostInstallCliCommand() { return { $fs: inject("fs"), $commandsService: inject("commandsService"), @@ -32,6 +22,10 @@ export function setupPostInstallCliCommand(): IPostInstallCliCommandServices { }; } +export type IPostInstallCliCommandServices = ReturnType< + typeof setupPostInstallCliCommand +>; + export async function runPostInstallCliCommand( context: CommandContext, services: IPostInstallCliCommandServices, diff --git a/lib/commands/prepare.ts b/lib/commands/prepare.ts index 18bb43e76c..a6e73d3577 100644 --- a/lib/commands/prepare.ts +++ b/lib/commands/prepare.ts @@ -1,7 +1,6 @@ import { canExecuteCommandBase, injectPlatformCommandServices, - IPlatformCommandServices, platformArgument, validatePlatformArgument, validatePlatformOptions, @@ -28,13 +27,7 @@ export type PrepareCommandContext = CommandContext< typeof prepareCommandOptions >; -export interface IPrepareCommandServices extends IPlatformCommandServices { - $prepareController: PrepareController; - $prepareDataService: PrepareDataService; - $migrateController: IMigrateController; -} - -export function setupPrepareCommand(): IPrepareCommandServices { +export function setupPrepareCommand() { const services = { ...injectPlatformCommandServices(), $prepareController: inject("prepareController"), @@ -46,6 +39,8 @@ export function setupPrepareCommand(): IPrepareCommandServices { return services; } +export type IPrepareCommandServices = ReturnType; + export async function canExecutePrepareCommand( context: PrepareCommandContext, services: IPrepareCommandServices, diff --git a/lib/commands/preview.ts b/lib/commands/preview.ts index 8e19715443..c70d667a6d 100644 --- a/lib/commands/preview.ts +++ b/lib/commands/preview.ts @@ -23,15 +23,7 @@ export type PreviewCommandContext = CommandContext< typeof previewCommandOptions >; -export interface IPreviewCommandServices { - $childProcess: IChildProcess; - $errors: IErrors; - $logger: ILogger; - $packageManager: IPackageManager; - $projectData: IProjectData; -} - -export function setupPreviewCommand(): IPreviewCommandServices { +export function setupPreviewCommand() { return { $childProcess: inject("childProcess"), $errors: inject("errors"), @@ -41,6 +33,8 @@ export function setupPreviewCommand(): IPreviewCommandServices { }; } +export type IPreviewCommandServices = ReturnType; + function getPreviewCLIPath(services: IPreviewCommandServices): string { return resolvePackagePath(PREVIEW_CLI_PACKAGE, { paths: [services.$projectData.projectDir], diff --git a/lib/commands/remove-platform.ts b/lib/commands/remove-platform.ts index 67e6805f64..f690f0455b 100644 --- a/lib/commands/remove-platform.ts +++ b/lib/commands/remove-platform.ts @@ -8,14 +8,7 @@ import { IErrors } from "../common/declarations"; import { CommandContext, defineCommand } from "../common/define-command"; import { inject } from "../common/di"; -export interface IRemovePlatformCommandServices { - $errors: IErrors; - $platformCommandHelper: IPlatformCommandHelper; - $platformValidationService: IPlatformValidationService; - $projectData: IProjectData; -} - -export function setupRemovePlatformCommand(): IRemovePlatformCommandServices { +export function setupRemovePlatformCommand() { const services = { $errors: inject("errors"), $platformCommandHelper: inject( @@ -31,6 +24,10 @@ export function setupRemovePlatformCommand(): IRemovePlatformCommandServices { return services; } +export type IRemovePlatformCommandServices = ReturnType< + typeof setupRemovePlatformCommand +>; + export async function canExecuteRemovePlatformCommand( context: CommandContext, services: IRemovePlatformCommandServices, diff --git a/lib/commands/resources/resources-update.ts b/lib/commands/resources/resources-update.ts index 0be239f3da..d958e9905f 100644 --- a/lib/commands/resources/resources-update.ts +++ b/lib/commands/resources/resources-update.ts @@ -4,13 +4,7 @@ import { IErrors } from "../../common/declarations"; import { CommandContext, defineCommand } from "../../common/define-command"; import { inject } from "../../common/di"; -export interface IResourcesUpdateCommandServices { - $projectData: IProjectData; - $errors: IErrors; - $androidResourcesMigrationService: IAndroidResourcesMigrationService; -} - -export function setupResourcesUpdateCommand(): IResourcesUpdateCommandServices { +export function setupResourcesUpdateCommand() { const services = { $projectData: inject("projectData"), $errors: inject("errors"), @@ -24,6 +18,10 @@ export function setupResourcesUpdateCommand(): IResourcesUpdateCommandServices { return services; } +export type IResourcesUpdateCommandServices = ReturnType< + typeof setupResourcesUpdateCommand +>; + export async function canExecuteResourcesUpdateCommand( context: CommandContext, services: IResourcesUpdateCommandServices, diff --git a/lib/commands/test-init.ts b/lib/commands/test-init.ts index 0db996dc5f..224e9e6e8f 100644 --- a/lib/commands/test-init.ts +++ b/lib/commands/test-init.ts @@ -31,20 +31,7 @@ const testInitCommandOptions = { framework: stringOption(), } satisfies CommandOptionsSchema; -interface ITestInitCommandServices { - $errors: IErrors; - $fs: IFileSystem; - $logger: ILogger; - $options: IOptions; - $packageManager: INodePackageManager; - $pluginsService: IPluginsService; - $projectData: IProjectData; - $prompter: IPrompter; - $resources: IResourceLoader; - $testInitializationService: ITestInitializationService; -} - -function setupTestInitCommand(): ITestInitCommandServices { +function setupTestInitCommand() { const services = { $errors: inject("errors"), $fs: inject("fs"), @@ -64,6 +51,8 @@ function setupTestInitCommand(): ITestInitCommandServices { return services; } +type ITestInitCommandServices = ReturnType; + /** * Android blocks cleartext traffic by default (API 28+), which would * reject the runner's ws:// connection to the host. Scope the exception diff --git a/lib/commands/test.ts b/lib/commands/test.ts index 49c528f48f..2715f37d98 100644 --- a/lib/commands/test.ts +++ b/lib/commands/test.ts @@ -51,25 +51,7 @@ const testCommandOptions = { export type TestCommandContext = CommandContext; -export interface ITestCommandServices { - platform: string; - $analyticsService: IAnalyticsService; - $cleanupService: ICleanupService; - $devicesService: Mobile.IDevicesService; - $errors: IErrors; - $liveSyncCommandHelper: ILiveSyncCommandHelper; - $logger: ILogger; - $migrateController: IMigrateController; - $options: IOptions; - $platformEnvironmentRequirements: IPlatformEnvironmentRequirements; - $projectData: IProjectData; - $testExecutionService: ITestExecutionService; - $vitestExecutionService: IVitestExecutionService; -} - -export function setupTestCommand( - testPlatform: TestPlatform, -): ITestCommandServices { +export function setupTestCommand(testPlatform: TestPlatform) { return { platform: testPlatform, $analyticsService: inject("analyticsService"), @@ -95,6 +77,8 @@ export function setupTestCommand( }; } +export type ITestCommandServices = ReturnType; + export async function canExecuteTestCommand( context: TestCommandContext, services: ITestCommandServices, diff --git a/lib/commands/typings.ts b/lib/commands/typings.ts index 79152d97de..d8c5d9175c 100644 --- a/lib/commands/typings.ts +++ b/lib/commands/typings.ts @@ -25,19 +25,7 @@ export type TypingsCommandContext = CommandContext< typeof typingsCommandOptions >; -export interface ITypingsCommandServices { - $childProcess: IChildProcess; - $fs: IFileSystem; - $hostInfo: IHostInfo; - $logger: ILogger; - $mobileHelper: Mobile.IMobileHelper; - $options: IOptions; - $projectData: IProjectData; - $prompter: IPrompter; - $staticConfig: IStaticConfig; -} - -export function setupTypingsCommand(): ITypingsCommandServices { +export function setupTypingsCommand() { return { $childProcess: inject("childProcess"), $fs: inject("fs"), @@ -51,6 +39,8 @@ export function setupTypingsCommand(): ITypingsCommandServices { }; } +export type ITypingsCommandServices = ReturnType; + async function resolveGradleDependencies( services: ITypingsCommandServices, target: string, diff --git a/lib/commands/update-platform.ts b/lib/commands/update-platform.ts index 85eb8e61f7..7eb4bd017b 100644 --- a/lib/commands/update-platform.ts +++ b/lib/commands/update-platform.ts @@ -13,16 +13,7 @@ import { IErrors } from "../common/declarations"; import { CommandContext, defineCommand } from "../common/define-command"; import { inject } from "../common/di"; -export interface IUpdatePlatformCommandServices { - $errors: IErrors; - $options: IOptions; - $platformEnvironmentRequirements: IPlatformEnvironmentRequirements; - $platformCommandHelper: IPlatformCommandHelper; - $platformValidationService: IPlatformValidationService; - $projectData: IProjectData; -} - -export function setupUpdatePlatformCommand(): IUpdatePlatformCommandServices { +export function setupUpdatePlatformCommand() { const services = { $errors: inject("errors"), $options: inject("options"), @@ -42,6 +33,10 @@ export function setupUpdatePlatformCommand(): IUpdatePlatformCommandServices { return services; } +export type IUpdatePlatformCommandServices = ReturnType< + typeof setupUpdatePlatformCommand +>; + export async function canExecuteUpdatePlatformCommand( context: CommandContext, services: IUpdatePlatformCommandServices, diff --git a/lib/commands/update.ts b/lib/commands/update.ts index 71524be88d..367736baca 100644 --- a/lib/commands/update.ts +++ b/lib/commands/update.ts @@ -21,17 +21,7 @@ const updateCommandOptions = { export type UpdateCommandContext = CommandContext; -export interface IUpdateCommandServices { - $devicePlatformsConstants: Mobile.IDevicePlatformsConstants; - $updateController: IUpdateController; - $migrateController: IMigrateController; - $errors: IErrors; - $logger: ILogger; - $projectData: IProjectData; - $markingModeService: IMarkingModeService; -} - -export function setupUpdateCommand(): IUpdateCommandServices { +export function setupUpdateCommand() { const services = { $devicePlatformsConstants: inject( "devicePlatformsConstants", @@ -48,6 +38,8 @@ export function setupUpdateCommand(): IUpdateCommandServices { return services; } +export type IUpdateCommandServices = ReturnType; + export async function canExecuteUpdateCommand( context: UpdateCommandContext, services: IUpdateCommandServices, diff --git a/lib/common/commands/analytics.ts b/lib/common/commands/analytics.ts index 36e39f76b8..3fee2c4041 100644 --- a/lib/common/commands/analytics.ts +++ b/lib/common/commands/analytics.ts @@ -26,16 +26,7 @@ export type AnalyticsCommandContext = CommandContext< typeof analyticsCommandOptions >; -export interface IAnalyticsCommandServices { - settingName: string; - humanReadableSettingName: string; - $analyticsService: IAnalyticsService; - $logger: ILogger; -} - -export function setupAnalyticsCommand( - setting: IAnalyticsSetting, -): IAnalyticsCommandServices { +export function setupAnalyticsCommand(setting: IAnalyticsSetting) { const $staticConfig = inject("staticConfig"); return { @@ -46,6 +37,10 @@ export function setupAnalyticsCommand( }; } +export type IAnalyticsCommandServices = ReturnType< + typeof setupAnalyticsCommand +>; + export function validateAnalyticsState(value: string): boolean | string { switch ((value || "").toLowerCase()) { case "enable": diff --git a/lib/common/commands/autocompletion.ts b/lib/common/commands/autocompletion.ts index 30d6ebed5f..774b75be67 100644 --- a/lib/common/commands/autocompletion.ts +++ b/lib/common/commands/autocompletion.ts @@ -3,12 +3,7 @@ import { IAutoCompletionService } from "../declarations"; import { defineCommand } from "../define-command"; import { inject } from "../di"; -export interface IAutoCompleteCommandServices { - $autoCompletionService: IAutoCompletionService; - $logger: ILogger; -} - -export function injectAutoCompleteCommandServices(): IAutoCompleteCommandServices { +export function injectAutoCompleteCommandServices() { return { $autoCompletionService: inject( "autoCompletionService", @@ -17,6 +12,10 @@ export function injectAutoCompleteCommandServices(): IAutoCompleteCommandService }; } +export type IAutoCompleteCommandServices = ReturnType< + typeof injectAutoCompleteCommandServices +>; + export const autoCompleteCommandDefinition = defineCommand({ name: "autocomplete|*default", description: "Prompts to enable command-line completion for the CLI.", diff --git a/lib/common/commands/device/device-log-stream.ts b/lib/common/commands/device/device-log-stream.ts index 8c0dcda6f6..ccc7f7ff12 100644 --- a/lib/common/commands/device/device-log-stream.ts +++ b/lib/common/commands/device/device-log-stream.ts @@ -19,15 +19,7 @@ export type OpenDeviceLogStreamCommandContext = CommandContext< typeof openDeviceLogStreamCommandOptions >; -export interface IOpenDeviceLogStreamCommandServices { - $commandsService: ICommandsService; - $deviceLogProvider: Mobile.IDeviceLogProvider; - $devicesService: Mobile.IDevicesService; - $errors: IErrors; - $loggingLevels: Mobile.ILoggingLevels; -} - -export function setupOpenDeviceLogStreamCommand(): IOpenDeviceLogStreamCommandServices { +export function setupOpenDeviceLogStreamCommand() { // The log stream is the command's whole output, so neither the simulator log // provider nor the cleanup process may be torn down while it is open. The // legacy command did this from its constructor, which ran before anything @@ -46,6 +38,10 @@ export function setupOpenDeviceLogStreamCommand(): IOpenDeviceLogStreamCommandSe }; } +export type IOpenDeviceLogStreamCommandServices = ReturnType< + typeof setupOpenDeviceLogStreamCommand +>; + export async function runOpenDeviceLogStreamCommand( context: OpenDeviceLogStreamCommandContext, services: IOpenDeviceLogStreamCommandServices, diff --git a/lib/common/commands/device/get-file.ts b/lib/common/commands/device/get-file.ts index a005cd5361..7265fe5f71 100644 --- a/lib/common/commands/device/get-file.ts +++ b/lib/common/commands/device/get-file.ts @@ -17,13 +17,7 @@ export type GetFileCommandContext = CommandContext< typeof getFileCommandOptions >; -export interface IGetFileCommandServices { - $devicesService: Mobile.IDevicesService; - $errors: IErrors; - $projectData: IProjectData; -} - -export function setupGetFileCommand(): IGetFileCommandServices { +export function setupGetFileCommand() { return { $devicesService: inject("devicesService"), $errors: inject("errors"), @@ -31,6 +25,8 @@ export function setupGetFileCommand(): IGetFileCommandServices { }; } +export type IGetFileCommandServices = ReturnType; + export async function runGetFileCommand( context: GetFileCommandContext, services: IGetFileCommandServices, diff --git a/lib/common/commands/device/list-applications.ts b/lib/common/commands/device/list-applications.ts index b53f6af372..d67a9fc058 100644 --- a/lib/common/commands/device/list-applications.ts +++ b/lib/common/commands/device/list-applications.ts @@ -17,18 +17,17 @@ export type ListApplicationsCommandContext = CommandContext< typeof listApplicationsCommandOptions >; -export interface IListApplicationsCommandServices { - $devicesService: Mobile.IDevicesService; - $logger: ILogger; -} - -export function setupListApplicationsCommand(): IListApplicationsCommandServices { +export function setupListApplicationsCommand() { return { $devicesService: inject("devicesService"), $logger: inject("logger"), }; } +export type IListApplicationsCommandServices = ReturnType< + typeof setupListApplicationsCommand +>; + export async function runListApplicationsCommand( context: ListApplicationsCommandContext, services: IListApplicationsCommandServices, diff --git a/lib/common/commands/device/list-devices.ts b/lib/common/commands/device/list-devices.ts index 3564288a9e..8e18771662 100644 --- a/lib/common/commands/device/list-devices.ts +++ b/lib/common/commands/device/list-devices.ts @@ -20,15 +20,7 @@ export type ListDevicesCommandContext = CommandContext< typeof listDevicesCommandOptions >; -export interface IListDevicesCommandServices { - $devicesService: Mobile.IDevicesService; - $emulatorHelper: Mobile.IEmulatorHelper; - $errors: IErrors; - $logger: ILogger; - $mobileHelper: Mobile.IMobileHelper; -} - -export function setupListDevicesCommand(): IListDevicesCommandServices { +export function setupListDevicesCommand() { return { $devicesService: inject("devicesService"), $emulatorHelper: inject("emulatorHelper"), @@ -38,6 +30,10 @@ export function setupListDevicesCommand(): IListDevicesCommandServices { }; } +export type IListDevicesCommandServices = ReturnType< + typeof setupListDevicesCommand +>; + function printEmulators( services: IListDevicesCommandServices, emulators: Mobile.IDeviceInfo[], @@ -175,10 +171,6 @@ export const listDevicesCommandDefinition = defineCommand({ }, }); -interface IListPlatformDevicesCommandServices extends IListDevicesCommandServices { - platform: string; -} - const defineListPlatformDevicesCommand = ( name: TName, listedPlatform: "iOS" | "Android", @@ -188,7 +180,7 @@ const defineListPlatformDevicesCommand = ( description: "Lists the connected devices and emulators for one platform.", options: listDevicesCommandOptions, arguments: "none", - setup(): IListPlatformDevicesCommandServices { + setup() { const $devicePlatformsConstants = inject("devicePlatformsConstants"); diff --git a/lib/common/commands/device/list-files.ts b/lib/common/commands/device/list-files.ts index 29b02f3097..37831a6cca 100644 --- a/lib/common/commands/device/list-files.ts +++ b/lib/common/commands/device/list-files.ts @@ -16,13 +16,7 @@ export type ListFilesCommandContext = CommandContext< typeof listFilesCommandOptions >; -export interface IListFilesCommandServices { - $devicesService: Mobile.IDevicesService; - $errors: IErrors; - $projectData: IProjectData; -} - -export function setupListFilesCommand(): IListFilesCommandServices { +export function setupListFilesCommand() { return { $devicesService: inject("devicesService"), $errors: inject("errors"), @@ -30,6 +24,10 @@ export function setupListFilesCommand(): IListFilesCommandServices { }; } +export type IListFilesCommandServices = ReturnType< + typeof setupListFilesCommand +>; + export async function runListFilesCommand( context: ListFilesCommandContext, services: IListFilesCommandServices, diff --git a/lib/common/commands/device/put-file.ts b/lib/common/commands/device/put-file.ts index b2ff383305..043f4fc86d 100644 --- a/lib/common/commands/device/put-file.ts +++ b/lib/common/commands/device/put-file.ts @@ -16,13 +16,7 @@ export type PutFileCommandContext = CommandContext< typeof putFileCommandOptions >; -export interface IPutFileCommandServices { - $devicesService: Mobile.IDevicesService; - $errors: IErrors; - $projectData: IProjectData; -} - -export function setupPutFileCommand(): IPutFileCommandServices { +export function setupPutFileCommand() { return { $devicesService: inject("devicesService"), $errors: inject("errors"), @@ -30,6 +24,8 @@ export function setupPutFileCommand(): IPutFileCommandServices { }; } +export type IPutFileCommandServices = ReturnType; + export async function runPutFileCommand( context: PutFileCommandContext, services: IPutFileCommandServices, diff --git a/lib/common/commands/device/run-application.ts b/lib/common/commands/device/run-application.ts index 147060988f..b56f10c32a 100644 --- a/lib/common/commands/device/run-application.ts +++ b/lib/common/commands/device/run-application.ts @@ -15,13 +15,7 @@ export type RunApplicationOnDeviceCommandContext = CommandContext< typeof runApplicationOnDeviceCommandOptions >; -export interface IRunApplicationOnDeviceCommandServices { - $devicesService: Mobile.IDevicesService; - $errors: IErrors; - $staticConfig: Config.IStaticConfig; -} - -export function setupRunApplicationOnDeviceCommand(): IRunApplicationOnDeviceCommandServices { +export function setupRunApplicationOnDeviceCommand() { return { $devicesService: inject("devicesService"), $errors: inject("errors"), @@ -29,6 +23,10 @@ export function setupRunApplicationOnDeviceCommand(): IRunApplicationOnDeviceCom }; } +export type IRunApplicationOnDeviceCommandServices = ReturnType< + typeof setupRunApplicationOnDeviceCommand +>; + export async function runRunApplicationOnDeviceCommand( context: RunApplicationOnDeviceCommandContext, services: IRunApplicationOnDeviceCommandServices, diff --git a/lib/common/commands/device/stop-application.ts b/lib/common/commands/device/stop-application.ts index 7c9c7a0e48..d151c82538 100644 --- a/lib/common/commands/device/stop-application.ts +++ b/lib/common/commands/device/stop-application.ts @@ -14,16 +14,16 @@ export type StopApplicationOnDeviceCommandContext = CommandContext< typeof stopApplicationOnDeviceCommandOptions >; -export interface IStopApplicationOnDeviceCommandServices { - $devicesService: Mobile.IDevicesService; -} - -export function setupStopApplicationOnDeviceCommand(): IStopApplicationOnDeviceCommandServices { +export function setupStopApplicationOnDeviceCommand() { return { $devicesService: inject("devicesService"), }; } +export type IStopApplicationOnDeviceCommandServices = ReturnType< + typeof setupStopApplicationOnDeviceCommand +>; + export async function runStopApplicationOnDeviceCommand( context: StopApplicationOnDeviceCommandContext, services: IStopApplicationOnDeviceCommandServices, diff --git a/lib/common/commands/device/uninstall-application.ts b/lib/common/commands/device/uninstall-application.ts index b37cffaa2d..ca14a530e4 100644 --- a/lib/common/commands/device/uninstall-application.ts +++ b/lib/common/commands/device/uninstall-application.ts @@ -14,16 +14,16 @@ export type UninstallApplicationCommandContext = CommandContext< typeof uninstallApplicationCommandOptions >; -export interface IUninstallApplicationCommandServices { - $devicesService: Mobile.IDevicesService; -} - -export function setupUninstallApplicationCommand(): IUninstallApplicationCommandServices { +export function setupUninstallApplicationCommand() { return { $devicesService: inject("devicesService"), }; } +export type IUninstallApplicationCommandServices = ReturnType< + typeof setupUninstallApplicationCommand +>; + export async function runUninstallApplicationCommand( context: UninstallApplicationCommandContext, services: IUninstallApplicationCommandServices, diff --git a/lib/common/commands/doctor.ts b/lib/common/commands/doctor.ts index b780b0d9e5..1657545889 100644 --- a/lib/common/commands/doctor.ts +++ b/lib/common/commands/doctor.ts @@ -3,15 +3,7 @@ import { CommandName, defineCommand } from "../define-command"; import { inject } from "../di"; import { PlatformTypes } from "../../constants"; -export interface IDoctorCommandServices { - platform: PlatformTypes; - $doctorService: IDoctorService; - $projectHelper: IProjectHelper; -} - -export function setupDoctorCommand( - platform?: PlatformTypes, -): IDoctorCommandServices { +export function setupDoctorCommand(platform?: PlatformTypes) { return { platform, $doctorService: inject("doctorService"), @@ -19,6 +11,8 @@ export function setupDoctorCommand( }; } +export type IDoctorCommandServices = ReturnType; + const defineDoctorCommand = ( name: TName, platform?: PlatformTypes, diff --git a/lib/common/commands/help.ts b/lib/common/commands/help.ts index eba2fad259..25b430dced 100644 --- a/lib/common/commands/help.ts +++ b/lib/common/commands/help.ts @@ -15,18 +15,15 @@ export const helpCommandOptions = { export type HelpCommandContext = CommandContext; -export interface IHelpCommandServices { - $commandRegistry: CommandRegistry; - $helpService: IHelpService; -} - -export function setupHelpCommand(): IHelpCommandServices { +export function setupHelpCommand() { return { $commandRegistry: inject(CommandRegistry), $helpService: inject("helpService"), }; } +export type IHelpCommandServices = ReturnType; + export async function runHelpCommand( context: HelpCommandContext, services: IHelpCommandServices, diff --git a/lib/common/commands/package-manager-get.ts b/lib/common/commands/package-manager-get.ts index f47eabaf0d..95c8b20732 100644 --- a/lib/common/commands/package-manager-get.ts +++ b/lib/common/commands/package-manager-get.ts @@ -2,18 +2,17 @@ import { IUserSettingsService } from "../declarations"; import { defineCommand } from "../define-command"; import { inject } from "../di"; -export interface IPackageManagerGetCommandServices { - $logger: ILogger; - $userSettingsService: IUserSettingsService; -} - -export function setupPackageManagerGetCommand(): IPackageManagerGetCommandServices { +export function setupPackageManagerGetCommand() { return { $logger: inject("logger"), $userSettingsService: inject("userSettingsService"), }; } +export type IPackageManagerGetCommandServices = ReturnType< + typeof setupPackageManagerGetCommand +>; + export const packageManagerGetCommandDefinition = defineCommand({ name: "package-manager|*get", description: "Prints the value of the current package manager.", diff --git a/lib/common/commands/package-manager-set.ts b/lib/common/commands/package-manager-set.ts index d192218634..9b831a6658 100644 --- a/lib/common/commands/package-manager-set.ts +++ b/lib/common/commands/package-manager-set.ts @@ -3,13 +3,7 @@ import { IErrors, IUserSettingsService } from "../declarations"; import { defineCommand } from "../define-command"; import { inject } from "../di"; -export interface IPackageManagerSetCommandServices { - $userSettingsService: IUserSettingsService; - $errors: IErrors; - $logger: ILogger; -} - -export function setupPackageManagerSetCommand(): IPackageManagerSetCommandServices { +export function setupPackageManagerSetCommand() { return { $userSettingsService: inject("userSettingsService"), $errors: inject("errors"), @@ -17,6 +11,10 @@ export function setupPackageManagerSetCommand(): IPackageManagerSetCommandServic }; } +export type IPackageManagerSetCommandServices = ReturnType< + typeof setupPackageManagerSetCommand +>; + export const packageManagerSetCommandDefinition = defineCommand({ name: "package-manager|set", description: "Sets the package manager the CLI installs dependencies with.", diff --git a/lib/common/commands/preuninstall.ts b/lib/common/commands/preuninstall.ts index 0eaa95b4c2..f26e01636e 100644 --- a/lib/common/commands/preuninstall.ts +++ b/lib/common/commands/preuninstall.ts @@ -17,15 +17,7 @@ import { IExtensibilityService } from "../definitions/extensibility"; // disabled for now (6/24/2020) // const FEEDBACK_FORM_URL = "https://www.nativescript.org/uninstall-feedback"; -export interface IPreUninstallCommandServices { - $analyticsService: IAnalyticsService; - $extensibilityService: IExtensibilityService; - $fs: IFileSystem; - $packageInstallationManager: IPackageInstallationManager; - $settingsService: ISettingsService; -} - -export function setupPreUninstallCommand(): IPreUninstallCommandServices { +export function setupPreUninstallCommand() { return { $analyticsService: inject("analyticsService"), $extensibilityService: inject( @@ -39,6 +31,10 @@ export function setupPreUninstallCommand(): IPreUninstallCommandServices { }; } +export type IPreUninstallCommandServices = ReturnType< + typeof setupPreUninstallCommand +>; + async function handleFeedbackForm(): Promise { // disabled for now (6/24/2020) // if (isInteractive()) { diff --git a/lib/common/commands/proxy/proxy-base.ts b/lib/common/commands/proxy/proxy-base.ts index e9fbd68597..222609d044 100644 --- a/lib/common/commands/proxy/proxy-base.ts +++ b/lib/common/commands/proxy/proxy-base.ts @@ -1,13 +1,7 @@ import { IAnalyticsService, IProxyService } from "../../declarations"; import { inject } from "../../di"; -export interface IProxyCommandServices { - $analyticsService: IAnalyticsService; - $logger: ILogger; - $proxyService: IProxyService; -} - -export function injectProxyCommandServices(): IProxyCommandServices { +export function injectProxyCommandServices() { return { $analyticsService: inject("analyticsService"), $logger: inject("logger"), @@ -15,6 +9,10 @@ export function injectProxyCommandServices(): IProxyCommandServices { }; } +export type IProxyCommandServices = ReturnType< + typeof injectProxyCommandServices +>; + export async function tryTrackProxyCommandUsage( services: IProxyCommandServices, commandName: string, diff --git a/lib/common/commands/proxy/proxy-set.ts b/lib/common/commands/proxy/proxy-set.ts index fd99b24ba9..0edf26251b 100644 --- a/lib/common/commands/proxy/proxy-set.ts +++ b/lib/common/commands/proxy/proxy-set.ts @@ -17,7 +17,6 @@ import { inject } from "../../di"; import { isInteractive } from "../../helpers"; import { injectProxyCommandServices, - IProxyCommandServices, tryTrackProxyCommandUsage, } from "./proxy-base"; const { getCredentialsFromAuth } = require("proxy-lib/lib/utils"); @@ -32,14 +31,7 @@ export type ProxySetCommandContext = CommandContext< typeof proxySetCommandOptions >; -export interface IProxySetCommandServices extends IProxyCommandServices { - $errors: IErrors; - $hostInfo: IHostInfo; - $prompter: IPrompter; - $staticConfig: Config.IStaticConfig; -} - -export function setupProxySetCommand(): IProxySetCommandServices { +export function setupProxySetCommand() { return { ...injectProxyCommandServices(), $errors: inject("errors"), @@ -49,6 +41,8 @@ export function setupProxySetCommand(): IProxySetCommandServices { }; } +export type IProxySetCommandServices = ReturnType; + function isPasswordRequired(username: string, password: string): boolean { return !!(username && !password); } From c5021434ea70f6cb94823ede18b11be96e64097d Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 14 Sep 2026 17:49:14 -0300 Subject: [PATCH 02/17] feat(commands): provide the invocation context through an injection token The adapter now builds a child injector per invocation providing COMMAND_CONTEXT, and runs setup, canExecute, run, postRun and shortcuts under it. Handler signatures are unchanged; the token is the way a service or a field initializer reaches the context without threading it through. The provider reads the stage's own context, and nothing outside an invocation can resolve the token. --- lib/common/contracts/command-context.ts | 12 ++++ lib/common/contracts/index.ts | 1 + .../services/command-definition-adapter.ts | 49 ++++++++++--- test/define-command.ts | 71 +++++++++++++++++++ 4 files changed, 122 insertions(+), 11 deletions(-) create mode 100644 lib/common/contracts/command-context.ts diff --git a/lib/common/contracts/command-context.ts b/lib/common/contracts/command-context.ts new file mode 100644 index 0000000000..322f9166e2 --- /dev/null +++ b/lib/common/contracts/command-context.ts @@ -0,0 +1,12 @@ +import { InjectionToken } from "../di/injection-token"; +import type { CommandContext } from "../define-command"; + +/** + * The context of the command invocation that is running. Provided by a child + * injector the adapter builds per invocation, so it resolves inside `setup`, + * `canExecute`, `run`, `postRun` and `shortcuts` — and nowhere else. A service + * registered on the root injector never sees it. + */ +export const COMMAND_CONTEXT = new InjectionToken>( + "commandContext", +); diff --git a/lib/common/contracts/index.ts b/lib/common/contracts/index.ts index 94e37d0104..6c5d9ec26f 100644 --- a/lib/common/contracts/index.ts +++ b/lib/common/contracts/index.ts @@ -14,5 +14,6 @@ export type { DeferredCommandRejection, DeferredCommandResult, } from "./command-registry"; +export { COMMAND_CONTEXT } from "./command-context"; export { ModuleRegistry } from "./module-registry"; export { PublicApiBuilder } from "./public-api-builder"; diff --git a/lib/common/services/command-definition-adapter.ts b/lib/common/services/command-definition-adapter.ts index cfe0a4ca53..a23e02ea69 100644 --- a/lib/common/services/command-definition-adapter.ts +++ b/lib/common/services/command-definition-adapter.ts @@ -5,6 +5,7 @@ import { getCurrentInjector, runInInjectionContext } from "../di/inject"; import { Injector } from "../di/injector"; import { IDictionary, IDashedOption, IErrors } from "../declarations"; import { ICommand } from "../definitions/commands"; +import { COMMAND_CONTEXT } from "../contracts/command-context"; import { COMMAND_OWNER, CommandRegistry, @@ -335,6 +336,9 @@ export function createCommandFromDefinition< // The state of one invocation. The command object itself is cached for the // process, so nothing invocation-scoped may live outside one of these. interface Invocation { + /** The context of the stage that is running; COMMAND_CONTEXT reads it. */ + context: CommandContext; + injector: Injector; setup: Promise>; hasRun: boolean; runResult?: Awaited; @@ -342,6 +346,7 @@ export function createCommandFromDefinition< const startSetup = ( context: CommandContext, + injector: Injector, ): Promise> => // The executor runs synchronously, so setup keeps its injection context // up to its first await, while a synchronous failure - ctx.fail() is one - @@ -350,7 +355,7 @@ export function createCommandFromDefinition< resolve( definition.setup ? ( - runInInjectionContext(targetInjector, () => + runInInjectionContext(injector, () => definition.setup.call(definition, context), ) ) @@ -365,9 +370,25 @@ export function createCommandFromDefinition< let currentInvocation: Invocation = null; const beginInvocation = (context: CommandContext): Invocation => { - currentInvocation = { setup: startSetup(context), hasRun: false }; + const invocation: Invocation = { + context, + // Each entry point builds its own context object, so the token reads + // the live one rather than a snapshot: a handler that injects it gets + // the very context it was handed. + injector: targetInjector.createChild([ + { + provide: COMMAND_CONTEXT, + useFactory: () => invocation.context, + shared: false, + }, + ]), + setup: undefined, + hasRun: false, + }; + invocation.setup = startSetup(context, invocation.injector); + currentInvocation = invocation; - return currentInvocation; + return invocation; }; /** @@ -377,6 +398,7 @@ export function createCommandFromDefinition< * take the host's keys with it. */ const attachShortcuts = ( + invocation: Invocation, context: CommandContext, setupResult: Awaited, ): void => { @@ -392,8 +414,9 @@ export function createCommandFromDefinition< return; } - const shortcuts: KeyShortcut[] = runInInjectionContext(targetInjector, () => - definition.shortcuts.call(definition, context, setupResult), + const shortcuts: KeyShortcut[] = runInInjectionContext( + invocation.injector, + () => definition.shortcuts.call(definition, context, setupResult), ); if (!shortcuts || !shortcuts.length) { return; @@ -428,8 +451,9 @@ export function createCommandFromDefinition< postCommandAction: async (args: string[]): Promise => { const context = buildContext(args); const invocation = currentInvocation || beginInvocation(context); + invocation.context = context; const setupResult = await invocation.setup; - await runInInjectionContext(targetInjector, () => + await runInInjectionContext(invocation.injector, () => definition.postRun.call( definition, context, @@ -446,7 +470,8 @@ export function createCommandFromDefinition< // arguments - so an argument validator can rely on it, and a command // run in the wrong place still reports that before complaining about // arity. - const setupResult = await beginInvocation(context).setup; + const invocation = beginInvocation(context); + const setupResult = await invocation.setup; await enforceArguments(context); @@ -457,7 +482,7 @@ export function createCommandFromDefinition< // Same first-await rule as execute: runInInjectionContext is // synchronous, so inject() is available up to the first await. - return await runInInjectionContext(targetInjector, () => + return await runInInjectionContext(invocation.injector, () => refine.call(definition, context, setupResult), ); }, @@ -467,15 +492,17 @@ export function createCommandFromDefinition< currentInvocation && !currentInvocation.hasRun ? currentInvocation : beginInvocation(context); + invocation.context = context; invocation.hasRun = true; const setupResult = await invocation.setup; - invocation.runResult = await runInInjectionContext(targetInjector, () => - definition.run.call(definition, context, setupResult), + invocation.runResult = await runInInjectionContext( + invocation.injector, + () => definition.run.call(definition, context, setupResult), ); if (definition.shortcuts) { - attachShortcuts(context, setupResult); + attachShortcuts(invocation, context, setupResult); } }, }; diff --git a/test/define-command.ts b/test/define-command.ts index ae7f8698cc..7c67b10271 100644 --- a/test/define-command.ts +++ b/test/define-command.ts @@ -8,6 +8,7 @@ import { InjectionToken, runInInjectionContext, } from "../lib/common/di"; +import { COMMAND_CONTEXT } from "../lib/common/contracts/command-context"; import { COMMAND_OWNER, CommandRegistry, @@ -2275,6 +2276,76 @@ describe("defineCommand", () => { }); }); + describe("COMMAND_CONTEXT", () => { + it("resolves to the context the handlers of the same stage receive", async () => { + const testInjector = createTestInjector(); + let injectedInSetup: any; + let injectedInRun: any; + let setupContext: any; + let runContext: any; + + const command = createCommandFromDefinition( + defineCommand({ + name: "dctest-command-context", + setup: (ctx) => { + setupContext = ctx; + injectedInSetup = inject(COMMAND_CONTEXT); + }, + run: (ctx) => { + runContext = ctx; + injectedInRun = inject(COMMAND_CONTEXT); + }, + }), + testInjector, + ); + + await command.execute([]); + + assert.strictEqual(injectedInSetup, setupContext); + assert.strictEqual(injectedInRun, runContext); + }); + + it("is scoped to the invocation, so the root injector never sees it", async () => { + const testInjector = createTestInjector(); + + const command = createCommandFromDefinition( + defineCommand({ + name: "dctest-command-context-scope", + run: (): void => undefined, + }), + testInjector, + ); + + await command.execute([]); + + assert.isNull(testInjector.get(COMMAND_CONTEXT, { optional: true })); + assert.throws( + () => testInjector.get(COMMAND_CONTEXT), + /unable to resolve/, + ); + }); + + it("gives each invocation a context of its own", async () => { + const testInjector = createTestInjector(); + const seen: any[] = []; + + const command = createCommandFromDefinition( + defineCommand({ + name: "dctest-command-context-per-invocation", + setup: () => seen.push(inject(COMMAND_CONTEXT)), + run: (): void => undefined, + }), + testInjector, + ); + + await command.execute([]); + await command.execute([]); + + assert.lengthOf(seen, 2); + assert.notStrictEqual(seen[0], seen[1]); + }); + }); + describe("per-registration parameterization with a child injector", () => { it("registers one definition per platform and resolves the child provider", async () => { const PLATFORM = new InjectionToken("dcTestCommandPlatform"); From 2ffeb517930b284ba83fb1e3ab85acb1103c3518 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 14 Sep 2026 17:59:15 -0300 Subject: [PATCH 03/17] feat(commands): add a class form built on defineCommand Command(meta) returns a base class whose static definition is a real defineCommand result: the handlers become methods, the instance is the setup result, and the adapter still only ever sees definitions. The definition is a static getter, so it resolves the subclass it is read through and caches on that constructor. Registration, the name-literal check and the extension manifest path take either form. COMMAND_CONTEXT is promoted to nativescript/contracts, which is what the base class reads in its field initializer. --- defining-commands.md | 123 +++++++++ lib/common/define-command.ts | 224 +++++++++++++++- .../services/command-definition-adapter.ts | 30 ++- lib/contracts/index.ts | 11 + lib/services/extensibility-service.ts | 7 +- test/define-command.ts | 245 ++++++++++++++++++ test/type-fixtures/define-command-types.ts | 79 +++++- 7 files changed, 698 insertions(+), 21 deletions(-) diff --git a/defining-commands.md b/defining-commands.md index 2eeda4792e..57951dcf5d 100644 --- a/defining-commands.md +++ b/defining-commands.md @@ -48,6 +48,10 @@ spread, so `{ ...baseDefinition, name: "widget|add2" }` is still recognised. `defineCommand` does not register anything by itself — see [Registering a definition](#registering-a-definition). +A command may also be written as a class, with the handlers as methods — see +[Class form](#class-form). It is sugar over `defineCommand`: everything below +describes both. + Validation happens where you can see it --------------------------------------- @@ -518,6 +522,120 @@ Other flags Both are simply passed through to the command the CLI executes; omitting them leaves the CLI's defaults in place. +Class form +---------- + +`Command(meta)` returns a base class to extend. It is sugar over +`defineCommand` and nothing more: the class carries a `static definition` built +by `defineCommand`, and that definition is the only thing the CLI ever +executes. + +```ts +import { Command, inject, stringOption } from "nativescript/contracts"; + +export class PlatformCleanCommand extends Command({ + name: "platform|clean", + description: "Removes and adds again the selected platform.", + options: { frameworkPath: stringOption() }, + arguments: "any", +}) { + private $platformCommandHelper = inject( + "platformCommandHelper", + ); + private $projectData = inject("projectData"); + + constructor() { + super(); + this.$projectData.initializeProjectData(); + } + + public async run(): Promise { + await this.$platformCommandHelper.cleanPlatforms( + this.args, + this.$projectData, + this.options.frameworkPath, + ); + } +} +``` + +`meta` is the definition minus its handlers: `name`, `description`, `options`, +`arguments`, `allowUnknownOptions`, `disableAnalytics` and `enableHooks`. The +handlers are methods instead — `run` is required, and `canExecute`, `postRun` +and `shortcuts` are optional, each with the same meaning and the same ordering +as the fields of the same name. `postRun(result)` receives what `run` returned; +`shortcuts()` returns the same table `shortcuts(ctx, setup)` does. A method the +class does not declare is left out of the definition entirely, so a class +without `postRun` gets no `postCommandAction`, exactly as an object without one +does. + +**Which form to use.** The class form is for a single named command. When a +function generates variants of one command — the `run|ios` / `run|vision` +family, one definition per platform — the object form is what fits, because +the thing being parameterized is a value and definitions are values. +Registering the same class twice under two names is not the equivalent: the +class is one definition. + +**The class is the setup.** One instance is constructed per invocation, as that +invocation's `setup`, before `canExecute` runs. So field initializers and the +constructor run inside the injection context: `inject()` in a field initializer +resolves, and a constructor — optional, and if written it must call a bare +`super()` — is where the work a legacy command did in its own constructor goes. +Because construction is the setup, `inject()` is valid throughout it; after the +first `await` inside a method, use `this.context.injector.get(token)` as +[Injection, and the first `await`](#injection-and-the-first-await) describes. + +**`this.context`, `this.options` and `this.args`** are the same context the +object form's handlers receive, typed from the `options` the meta declares: +`this.options.frameworkPath` is `string | undefined` above, and a name the +schema does not declare is a compile error. `this.context` also carries +`params`, `injector` and `fail`. + +**Per-command providers see the invocation.** The context is provided to the +invocation's own child injector under the `COMMAND_CONTEXT` token, which is how +the base class reads it. A provider registered for one command — through the +`providers` argument of `registerCommand` or `registerLazyCommand` — can inject +it too, and resolves nothing outside a running invocation. + +**Share through functions, not base classes.** Two commands that need the same +services share an `inject()`-based helper, not a common ancestor: + +```ts +export function injectPlatformCommandServices() { + const projectData = inject(ProjectData); + projectData.initializeProjectData(); + return { projectData, platformHelper: inject(PlatformCommandHelper) }; +} + +export class PlatformAddCommand extends Command({ name: "platform|add" }) { + private services = injectPlatformCommandServices(); + // ... +} +``` + +A helper composes — a command can call two of them — and it stays readable +without the reader walking a chain of files. A base class between `Command()` +and the command does not: it is the pattern the legacy `ICommand` hierarchy +used, and untangling it is most of why this API exists. + +Registration takes the class itself; see +[Registering a definition](#registering-a-definition): + +```ts +registerBuiltInCommand< + typeof import("./commands/platform-clean").PlatformCleanCommand +>( + "platform|clean", + () => require("./commands/platform-clean").PlatformCleanCommand, +); +``` + +`isCommandClass(value)` is the exported check, and `Ctor.definition` is the +definition the class stands for — derived once per class, and derived for the +subclass rather than for the base `Command()` returned. A class that implements +no `run`, or a class that did not come from `Command()`, is refused with the +same message shape a bad object gets. + Registering a definition ------------------------ @@ -533,6 +651,11 @@ registerCommand({ }); ``` +Every registration helper — `registerCommand`, `registerLazyCommand` and +`registerBuiltInCommand` — takes a [class form](#class-form) command wherever +it takes a definition, and reads the name it declares through its +`static definition`. + It takes either a `DefinedCommand` — the result of `defineCommand`, marker and all — or the definition itself, which it defines on your behalf, so registering a command is one call. Either way the definition is validated before it reaches diff --git a/lib/common/define-command.ts b/lib/common/define-command.ts index 10730417ef..43031f5d70 100644 --- a/lib/common/define-command.ts +++ b/lib/common/define-command.ts @@ -6,7 +6,9 @@ * lib/common/services/command-definition-adapter. */ +import { COMMAND_CONTEXT } from "./contracts/command-context"; import type { KeyShortcut } from "./contracts/key-shortcuts"; +import { inject } from "./di/inject"; import type { Injector } from "./di/injector"; /** @@ -255,7 +257,10 @@ const OPTION_TYPES: CommandOptionType[] = [ const ACCEPTED_FORM = 'defineCommand({ name: "widget|add", run(ctx) { ... } }) — with the ' + "optional fields description, options, arguments, allowUnknownOptions, " + - "setup, canExecute, shortcuts, postRun, disableAnalytics and enableHooks."; + "setup, canExecute, shortcuts, postRun, disableAnalytics and enableHooks. " + + 'Or the class form, class WidgetAdd extends Command({ name: "widget|add" }) ' + + "{ run() { ... } }, which declares the same fields except the handlers and " + + "implements run, and optionally canExecute, postRun and shortcuts, as methods."; const describeDefinition = (definition: any): string => { const name = definition && definition.name; @@ -556,12 +561,18 @@ export type CommandName = string | readonly string[]; * be checked against them. */ export type CommandNamesOf = TDefinition extends { - name: infer TName; + definition: infer TClassDefinition; } - ? TName extends readonly (infer TAlias)[] - ? TAlias - : TName - : never; + ? // A constructor's own `name` is Function.name, so the class form has to be + // read through its static definition before the `name` branch sees it. + CommandNamesOf + : TDefinition extends { + name: infer TName; + } + ? TName extends readonly (infer TAlias)[] + ? TAlias + : TName + : never; export function defineCommand< TSchema extends CommandOptionsSchema = {}, @@ -583,3 +594,204 @@ export function isCommandDefinition( ): value is DefinedCommand { return !!value && (value)[COMMAND_DEFINITION_MARKER] === true; } + +/** + * Marks a constructor produced by `Command()`. Same `Symbol.for` reasoning as + * COMMAND_DEFINITION_MARKER, and the same reason it is read rather than + * `instanceof`: an extension bundles its own copy of this module. + */ +export const COMMAND_CLASS_MARKER: unique symbol = Symbol.for( + "nativescript:cli:commandClass", +); + +/** The meta `Command()` was called with, inherited by every subclass. */ +const COMMAND_CLASS_META = Symbol.for("nativescript:cli:commandClassMeta"); + +/** Per-constructor cache of the derived definition; own-property only. */ +const COMMAND_CLASS_DEFINITION = Symbol.for( + "nativescript:command:classDefinition", +); + +/** + * What the class form declares up front: a definition without the handlers, + * which the class supplies as methods instead. + */ +export type CommandMeta< + TName extends CommandName = CommandName, + TSchema extends CommandOptionsSchema = {}, +> = Omit< + CommandDefinition, + "name" | "setup" | "canExecute" | "run" | "postRun" | "shortcuts" +> & { name: TName }; + +/** + * The instance side of the class form. Exported because it names the base of + * every `Command()` class — a subclass's declaration emit refers to it — not + * because anything should extend it directly. + */ +export abstract class CommandBase< + TSchema extends CommandOptionsSchema = {}, + TResult = void, +> { + /** + * The instance is built once per invocation, as that invocation's `setup`, + * so the context captured here is the one its own run was handed. + */ + protected readonly context: CommandContext = inject(COMMAND_CONTEXT); + + protected get options(): CommandOptionValues { + return this.context.options; + } + + protected get args(): string[] { + return this.context.args; + } + + abstract run(): Promise | TResult; + canExecute?(): Promise | boolean; + postRun?(result: Awaited): Promise | void; + shortcuts?(): KeyShortcut[]; +} + +/** + * The static side. An abstract construct signature, so the compiler still + * requires a subclass to implement `run`, and a named type, so declaration + * emit for `class X extends Command({ ... })` has something to refer to. + */ +export type CommandClass< + TName extends CommandName = CommandName, + TSchema extends CommandOptionsSchema = {}, + TResult = void, +> = (abstract new () => CommandBase) & { + readonly definition: NamedCommand< + TSchema, + TResult, + CommandBase, + TName + >; + readonly [COMMAND_CLASS_MARKER]: true; +}; + +/** Either accepted form of a command, as a registration site takes it. */ +export type RegisterableCommand = + DefinedCommand | CommandClass; + +export function isCommandClass( + value: any, +): value is CommandClass { + return ( + typeof value === "function" && (value)[COMMAND_CLASS_MARKER] === true + ); +} + +const buildClassDefinition = (ctor: any): DefinedCommand => { + const meta = ctor[COMMAND_CLASS_META]; + const prototype = ctor.prototype; + const implementsMethod = (method: string): boolean => + typeof prototype[method] === "function"; + + if (!implementsMethod("run")) { + invalid( + meta, + `the class '${ctor.name || ""}' implements no 'run' method`, + ); + } + + // The instance IS the setup result, so every handler reaches it as the + // second argument the adapter already threads through. + const definition: any = { + ...meta, + setup: () => new ctor(), + run: (context: any, instance: any) => instance.run(), + }; + + if (implementsMethod("canExecute")) { + definition.canExecute = (context: any, instance: any) => + instance.canExecute(); + } + + if (implementsMethod("postRun")) { + definition.postRun = (context: any, result: any, instance: any) => + instance.postRun(result); + } + + if (implementsMethod("shortcuts")) { + definition.shortcuts = (context: any, instance: any) => + instance.shortcuts(); + } + + return defineCommand(definition); +}; + +/** + * The definition a `Command()` class stands for, cached on the constructor it + * was read from. The cache entry is an own property so a class extending + * another command class never serves its parent's definition. + */ +export function classCommandDefinition( + ctor: any, +): DefinedCommand { + if (!isCommandClass(ctor)) { + throw new Error( + `${describeDefinition(ctor)} is not a command class: it did not come ` + + `from Command(). Accepted form: ${ACCEPTED_FORM}`, + ); + } + + const target: any = ctor; + if (Object.prototype.hasOwnProperty.call(target, COMMAND_CLASS_DEFINITION)) { + return target[COMMAND_CLASS_DEFINITION]; + } + + const definition = buildClassDefinition(target); + Object.defineProperty(target, COMMAND_CLASS_DEFINITION, { + value: definition, + }); + + return definition; +} + +/** The definition behind either form, or null for anything else. */ +export function toCommandDefinition( + value: any, +): DefinedCommand | null { + if (isCommandClass(value)) { + return classCommandDefinition(value); + } + + return isCommandDefinition(value) ? value : null; +} + +/** + * The class authoring form: sugar over defineCommand, not a second execution + * path. The returned base carries a `definition` that reads the class it is + * accessed through, so the subclass — not this base — is what `setup` + * instantiates, and registration keeps taking definitions only. + * + * export class PlatformClean extends Command({ + * name: "platform|clean", + * options: { frameworkPath: stringOption() }, + * }) { + * private $helper = inject("platformCommandHelper"); + * run() { return this.$helper.clean(this.args, this.options.frameworkPath); } + * } + */ +export function Command< + const TName extends CommandName, + TSchema extends CommandOptionsSchema = {}, + TResult = void, +>(meta: CommandMeta): CommandClass { + abstract class Base extends CommandBase { + // A getter, because `this` in a static accessor is the constructor the + // property was read through: that is the only hook that resolves the + // subclass without the subclass having to name itself. + static get definition(): DefinedCommand { + return classCommandDefinition(this); + } + } + + Object.defineProperty(Base, COMMAND_CLASS_MARKER, { value: true }); + Object.defineProperty(Base, COMMAND_CLASS_META, { value: meta }); + + return Base; +} diff --git a/lib/common/services/command-definition-adapter.ts b/lib/common/services/command-definition-adapter.ts index a23e02ea69..f631c9c72f 100644 --- a/lib/common/services/command-definition-adapter.ts +++ b/lib/common/services/command-definition-adapter.ts @@ -23,13 +23,16 @@ import { CommandArgumentValues, CommandContext, CommandDefinition, + CommandClass, + CommandName, CommandNamesOf, CommandOptionSpec, CommandOptionType, CommandOptionsSchema, DefinedCommand, + RegisterableCommand, defineCommand, - isCommandDefinition, + toCommandDefinition, } from "../define-command"; const OPTION_TYPES: IDictionary = { @@ -564,8 +567,9 @@ export async function runCommand( } /** - * Registers a command with the CLI. Takes either the result of defineCommand() - * or the definition itself, which it defines on the caller's behalf. + * Registers a command with the CLI. Takes a Command() class, the result of + * defineCommand(), or a bare definition, which it defines on the caller's + * behalf. * * Registration targets the injector of the current injection context, and * `providers` scope the command to a child of it. To register against some @@ -583,13 +587,14 @@ export function registerCommand< TSetup = any, >( definition: + | CommandClass | DefinedCommand | CommandDefinition, providers: Provider[] = [], ): DeferredCommandResult { - const defined = isCommandDefinition(definition) - ? definition - : defineCommand(>definition); + const defined = + toCommandDefinition(definition) || + defineCommand(>definition); const target = contextInjector(); const scope = providers.length ? target.createChild(providers) : target; const owner = target.get(COMMAND_OWNER, { optional: true }) || CLI_OWNER; @@ -648,7 +653,7 @@ type MissingTypeArgument = * aborts startup instead of returning a result nobody would check. */ export function registerBuiltInCommand< - TDefinition extends DefinedCommand = never, + TDefinition extends RegisterableCommand = never, >( name: [TDefinition] extends [never] ? MissingTypeArgument @@ -669,7 +674,7 @@ export function registerBuiltInCommand< } export function registerLazyCommand< - TDefinition extends DefinedCommand = never, + TDefinition extends RegisterableCommand = never, >( name: [TDefinition] extends [never] ? MissingTypeArgument @@ -684,13 +689,16 @@ export function registerLazyCommand< return registry.registerDeferredCommand(commandName, { owner: target.get(COMMAND_OWNER, { optional: true }) || CLI_OWNER, load: () => { - const definition = load(); + const loaded = load(); // The compile-time check above is only as good as the type argument the // call site passes, so the same mismatch is caught here as well. - if (!isCommandDefinition(definition)) { + const definition = toCommandDefinition(loaded); + if (!definition) { throw new Error( - "the loader did not return a defineCommand() definition", + typeof loaded === "function" + ? "the loader returned a class that did not come from Command()" + : "the loader did not return a defineCommand() definition or a Command() class", ); } diff --git a/lib/contracts/index.ts b/lib/contracts/index.ts index 2a6137e2e7..db83a537a9 100644 --- a/lib/contracts/index.ts +++ b/lib/contracts/index.ts @@ -55,8 +55,13 @@ export { PBXPROJ_DOM_XCODE } from "./pbxproj-dom-xcode"; export { XCODE } from "./xcode"; export { + Command, + CommandBase, + COMMAND_CLASS_MARKER, defineCommand, + isCommandClass, isCommandDefinition, + toCommandDefinition, booleanOption, stringOption, numberOption, @@ -67,11 +72,14 @@ export type { ArgumentSpec, ArgumentsPolicy, CommandArgumentValues, + CommandClass, CommandDefinition, + CommandMeta, CommandName, CommandNamesOf, DefinedCommand, NamedCommand, + RegisterableCommand, CommandContext, CommandOptionSpec, DefaultedCommandOptionSpec, @@ -80,6 +88,9 @@ export type { CommandOptionType, CommandOptionValues, } from "../common/define-command"; +// Promoted from the internal contracts index: the class form reads it in a +// field initializer, and a per-command provider is written against it. +export { COMMAND_CONTEXT } from "../common/contracts/command-context"; export { defineHook, isHookDefinition } from "../common/define-hook"; export type { HookContext, diff --git a/lib/services/extensibility-service.ts b/lib/services/extensibility-service.ts index 48fc3584f6..9c2884c534 100644 --- a/lib/services/extensibility-service.ts +++ b/lib/services/extensibility-service.ts @@ -27,7 +27,7 @@ import { CommandRegistry, describeRejection, } from "../common/contracts"; -import { DefinedCommand, isCommandDefinition } from "../common/define-command"; +import { DefinedCommand, toCommandDefinition } from "../common/define-command"; import { registerDefinitionAs } from "../common/services/command-definition-adapter"; function isNonEmptyString(value: any): boolean { @@ -449,9 +449,10 @@ export class ExtensibilityService implements IExtensibilityService { const exported = this.loadInExtensionScope(extensionName, () => require(absoluteModulePath), ); - const candidate = (exported && exported.default) ?? exported; + const exportedValue = (exported && exported.default) ?? exported; - if (!isCommandDefinition(candidate)) { + const candidate = toCommandDefinition(exportedValue); + if (!candidate) { return; } diff --git a/test/define-command.ts b/test/define-command.ts index 7c67b10271..b833ec8f4a 100644 --- a/test/define-command.ts +++ b/test/define-command.ts @@ -21,13 +21,16 @@ import { LoggerStub, HooksServiceStub } from "./stubs"; import { arrayOption, booleanOption, + Command, defineCommand, + isCommandClass, isCommandDefinition, numberOption, stringOption, } from "../lib/common/define-command"; import { createCommandFromDefinition, + registerBuiltInCommand, registerCommand, registerLazyCommand, } from "../lib/common/services/command-definition-adapter"; @@ -2276,6 +2279,248 @@ describe("defineCommand", () => { }); }); + describe("class form", () => { + it("runs with the declared options and arguments", async () => { + const testInjector = createTestInjector({ release: true }); + const seen: any[] = []; + + class Widget extends Command({ + name: "dctest-class", + options: { release: booleanOption({ default: false }) }, + arguments: "any", + }) { + public run(): void { + seen.push([this.options.release, this.args, this.context.params]); + } + } + + assert.isTrue(isCommandClass(Widget)); + assert.isTrue(isCommandDefinition(Widget.definition)); + + const command = createCommandFromDefinition( + Widget.definition, + testInjector, + ); + await command.execute(["android"]); + + assert.deepEqual(seen, [[true, ["android"], {}]]); + }); + + it("derives one definition per class, not per access", () => { + class Widget extends Command({ name: "dctest-class-cached" }) { + public run(): void { + /* intentionally left blank */ + } + } + + assert.strictEqual(Widget.definition, Widget.definition); + assert.equal(Widget.definition.name, "dctest-class-cached"); + }); + + it("builds one instance per invocation, with inject() fields resolved", async () => { + const testInjector = createTestInjector(); + testInjector.register("dcTestGreeter", { greet: () => "hello" }); + const instances: any[] = []; + + class Widget extends Command({ name: "dctest-class-instances" }) { + private $greeter = inject("dcTestGreeter"); + + public run(): void { + instances.push(this); + assert.equal(this.$greeter.greet(), "hello"); + } + } + + const command = createCommandFromDefinition( + Widget.definition, + testInjector, + ); + await command.execute([]); + await command.execute([]); + + assert.lengthOf(instances, 2); + assert.notStrictEqual(instances[0], instances[1]); + assert.instanceOf(instances[0], Widget); + }); + + it("honours an optional canExecute and leaves it out when undeclared", async () => { + const testInjector = createTestInjector(); + + class Refusing extends Command({ + name: "dctest-class-refuses", + arguments: "any", + }) { + public canExecute(): boolean { + return this.args[0] === "yes"; + } + + public run(): void { + /* intentionally left blank */ + } + } + + class Plain extends Command({ name: "dctest-class-plain" }) { + public run(): void { + /* intentionally left blank */ + } + } + + assert.isUndefined(Plain.definition.canExecute); + + const refusing = createCommandFromDefinition( + Refusing.definition, + testInjector, + ); + assert.isFalse(await refusing.canExecute(["no"])); + assert.isTrue(await refusing.canExecute(["yes"])); + + const plain = createCommandFromDefinition(Plain.definition, testInjector); + assert.isTrue(await plain.canExecute([])); + }); + + it("wires postRun to the result run returned", async () => { + const testInjector = createTestInjector(); + const order: string[] = []; + + class Widget extends Command<"dctest-class-postrun", {}, number>({ + name: "dctest-class-postrun", + }) { + public run(): number { + order.push("run"); + return 7; + } + + public postRun(result: number): void { + order.push(`postRun:${result}`); + } + } + + const command = createCommandFromDefinition( + Widget.definition, + testInjector, + ); + await command.execute([]); + await command.postCommandAction([]); + + assert.deepEqual(order, ["run", "postRun:7"]); + }); + + it("wires shortcuts, and declares none when the class has no method", async () => { + const savedSetting = process.env.NS_COMMAND_SHORTCUTS; + process.env.NS_COMMAND_SHORTCUTS = "true"; + + try { + const testInjector = createTestInjector(); + const attached: string[][] = []; + testInjector.register("keyShortcutService", { + attach(options: { shortcuts: KeyShortcut[] }): boolean { + attached.push(options.shortcuts.map((shortcut) => shortcut.key)); + return true; + }, + printHint: (): void => undefined, + }); + + class Widget extends Command({ name: "dctest-class-shortcuts" }) { + public run(): void { + /* intentionally left blank */ + } + + public shortcuts(): KeyShortcut[] { + return [ + { + key: "r", + description: `Restart ${this.args[0]}`, + action: (): void => undefined, + }, + ]; + } + } + + class Plain extends Command({ name: "dctest-class-no-shortcuts" }) { + public run(): void { + /* intentionally left blank */ + } + } + + assert.isUndefined(Plain.definition.shortcuts); + + const command = createCommandFromDefinition( + Widget.definition, + testInjector, + ); + await command.execute(["ios"]); + + assert.deepEqual(attached, [["r"]]); + } finally { + if (savedSetting === undefined) { + delete process.env.NS_COMMAND_SHORTCUTS; + } else { + process.env.NS_COMMAND_SHORTCUTS = savedSetting; + } + } + }); + + it("registers through registerCommand and registerBuiltInCommand", async () => { + const testInjector = createTestInjector(); + const ran: string[] = []; + + class Direct extends Command({ name: "dctest-class-direct" }) { + public run(): void { + ran.push("direct"); + } + } + + class Lazy extends Command({ name: "dctest-class-lazy" }) { + public run(): void { + ran.push("lazy"); + } + } + + runInInjectionContext(testInjector, () => registerCommand(Direct)); + runInInjectionContext(testInjector, () => + registerBuiltInCommand( + "dctest-class-lazy", + () => Lazy, + ), + ); + + await testInjector.resolveCommand("dctest-class-direct").execute([]); + await testInjector.resolveCommand("dctest-class-lazy").execute([]); + + assert.deepEqual(ran, ["direct", "lazy"]); + }); + + it("rejects a class that implements no run", () => { + const noRun: any = Command({ name: "dctest-class-norun" }); + + assert.throws( + () => noRun.definition, + /Invalid command definition for 'dctest-class-norun'.*implements no 'run' method.*Accepted form:/s, + ); + }); + + it("reports a class Command() did not produce, through the deferred loader", () => { + const testInjector = createTestInjector(); + + class Impostor { + public run(): void { + /* intentionally left blank */ + } + } + + assert.isFalse(isCommandClass(Impostor)); + + runInInjectionContext(testInjector, () => + registerLazyCommand("dctest-class-impostor2", () => Impostor), + ); + + assert.throws( + () => testInjector.resolveCommand("dctest-class-impostor2"), + /class that did not come from Command\(\)/, + ); + }); + }); + describe("COMMAND_CONTEXT", () => { it("resolves to the context the handlers of the same stage receive", async () => { const testInjector = createTestInjector(); diff --git a/test/type-fixtures/define-command-types.ts b/test/type-fixtures/define-command-types.ts index 85fd7107d2..34cf91698a 100644 --- a/test/type-fixtures/define-command-types.ts +++ b/test/type-fixtures/define-command-types.ts @@ -9,12 +9,16 @@ import { arrayOption, booleanOption, + Command, defineCommand, numberOption, stringOption, } from "../../lib/common/define-command"; import type { CommandArgumentValues } from "../../lib/common/define-command"; -import { registerLazyCommand } from "../../lib/common/services/command-definition-adapter"; +import { + registerBuiltInCommand, + registerLazyCommand, +} from "../../lib/common/services/command-definition-adapter"; import type { Injector } from "../../lib/common/di/injector"; type IsExact = @@ -244,3 +248,76 @@ registerLazyCommand( ); declare function setupLazyCommand(): { projectDir: string }; + +// The class form types this.options, this.args and this.context off the schema +// the meta declares, exactly as the object form types ctx. +class TypefixturePlatformClean extends Command({ + name: "typefixture|class-clean", + options: { + frameworkPath: stringOption({ default: "platforms" }), + verbose: booleanOption(), + }, + arguments: "any", +}) { + run(): void { + const frameworkPath = this.options.frameworkPath; + const verbose = this.options.verbose; + const args = this.args; + const fail = this.context.fail; + + expectExactType>(); + expectExactType>(); + expectExactType>(); + expectExactType, never>>(); + + // @ts-expect-error - the schema types this.options and nothing else + this.options.undeclared; + } +} + +class TypefixtureResult extends Command<"typefixture|class-result", {}, number>( + { name: "typefixture|class-result" }, +) { + run(): number { + return 1; + } + + postRun(result: number): void { + expectExactType>(); + } +} + +// @ts-expect-error - run is abstract; a command class has to implement it +class TypefixtureNoRun extends Command({ name: "typefixture|class-no-run" }) {} + +// The static definition is what a registration site is checked against, so the +// literal name has to survive from the meta through to the call. +registerBuiltInCommand( + "typefixture|class-clean", + () => require("./commands/clean").TypefixturePlatformClean, +); + +registerBuiltInCommand( + // @ts-expect-error - the class declares 'typefixture|class-clean' + "typefixture|class-cleann", + () => require("./commands/clean").TypefixturePlatformClean, +); + +class TypefixtureAliased extends Command({ + name: ["typefixture|class-vision", "typefixture|class-visionos"], +}) { + run(): void { + return undefined; + } +} + +registerLazyCommand( + "typefixture|class-visionos", + () => require("./commands/clean").TypefixtureAliased, +); + +registerLazyCommand( + // @ts-expect-error - not one of the names the class declares + "typefixture|class-vision2", + () => require("./commands/clean").TypefixtureAliased, +); From 1aed82351321a5473db201ee3e4e1707f7dfed56 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 14 Sep 2026 17:59:22 -0300 Subject: [PATCH 04/17] refactor(commands): move three commands to the class form platform|clean, update and device|*list are written as Command() classes, with their services as inject() fields and the constructor doing the initializeProjectData() work setup did. The per-platform device listings stay in the object form: they are generated from one function, which is what that form is for. --- lib/bootstrap.ts | 11 +- lib/commands/platform-clean.ts | 137 +++++++++------------ lib/commands/update.ts | 131 +++++++++----------- lib/common/bootstrap.ts | 8 +- lib/common/commands/device/list-devices.ts | 17 ++- test/platform-commands.ts | 4 +- test/update.ts | 6 +- 7 files changed, 140 insertions(+), 174 deletions(-) diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 860a46f609..adba09da3c 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -527,10 +527,10 @@ injector.require("androidToolsInfo", "./android-tools-info"); injector.require("devicePathProvider", "./device-path-provider"); registerBuiltInCommand< - typeof import("./commands/platform-clean").platformCleanCommandDefinition + typeof import("./commands/platform-clean").PlatformCleanCommand >( "platform|clean", - () => require("./commands/platform-clean").platformCleanCommandDefinition, + () => require("./commands/platform-clean").PlatformCleanCommand, ); injector.require( @@ -583,9 +583,10 @@ registerBuiltInCommand< registerBuiltInCommand< typeof import("./commands/migrate").migrateCommandDefinition >("migrate", () => require("./commands/migrate").migrateCommandDefinition); -registerBuiltInCommand< - typeof import("./commands/update").updateCommandDefinition ->("update", () => require("./commands/update").updateCommandDefinition); +registerBuiltInCommand( + "update", + () => require("./commands/update").UpdateCommand, +); injector.require("iOSLogFilter", "./services/ios-log-filter"); injector.require("logSourceMapService", "./services/log-source-map-service"); diff --git a/lib/commands/platform-clean.ts b/lib/commands/platform-clean.ts index 521985c7ce..873558cac9 100644 --- a/lib/commands/platform-clean.ts +++ b/lib/commands/platform-clean.ts @@ -8,9 +8,8 @@ import { import { IPlatformEnvironmentRequirements } from "../definitions/platform"; import { IErrors } from "../common/declarations"; import { - CommandContext, + Command, CommandOptionsSchema, - defineCommand, stringOption, } from "../common/define-command"; import { inject } from "../common/di"; @@ -19,93 +18,71 @@ const platformCleanCommandOptions = { frameworkPath: stringOption(), } satisfies CommandOptionsSchema; -export type PlatformCleanCommandContext = CommandContext< - typeof platformCleanCommandOptions ->; - -export function setupPlatformCleanCommand() { - const services = { - $errors: inject("errors"), - $options: inject("options"), - $platformCommandHelper: inject( - "platformCommandHelper", - ), - $platformValidationService: inject( - "platformValidationService", - ), - $platformEnvironmentRequirements: inject( - "platformEnvironmentRequirements", - ), - $projectData: inject("projectData"), - }; - services.$projectData.initializeProjectData(); - - return services; -} - -export type IPlatformCleanCommandServices = ReturnType< - typeof setupPlatformCleanCommand ->; +export class PlatformCleanCommand extends Command({ + name: "platform|clean", + description: "Removes and adds again the selected platform.", + options: platformCleanCommandOptions, + arguments: "any", +}) { + private $errors = inject("errors"); + private $options = inject("options"); + private $platformCommandHelper = inject( + "platformCommandHelper", + ); + private $platformValidationService = inject( + "platformValidationService", + ); + private $platformEnvironmentRequirements = + inject("platformEnvironmentRequirements"); + private $projectData = inject("projectData"); -export async function canExecutePlatformCleanCommand( - context: PlatformCleanCommandContext, - services: IPlatformCleanCommandServices, -): Promise { - const args = context.args; - if (!args || args.length === 0) { - services.$errors.failWithHelp( - "No platform specified. Please specify a platform to clean.", - ); + constructor() { + super(); + this.$projectData.initializeProjectData(); } - _.each(args, (platform) => { - services.$platformValidationService.validatePlatform( - platform, - services.$projectData, - ); - }); + public async canExecute(): Promise { + const args = this.args; + if (!args || args.length === 0) { + this.$errors.failWithHelp( + "No platform specified. Please specify a platform to clean.", + ); + } - for (const platform of args) { - services.$platformValidationService.validatePlatformInstalled( - platform, - services.$projectData, - ); + _.each(args, (platform) => { + this.$platformValidationService.validatePlatform( + platform, + this.$projectData, + ); + }); - const currentRuntimeVersion = - services.$platformCommandHelper.getCurrentPlatformVersion( + for (const platform of args) { + this.$platformValidationService.validatePlatformInstalled( platform, - services.$projectData, + this.$projectData, ); - await services.$platformEnvironmentRequirements.checkEnvironmentRequirements( - { + + const currentRuntimeVersion = + this.$platformCommandHelper.getCurrentPlatformVersion( + platform, + this.$projectData, + ); + await this.$platformEnvironmentRequirements.checkEnvironmentRequirements({ platform, - projectDir: services.$projectData.projectDir, + projectDir: this.$projectData.projectDir, runtimeVersion: currentRuntimeVersion, - options: services.$options, - }, - ); - } + options: this.$options, + }); + } - return true; -} + return true; + } -export async function runPlatformCleanCommand( - context: PlatformCleanCommandContext, - services: IPlatformCleanCommandServices, -): Promise { - await services.$platformCommandHelper.cleanPlatforms( - context.args, - services.$projectData, - context.options.frameworkPath, - ); + public async run(): Promise { + await this.$platformCommandHelper.cleanPlatforms( + this.args, + this.$projectData, + this.options.frameworkPath, + ); + } } - -export const platformCleanCommandDefinition = defineCommand({ - name: "platform|clean", - description: "Removes and adds again the selected platform.", - options: platformCleanCommandOptions, - arguments: "any", - setup: setupPlatformCleanCommand, - canExecute: canExecutePlatformCleanCommand, - run: runPlatformCleanCommand, -}); diff --git a/lib/commands/update.ts b/lib/commands/update.ts index 367736baca..589ad5ecb1 100644 --- a/lib/commands/update.ts +++ b/lib/commands/update.ts @@ -3,9 +3,8 @@ import { IMigrateController } from "../definitions/migrate"; import { IErrors } from "../common/declarations"; import { booleanOption, - CommandContext, + Command, CommandOptionsSchema, - defineCommand, stringOption, } from "../common/define-command"; import { inject } from "../common/di"; @@ -19,84 +18,70 @@ const updateCommandOptions = { frameworkPath: stringOption(), } satisfies CommandOptionsSchema; -export type UpdateCommandContext = CommandContext; - -export function setupUpdateCommand() { - const services = { - $devicePlatformsConstants: inject( - "devicePlatformsConstants", - ), - $updateController: inject("updateController"), - $migrateController: inject("migrateController"), - $errors: inject("errors"), - $logger: inject("logger"), - $projectData: inject("projectData"), - $markingModeService: inject("markingModeService"), - }; - services.$projectData.initializeProjectData(); +export class UpdateCommand extends Command({ + name: "update", + description: + "Updates the project with the latest versions of its NativeScript dependencies.", + options: updateCommandOptions, + arguments: "any", +}) { + private $devicePlatformsConstants = inject( + "devicePlatformsConstants", + ); + private $updateController = inject("updateController"); + private $migrateController = inject("migrateController"); + private $errors = inject("errors"); + private $logger = inject("logger"); + private $projectData = inject("projectData"); + private $markingModeService = + inject("markingModeService"); - return services; -} + constructor() { + super(); + this.$projectData.initializeProjectData(); + } -export type IUpdateCommandServices = ReturnType; + public async canExecute(): Promise { + const shouldMigrate = await this.$migrateController.shouldMigrate({ + projectDir: this.$projectData.projectDir, + platforms: [ + this.$devicePlatformsConstants.Android, + this.$devicePlatformsConstants.iOS, + ], + loose: true, + }); -export async function canExecuteUpdateCommand( - context: UpdateCommandContext, - services: IUpdateCommandServices, -): Promise { - const shouldMigrate = await services.$migrateController.shouldMigrate({ - projectDir: services.$projectData.projectDir, - platforms: [ - services.$devicePlatformsConstants.Android, - services.$devicePlatformsConstants.iOS, - ], - loose: true, - }); + if (shouldMigrate) { + this.$errors.fail(SHOULD_MIGRATE_PROJECT_MESSAGE); + } - if (shouldMigrate) { - services.$errors.fail(SHOULD_MIGRATE_PROJECT_MESSAGE); + return this.args.length < 2 && this.$projectData.projectDir !== ""; } - return context.args.length < 2 && services.$projectData.projectDir !== ""; -} + public async run(): Promise { + if (this.options.markingMode) { + // ns update --markingMode + await this.$markingModeService.handleMarkingModeFullDeprecation({ + projectDir: this.$projectData.projectDir, + forceSwitch: true, + }); + return; + } -export async function runUpdateCommand( - context: UpdateCommandContext, - services: IUpdateCommandServices, -): Promise { - if (context.options.markingMode) { - // ns update --markingMode - await services.$markingModeService.handleMarkingModeFullDeprecation({ - projectDir: services.$projectData.projectDir, - forceSwitch: true, - }); - return; - } + if ( + !(await this.$updateController.shouldUpdate({ + projectDir: this.$projectData.projectDir, + version: this.args[0], + })) + ) { + this.$logger.printMarkdown(`__${PROJECT_UP_TO_DATE_MESSAGE}__`); + return; + } - if ( - !(await services.$updateController.shouldUpdate({ - projectDir: services.$projectData.projectDir, - version: context.args[0], - })) - ) { - services.$logger.printMarkdown(`__${PROJECT_UP_TO_DATE_MESSAGE}__`); - return; + await this.$updateController.update({ + projectDir: this.$projectData.projectDir, + version: this.args[0], + frameworkPath: this.options.frameworkPath, + }); } - - await services.$updateController.update({ - projectDir: services.$projectData.projectDir, - version: context.args[0], - frameworkPath: context.options.frameworkPath, - }); } - -export const updateCommandDefinition = defineCommand({ - name: "update", - description: - "Updates the project with the latest versions of its NativeScript dependencies.", - options: updateCommandOptions, - arguments: "any", - setup: setupUpdateCommand, - canExecute: canExecuteUpdateCommand, - run: runUpdateCommand, -}); diff --git a/lib/common/bootstrap.ts b/lib/common/bootstrap.ts index 4a401ec5ac..644146c909 100644 --- a/lib/common/bootstrap.ts +++ b/lib/common/bootstrap.ts @@ -91,16 +91,16 @@ registerBuiltInCommand< ); registerBuiltInCommand< - typeof import("./commands/device/list-devices").listDevicesCommandDefinition + typeof import("./commands/device/list-devices").ListDevicesCommand >( "device|*list", - () => require("./commands/device/list-devices").listDevicesCommandDefinition, + () => require("./commands/device/list-devices").ListDevicesCommand, ); registerBuiltInCommand< - typeof import("./commands/device/list-devices").listDevicesCommandDefinition + typeof import("./commands/device/list-devices").ListDevicesCommand >( "devices|*list", - () => require("./commands/device/list-devices").listDevicesCommandDefinition, + () => require("./commands/device/list-devices").ListDevicesCommand, ); registerBuiltInCommand< typeof import("./commands/device/list-devices").androidListDevicesCommand diff --git a/lib/common/commands/device/list-devices.ts b/lib/common/commands/device/list-devices.ts index 8e18771662..81feb6e834 100644 --- a/lib/common/commands/device/list-devices.ts +++ b/lib/common/commands/device/list-devices.ts @@ -3,6 +3,7 @@ import { DeviceConnectionType } from "../../../constants"; import { IErrors } from "../../declarations"; import { booleanOption, + Command, CommandContext, CommandName, CommandOptionsSchema, @@ -160,17 +161,21 @@ export async function runListDevicesCommand( } } -export const listDevicesCommandDefinition = defineCommand({ +export class ListDevicesCommand extends Command({ name: ["device|*list", "devices|*list"], description: "Lists the connected devices and emulators.", options: listDevicesCommandOptions, arguments: [{ name: "platform" }], - setup: setupListDevicesCommand, - run(context, services): Promise { - return runListDevicesCommand(context, services, context.args[0]); - }, -}); +}) { + private services = setupListDevicesCommand(); + public run(): Promise { + return runListDevicesCommand(this.context, this.services, this.args[0]); + } +} + +// One definition per platform, generated: the object form is what a family of +// commands needs, where the class form fits a single named command. const defineListPlatformDevicesCommand = ( name: TName, listedPlatform: "iOS" | "Android", diff --git a/test/platform-commands.ts b/test/platform-commands.ts index 35dd3d0836..4bc0be599b 100644 --- a/test/platform-commands.ts +++ b/test/platform-commands.ts @@ -3,7 +3,7 @@ import * as stubs from "./stubs"; import { addPlatformCommandDefinition } from "../lib/commands/add-platform"; import { removePlatformCommandDefinition } from "../lib/commands/remove-platform"; import { updatePlatformCommandDefinition } from "../lib/commands/update-platform"; -import { platformCleanCommandDefinition } from "../lib/commands/platform-clean"; +import { PlatformCleanCommand } from "../lib/commands/platform-clean"; import { registerCommand } from "../lib/common/services/command-definition-adapter"; import * as StaticConfigLib from "../lib/config"; import * as CommandsServiceLib from "../lib/common/services/commands-service"; @@ -171,7 +171,7 @@ function createTestInjector() { registerCommand(updatePlatformCommandDefinition), ); runInInjectionContext(testInjector, () => - registerCommand(platformCleanCommandDefinition), + registerCommand(PlatformCleanCommand), ); testInjector.register("resources", {}); testInjector.register("commandsService", { diff --git a/test/update.ts b/test/update.ts index 1cd0eda4fc..820ef09eef 100644 --- a/test/update.ts +++ b/test/update.ts @@ -1,6 +1,6 @@ import * as stubs from "./stubs"; import * as yok from "../lib/common/yok"; -import { updateCommandDefinition } from "../lib/commands/update"; +import { UpdateCommand } from "../lib/commands/update"; import { registerCommand } from "../lib/common/services/command-definition-adapter"; import { ICommand } from "../lib/common/definitions/commands"; import { assert } from "chai"; @@ -45,9 +45,7 @@ function createTestInjector(projectDir: string = projectFolder): IInjector { }, }); - runInInjectionContext(testInjector, () => - registerCommand(updateCommandDefinition), - ); + runInInjectionContext(testInjector, () => registerCommand(UpdateCommand)); return testInjector; } From 92002a2a631228c5341029e7146917ca7801602b Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 14 Sep 2026 18:41:02 -0300 Subject: [PATCH 05/17] refactor(commands): build one context per invocation canExecute opens the invocation with the context it builds; execute and postCommandAction reuse it, so every stage, the class instance and COMMAND_CONTEXT hold the same object. --- .../services/command-definition-adapter.ts | 27 ++++++--------- test/define-command.ts | 34 +++++++++++++++++++ 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/lib/common/services/command-definition-adapter.ts b/lib/common/services/command-definition-adapter.ts index f631c9c72f..9b4a165890 100644 --- a/lib/common/services/command-definition-adapter.ts +++ b/lib/common/services/command-definition-adapter.ts @@ -254,8 +254,9 @@ export function createCommandFromDefinition< return values; }; - // Read per call rather than snapshotted here: the options service only holds - // this command's parsed values once validateOptions has run for it. + // Read when an invocation opens rather than at definition time: the options + // service only holds this command's parsed values once validateOptions has + // run for it. const buildContext = (args: string[]): CommandContext => { const options: any = {}; for (const optionName of optionNames) { @@ -339,7 +340,7 @@ export function createCommandFromDefinition< // The state of one invocation. The command object itself is cached for the // process, so nothing invocation-scoped may live outside one of these. interface Invocation { - /** The context of the stage that is running; COMMAND_CONTEXT reads it. */ + /** Built once when the invocation opens; every stage and COMMAND_CONTEXT share it. */ context: CommandContext; injector: Injector; setup: Promise>; @@ -375,15 +376,8 @@ export function createCommandFromDefinition< const beginInvocation = (context: CommandContext): Invocation => { const invocation: Invocation = { context, - // Each entry point builds its own context object, so the token reads - // the live one rather than a snapshot: a handler that injects it gets - // the very context it was handed. injector: targetInjector.createChild([ - { - provide: COMMAND_CONTEXT, - useFactory: () => invocation.context, - shared: false, - }, + { provide: COMMAND_CONTEXT, useValue: context }, ]), setup: undefined, hasRun: false, @@ -452,9 +446,9 @@ export function createCommandFromDefinition< ? {} : { postCommandAction: async (args: string[]): Promise => { - const context = buildContext(args); - const invocation = currentInvocation || beginInvocation(context); - invocation.context = context; + const invocation = + currentInvocation || beginInvocation(buildContext(args)); + const context = invocation.context; const setupResult = await invocation.setup; await runInInjectionContext(invocation.injector, () => definition.postRun.call( @@ -490,12 +484,11 @@ export function createCommandFromDefinition< ); }, execute: async (args: string[]): Promise => { - const context = buildContext(args); const invocation = currentInvocation && !currentInvocation.hasRun ? currentInvocation - : beginInvocation(context); - invocation.context = context; + : beginInvocation(buildContext(args)); + const context = invocation.context; invocation.hasRun = true; const setupResult = await invocation.setup; diff --git a/test/define-command.ts b/test/define-command.ts index b833ec8f4a..3c4b43ae18 100644 --- a/test/define-command.ts +++ b/test/define-command.ts @@ -2550,6 +2550,40 @@ describe("defineCommand", () => { assert.strictEqual(injectedInRun, runContext); }); + it("hands one context object to every stage of an invocation", async () => { + const testInjector = createTestInjector(); + const seen: any[] = []; + + const command = createCommandFromDefinition( + defineCommand({ + name: "dctest-command-context-shared", + setup: (ctx) => { + seen.push(ctx, inject(COMMAND_CONTEXT)); + }, + canExecute: (ctx) => { + seen.push(ctx, inject(COMMAND_CONTEXT)); + return true; + }, + run: (ctx) => { + seen.push(ctx, inject(COMMAND_CONTEXT)); + }, + postRun: (ctx) => { + seen.push(ctx, inject(COMMAND_CONTEXT)); + }, + }), + testInjector, + ); + + await command.canExecute([]); + await command.execute([]); + await command.postCommandAction([]); + + assert.lengthOf(seen, 8); + for (const context of seen) { + assert.strictEqual(context, seen[0]); + } + }); + it("is scoped to the invocation, so the root injector never sees it", async () => { const testInjector = createTestInjector(); From 60d2885a263406efaa8fa71ff2287b56441af384 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 14 Sep 2026 19:36:55 -0300 Subject: [PATCH 06/17] feat(commands): ask another command whether it can execute canExecuteCommand(name, args) resolves a registered command and primes its options exactly as runCommand does, then returns its own canExecute verdict. The child builds its setup from its own services, so a command can reuse another's precondition without importing its handlers. --- lib/common/definitions/commands-service.d.ts | 8 ++ .../services/command-definition-adapter.ts | 20 +++ lib/common/services/commands-service.ts | 33 +++++ test/define-command.ts | 123 ++++++++++++++++++ test/stubs.ts | 7 + 5 files changed, 191 insertions(+) diff --git a/lib/common/definitions/commands-service.d.ts b/lib/common/definitions/commands-service.d.ts index c4e9b05fca..39a1cbcd98 100644 --- a/lib/common/definitions/commands-service.d.ts +++ b/lib/common/definitions/commands-service.d.ts @@ -23,6 +23,14 @@ interface ICommandsService { commandName: string, commandArguments?: string[], ): Promise; + /** + * Asks a command whether it could run, without running it. The command + * builds its own setup from its own services. + */ + canExecuteCommandInProcess( + commandName: string, + commandArguments?: string[], + ): Promise; } /** diff --git a/lib/common/services/command-definition-adapter.ts b/lib/common/services/command-definition-adapter.ts index 9b4a165890..10047daf25 100644 --- a/lib/common/services/command-definition-adapter.ts +++ b/lib/common/services/command-definition-adapter.ts @@ -559,6 +559,26 @@ export async function runCommand( await commandsService.executeCommandInProcess(name, args); } +/** + * Asks a registered command whether it could run on `args`, without running it. + * The named command is resolved and its options primed exactly as `runCommand` + * does, and its own `canExecute` returns the verdict. + * + * This is how one command reuses another's precondition — `embed` asking + * whether `prepare` would run. The child resolves its own services, so nothing + * crosses between the two but the name and the arguments; pass only the + * arguments the child's own `arguments` policy accepts. + */ +export async function canExecuteCommand( + name: string, + args: string[] = [], +): Promise { + const commandsService = + contextInjector().get("commandsService"); + + return commandsService.canExecuteCommandInProcess(name, args); +} + /** * Registers a command with the CLI. Takes a Command() class, the result of * defineCommand(), or a bare definition, which it defines on the caller's diff --git a/lib/common/services/commands-service.ts b/lib/common/services/commands-service.ts index 2f57a18e99..eaa797860b 100644 --- a/lib/common/services/commands-service.ts +++ b/lib/common/services/commands-service.ts @@ -283,6 +283,39 @@ export class CommandsService implements ICommandsService { } } + /** + * The `canExecute` half of {@link executeCommandInProcess}: the named command + * is resolved and its options are primed the same way, and its own + * `canExecute` returns the verdict. The child builds its own setup from its + * own services — nothing is threaded in from the caller — which is what lets + * one command reuse another's precondition without importing its handlers. + */ + public async canExecuteCommandInProcess( + commandName: string, + commandArguments: string[] = [], + ): Promise { + this.inProcessDepth++; + try { + const command = this.$injector.resolveCommand(commandName); + if (!command) { + this.$errors.failWithHelp( + `Unknown command '${helpers.stringReplaceAll(commandName, "|", " ")}'.`, + ); + } + + this.commands.push({ commandName, commandArguments }); + const restoreOptions = this.primeOptions(command); + try { + return await this.canExecuteCommand(commandName, commandArguments); + } finally { + restoreOptions(); + this.commands.pop(); + } + } finally { + this.inProcessDepth--; + } + } + /** * Merging a command's options into the parser rewrites the values the host * process is still running on: a declared default replaces the CLI-wide one diff --git a/test/define-command.ts b/test/define-command.ts index 3c4b43ae18..67241c2d2b 100644 --- a/test/define-command.ts +++ b/test/define-command.ts @@ -29,6 +29,7 @@ import { stringOption, } from "../lib/common/define-command"; import { + canExecuteCommand, createCommandFromDefinition, registerBuiltInCommand, registerCommand, @@ -1344,6 +1345,128 @@ describe("defineCommand", () => { }); }); + describe("canExecuteCommand", () => { + const createInProcessInjector = (): IInjector => { + const testInjector = new Yok(); + testInjector.register("errors", { + beginCommand: async (action: () => Promise) => action(), + failWithHelp: (message: string) => { + throw new Error(message); + }, + fail: (message: string) => { + throw new Error(message); + }, + reportCommandError: async (ex: Error) => { + throw ex; + }, + }); + testInjector.register("hooksService", HooksServiceStub); + testInjector.register("logger", LoggerStub); + testInjector.register("staticConfig", { + disableAnalytics: true, + disableCommandHooks: true, + }); + testInjector.register("extensibilityService", {}); + testInjector.register("optionsTracker", {}); + testInjector.register("options", { + validateOptions: (): void => undefined, + }); + testInjector.register("commandsService", CommandsService); + return testInjector; + }; + + it("returns the named command's own verdict without running it", async () => { + const testInjector = createInProcessInjector(); + let ran = false; + + runInInjectionContext(testInjector, () => { + registerCommand( + defineCommand({ + name: "dctest-can-yes", + arguments: "any", + canExecute: (context) => context.args[0] === "ok", + run: () => { + ran = true; + }, + }), + ); + }); + + const verdicts = [ + await runInInjectionContext(testInjector, () => + canExecuteCommand("dctest-can-yes", ["ok"]), + ), + await runInInjectionContext(testInjector, () => + canExecuteCommand("dctest-can-yes", ["nope"]), + ), + ]; + + assert.deepEqual(verdicts, [true, false]); + assert.isFalse(ran); + }); + + it("enforces the child's arguments policy before its canExecute", async () => { + const testInjector = createInProcessInjector(); + let consulted = false; + + runInInjectionContext(testInjector, () => + registerCommand( + defineCommand({ + name: "dctest-can-none", + canExecute: () => { + consulted = true; + return true; + }, + run: (): void => undefined, + }), + ), + ); + + await assert.isRejected( + runInInjectionContext(testInjector, () => + canExecuteCommand("dctest-can-none", ["stray"]), + ), + /doesn't accept parameters/, + ); + assert.isFalse(consulted); + }); + + it("builds the child's setup from the child's own services", async () => { + const testInjector = createInProcessInjector(); + testInjector.register("gadgetService", { ready: true }); + + runInInjectionContext(testInjector, () => + registerCommand( + defineCommand({ + name: "dctest-can-setup", + setup: () => ({ + $gadgetService: inject("gadgetService"), + }), + canExecute: (context, services) => services.$gadgetService.ready, + run: (): void => undefined, + }), + ), + ); + + assert.isTrue( + await runInInjectionContext(testInjector, () => + canExecuteCommand("dctest-can-setup"), + ), + ); + }); + + it("fails by name for a command that is not registered", async () => { + const testInjector = createInProcessInjector(); + + await assert.isRejected( + runInInjectionContext(testInjector, () => + canExecuteCommand("dctest-can-missing"), + ), + /Unknown command 'dctest-can-missing'/, + ); + }); + }); + describe("positional argument specs", () => { const platformCommand = (extra: any = {}) => createCommandFromDefinition( diff --git a/test/stubs.ts b/test/stubs.ts index 4b28326a29..96d60ca966 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -1335,6 +1335,13 @@ export class CommandsService implements ICommandsService { return Promise.resolve(); } + public canExecuteCommandInProcess( + commandName: string, + commandArguments?: string[], + ): Promise { + return Promise.resolve(true); + } + public completeCommand(): Promise { return Promise.resolve(true); } From 955efb2bc63577374fea96316aad84ee9f21a2d9 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 14 Sep 2026 19:37:10 -0300 Subject: [PATCH 07/17] refactor(commands): move the structured commands to the class form The commands with real internal structure - state shared between canExecute and run, values derived once per invocation, several private steps - are classes now, with one inject() field per dependency and the handlers as methods. Long handlers are split into private methods along the seams that were already there. EmbedCommand asks prepare whether it could run instead of importing its canExecute, which is what canExecuteCommand exists for. --- lib/bootstrap.ts | 71 ++-- lib/commands/add-platform.ts | 131 +++---- lib/commands/appstore-list.ts | 171 ++++---- lib/commands/appstore-upload.ts | 242 ++++++------ lib/commands/create-project.ts | 329 ++++++++-------- lib/commands/embedding/embed.ts | 99 ++--- lib/commands/plugin/build-plugin.ts | 169 ++++---- lib/commands/plugin/create-plugin.ts | 385 +++++++++--------- lib/commands/post-install.ts | 121 +++--- lib/commands/preview.ts | 112 +++--- lib/commands/test-init.ts | 319 ++++++++------- lib/commands/typings.ts | 435 ++++++++++----------- lib/commands/update-platform.ts | 138 +++---- lib/common/bootstrap.ts | 7 +- lib/common/commands/device/list-devices.ts | 86 ++-- lib/common/commands/proxy/proxy-set.ts | 296 +++++++------- test/commands/post-install.ts | 4 +- test/platform-commands.ts | 8 +- test/plugin-create.ts | 4 +- test/project-commands.ts | 4 +- test/tns-appstore-upload.ts | 4 +- 21 files changed, 1471 insertions(+), 1664 deletions(-) diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index adba09da3c..154a325910 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -176,11 +176,8 @@ injector.require( ); injector.require("platformCommandParameter", "./platform-command-param"); registerBuiltInCommand< - typeof import("./commands/create-project").createProjectCommandDefinition ->( - "create", - () => require("./commands/create-project").createProjectCommandDefinition, -); + typeof import("./commands/create-project").CreateProjectCommand +>("create", () => require("./commands/create-project").CreateProjectCommand); registerBuiltInCommand< typeof import("./commands/clean").cleanCommandDefinition >("clean", () => require("./commands/clean").cleanCommandDefinition); @@ -206,11 +203,8 @@ registerBuiltInCommand< () => require("./commands/list-platforms").listPlatformsCommandDefinition, ); registerBuiltInCommand< - typeof import("./commands/add-platform").addPlatformCommandDefinition ->( - "platform|add", - () => require("./commands/add-platform").addPlatformCommandDefinition, -); + typeof import("./commands/add-platform").AddPlatformCommand +>("platform|add", () => require("./commands/add-platform").AddPlatformCommand); registerBuiltInCommand< typeof import("./commands/remove-platform").removePlatformCommandDefinition >( @@ -218,10 +212,10 @@ registerBuiltInCommand< () => require("./commands/remove-platform").removePlatformCommandDefinition, ); registerBuiltInCommand< - typeof import("./commands/update-platform").updatePlatformCommandDefinition + typeof import("./commands/update-platform").UpdatePlatformCommand >( "platform|update", - () => require("./commands/update-platform").updatePlatformCommandDefinition, + () => require("./commands/update-platform").UpdatePlatformCommand, ); registerBuiltInCommand( "run|*all", @@ -259,13 +253,15 @@ registerBuiltInCommand( "open|vision", () => require("./commands/open").visionOpenCommand, ); -registerBuiltInCommand< - typeof import("./commands/typings").typingsCommandDefinition ->("typings", () => require("./commands/typings").typingsCommandDefinition); +registerBuiltInCommand( + "typings", + () => require("./commands/typings").TypingsCommand, +); -registerBuiltInCommand< - typeof import("./commands/preview").previewCommandDefinition ->("preview", () => require("./commands/preview").previewCommandDefinition); +registerBuiltInCommand( + "preview", + () => require("./commands/preview").PreviewCommand, +); registerBuiltInCommand( "debug|ios", @@ -312,8 +308,8 @@ registerBuiltInCommand< >("deploy", () => require("./commands/deploy").deployCommandDefinition); registerBuiltInCommand< - typeof import("./commands/embedding/embed").embedCommandDefinition ->("embed", () => require("./commands/embedding/embed").embedCommandDefinition); + typeof import("./commands/embedding/embed").EmbedCommand +>("embed", () => require("./commands/embedding/embed").EmbedCommand); injector.require("testExecutionService", "./services/test-execution-service"); injector.require( @@ -342,9 +338,10 @@ registerBuiltInCommand< "test|visionos", () => require("./commands/test").testVisionOSCommandDefinition, ); -registerBuiltInCommand< - typeof import("./commands/test-init").testInitCommandDefinition ->("test|init", () => require("./commands/test-init").testInitCommandDefinition); +registerBuiltInCommand( + "test|init", + () => require("./commands/test-init").TestInitCommand, +); registerBuiltInCommand< typeof import("./commands/generate-help").generateHelpCommandDefinition >( @@ -353,23 +350,20 @@ registerBuiltInCommand< ); registerBuiltInCommand< - typeof import("./commands/appstore-list").listiOSAppsCommandDefinition + typeof import("./commands/appstore-list").ListiOSAppsCommand >( "appstore|*list", - () => require("./commands/appstore-list").listiOSAppsCommandDefinition, + () => require("./commands/appstore-list").ListiOSAppsCommand, ); registerBuiltInCommand< - typeof import("./commands/appstore-upload").publishIOSCommandDefinition + typeof import("./commands/appstore-upload").PublishIOSCommand >( "appstore|upload", - () => require("./commands/appstore-upload").publishIOSCommandDefinition, + () => require("./commands/appstore-upload").PublishIOSCommand, ); registerBuiltInCommand< - typeof import("./commands/appstore-upload").publishIOSCommandDefinition ->( - "publish|ios", - () => require("./commands/appstore-upload").publishIOSCommandDefinition, -); + typeof import("./commands/appstore-upload").PublishIOSCommand +>("publish|ios", () => require("./commands/appstore-upload").PublishIOSCommand); registerBuiltInCommand< typeof import("./commands/apple-login").appleLoginCommandDefinition >( @@ -459,17 +453,16 @@ registerBuiltInCommand< require("./commands/plugin/update-plugin").updatePluginCommandDefinition, ); registerBuiltInCommand< - typeof import("./commands/plugin/build-plugin").buildPluginCommandDefinition + typeof import("./commands/plugin/build-plugin").BuildPluginCommand >( "plugin|build", - () => require("./commands/plugin/build-plugin").buildPluginCommandDefinition, + () => require("./commands/plugin/build-plugin").BuildPluginCommand, ); registerBuiltInCommand< - typeof import("./commands/plugin/create-plugin").createPluginCommandDefinition + typeof import("./commands/plugin/create-plugin").CreatePluginCommand >( "plugin|create", - () => - require("./commands/plugin/create-plugin").createPluginCommandDefinition, + () => require("./commands/plugin/create-plugin").CreatePluginCommand, ); registerBuiltInCommand< @@ -575,10 +568,10 @@ injector.require( injector.require("messages", "./common/messages/messages"); registerBuiltInCommand< - typeof import("./commands/post-install").postInstallCliCommandDefinition + typeof import("./commands/post-install").PostInstallCliCommand >( "post-install-cli", - () => require("./commands/post-install").postInstallCliCommandDefinition, + () => require("./commands/post-install").PostInstallCliCommand, ); registerBuiltInCommand< typeof import("./commands/migrate").migrateCommandDefinition diff --git a/lib/commands/add-platform.ts b/lib/commands/add-platform.ts index 8ecbd166b1..049978876e 100644 --- a/lib/commands/add-platform.ts +++ b/lib/commands/add-platform.ts @@ -1,13 +1,13 @@ +import { canExecuteCommandBase } from "./command-base"; import { - canExecuteCommandBase, - injectPlatformCommandServices, -} from "./command-base"; -import { IPlatformCommandHelper } from "../declarations"; + IPlatformCommandHelper, + IPlatformValidationService, +} from "../declarations"; +import { IProjectData } from "../definitions/project"; import { IErrors } from "../common/declarations"; import { - CommandContext, + Command, CommandOptionsSchema, - defineCommand, stringOption, } from "../common/define-command"; import { inject } from "../common/di"; @@ -16,82 +16,63 @@ const addPlatformCommandOptions = { frameworkPath: stringOption(), } satisfies CommandOptionsSchema; -export type AddPlatformCommandContext = CommandContext< - typeof addPlatformCommandOptions ->; +export class AddPlatformCommand extends Command({ + name: "platform|add", + description: + "Configures the current project to target the selected platform.", + options: addPlatformCommandOptions, + arguments: "any", +}) { + private $errors = inject("errors"); + private $platformCommandHelper = inject( + "platformCommandHelper", + ); + private $platformValidationService = inject( + "platformValidationService", + ); + private $projectData = inject("projectData"); -export function setupAddPlatformCommand() { - const services = { - ...injectPlatformCommandServices(), - $errors: inject("errors"), - $platformCommandHelper: inject( - "platformCommandHelper", - ), - }; - services.$projectData.initializeProjectData(); + constructor() { + super(); + this.$projectData.initializeProjectData(); + } - return services; -} + public async canExecute(): Promise { + const args = this.args; + if (!args || args.length === 0) { + this.$errors.failWithHelp( + "No platform specified. Please specify a platform to add.", + ); + } -export type IAddPlatformCommandServices = ReturnType< - typeof setupAddPlatformCommand ->; + let canExecute = true; + for (const arg of args) { + this.$platformValidationService.validatePlatform(arg, this.$projectData); -export async function canExecuteAddPlatformCommand( - context: AddPlatformCommandContext, - services: IAddPlatformCommandServices, -): Promise { - const args = context.args; - if (!args || args.length === 0) { - services.$errors.failWithHelp( - "No platform specified. Please specify a platform to add.", - ); - } + if ( + !this.$platformValidationService.isPlatformSupportedForOS( + arg, + this.$projectData, + ) + ) { + this.$errors.fail( + `Applications for platform ${arg} cannot be built on this OS`, + ); + } - let canExecute = true; - for (const arg of args) { - services.$platformValidationService.validatePlatform( - arg, - services.$projectData, - ); - - if ( - !services.$platformValidationService.isPlatformSupportedForOS( - arg, - services.$projectData, - ) - ) { - services.$errors.fail( - `Applications for platform ${arg} cannot be built on this OS`, - ); + // The assignment overwrites the previous platform's verdict, so only the + // last one decides. + canExecute = await canExecuteCommandBase(this.context, arg); } - // The assignment overwrites the previous platform's verdict, so only the - // last one decides. Kept as it was. - canExecute = await canExecuteCommandBase(services, arg); + return canExecute; } - return canExecute; -} - -export async function runAddPlatformCommand( - context: AddPlatformCommandContext, - services: IAddPlatformCommandServices, -): Promise { - await services.$platformCommandHelper.addPlatforms( - context.args, - services.$projectData, - context.options.frameworkPath, - ); + public async run(): Promise { + await this.$platformCommandHelper.addPlatforms( + this.args, + this.$projectData, + this.options.frameworkPath, + ); + } } - -export const addPlatformCommandDefinition = defineCommand({ - name: "platform|add", - description: - "Configures the current project to target the selected platform.", - options: addPlatformCommandOptions, - arguments: "any", - setup: setupAddPlatformCommand, - canExecute: canExecuteAddPlatformCommand, - run: runAddPlatformCommand, -}); diff --git a/lib/commands/appstore-list.ts b/lib/commands/appstore-list.ts index 17103adfa7..1279a3b27f 100644 --- a/lib/commands/appstore-list.ts +++ b/lib/commands/appstore-list.ts @@ -1,8 +1,7 @@ import { IErrors } from "../common/declarations"; import { - CommandContext, + Command, CommandOptionsSchema, - defineCommand, stringOption, } from "../common/define-command"; import { inject } from "../common/di"; @@ -18,107 +17,91 @@ const listiOSAppsCommandOptions = { appleSessionBase64: stringOption(), } satisfies CommandOptionsSchema; -export type ListiOSAppsCommandContext = CommandContext< - typeof listiOSAppsCommandOptions ->; - -export function setupListiOSAppsCommand() { - const services = { - $applePortalApplicationService: inject( - "applePortalApplicationService", - ), - $applePortalSessionService: inject( - "applePortalSessionService", - ), - $devicePlatformsConstants: inject( - "devicePlatformsConstants", - ), - $errors: inject("errors"), - $logger: inject("logger"), - $platformValidationService: inject( - "platformValidationService", - ), - $projectData: inject("projectData"), - $prompter: inject("prompter"), - }; - services.$projectData.initializeProjectData(); - - return services; -} - -export type IListiOSAppsCommandServices = ReturnType< - typeof setupListiOSAppsCommand ->; +export class ListiOSAppsCommand extends Command({ + name: "appstore|*list", + description: "Lists the applications in App Store Connect.", + options: listiOSAppsCommandOptions, + arguments: [{ name: "appleId" }, { name: "password" }], +}) { + private $applePortalApplicationService = + inject("applePortalApplicationService"); + private $applePortalSessionService = inject( + "applePortalSessionService", + ); + private $devicePlatformsConstants = inject( + "devicePlatformsConstants", + ); + private $errors = inject("errors"); + private $logger = inject("logger"); + private $platformValidationService = inject( + "platformValidationService", + ); + private $projectData = inject("projectData"); + private $prompter = inject("prompter"); -export async function runListiOSAppsCommand( - context: ListiOSAppsCommandContext, - services: IListiOSAppsCommandServices, -): Promise { - if ( - !services.$platformValidationService.isPlatformSupportedForOS( - services.$devicePlatformsConstants.iOS, - services.$projectData, - ) - ) { - services.$errors.fail( - `Applications for platform ${services.$devicePlatformsConstants.iOS} can not be built on this OS`, - ); + constructor() { + super(); + this.$projectData.initializeProjectData(); } - let username = context.args[0]; - let password = context.args[1]; + public async run(): Promise { + if ( + !this.$platformValidationService.isPlatformSupportedForOS( + this.$devicePlatformsConstants.iOS, + this.$projectData, + ) + ) { + this.$errors.fail( + `Applications for platform ${this.$devicePlatformsConstants.iOS} can not be built on this OS`, + ); + } - if (!username) { - username = await services.$prompter.getString("Apple ID", { - allowEmpty: false, - }); - } + let username = this.args[0]; + let password = this.args[1]; - if (!password) { - password = await services.$prompter.getPassword("Apple ID password"); - } + if (!username) { + username = await this.$prompter.getString("Apple ID", { + allowEmpty: false, + }); + } - const user = await services.$applePortalSessionService.createUserSession( - { username, password }, - { - sessionBase64: context.options.appleSessionBase64, - }, - ); - if (!user.areCredentialsValid) { - services.$errors.fail( - `Invalid username and password combination. Used '${username}' as the username.`, + if (!password) { + password = await this.$prompter.getPassword("Apple ID password"); + } + + const user = await this.$applePortalSessionService.createUserSession( + { username, password }, + { + sessionBase64: this.options.appleSessionBase64, + }, ); - } + if (!user.areCredentialsValid) { + this.$errors.fail( + `Invalid username and password combination. Used '${username}' as the username.`, + ); + } - const applications = - await services.$applePortalApplicationService.getApplications(user); + const applications = + await this.$applePortalApplicationService.getApplications(user); - if (!applications || !applications.length) { - services.$logger.info("Seems you don't have any applications yet."); - } else { - const table: any = createTable( - ["Application Name", "Bundle Identifier", "In Flight Version"], - applications.map((application) => { - const version = - (application && - application.versionSets && - application.versionSets.length && - application.versionSets[0].inFlightVersion && - application.versionSets[0].inFlightVersion.version) || - ""; - return [application.name, application.bundleId, version]; - }), - ); + if (!applications || !applications.length) { + this.$logger.info("Seems you don't have any applications yet."); + } else { + const table: any = createTable( + ["Application Name", "Bundle Identifier", "In Flight Version"], + applications.map((application) => { + const version = + (application && + application.versionSets && + application.versionSets.length && + application.versionSets[0].inFlightVersion && + application.versionSets[0].inFlightVersion.version) || + ""; + return [application.name, application.bundleId, version]; + }), + ); - services.$logger.info(table.toString()); + this.$logger.info(table.toString()); + } } } - -export const listiOSAppsCommandDefinition = defineCommand({ - name: "appstore|*list", - description: "Lists the applications in App Store Connect.", - options: listiOSAppsCommandOptions, - arguments: [{ name: "appleId" }, { name: "password" }], - setup: setupListiOSAppsCommand, - run: runListiOSAppsCommand, -}); diff --git a/lib/commands/appstore-upload.ts b/lib/commands/appstore-upload.ts index 7814e9cb2f..3d13e24cf8 100644 --- a/lib/commands/appstore-upload.ts +++ b/lib/commands/appstore-upload.ts @@ -2,9 +2,8 @@ import * as path from "path"; import { IErrors, IHostInfo } from "../common/declarations"; import { booleanOption, - CommandContext, + Command, CommandOptionsSchema, - defineCommand, objectOption, stringOption, } from "../common/define-command"; @@ -28,164 +27,153 @@ const publishIOSCommandOptions = { teamId: objectOption(), } satisfies CommandOptionsSchema; -export type PublishIOSCommandContext = CommandContext< - typeof publishIOSCommandOptions ->; - -export function setupPublishIOSCommand() { - const services = { - $applePortalSessionService: inject( - "applePortalSessionService", - ), - $buildController: inject("buildController"), - $devicePlatformsConstants: inject( - "devicePlatformsConstants", - ), - $errors: inject("errors"), - $hostInfo: inject("hostInfo"), - $itmsTransporterService: inject( - "itmsTransporterService", - ), - $logger: inject("logger"), - $options: inject("options"), - $platformValidationService: inject( - "platformValidationService", - ), - $projectData: inject("projectData"), - $prompter: inject("prompter"), - }; - services.$projectData.initializeProjectData(); - - return services; -} +export class PublishIOSCommand extends Command({ + name: ["publish|ios", "appstore|upload"], + description: "Uploads a project to App Store Connect.", + options: publishIOSCommandOptions, + // Arguments have never been rejected here, only ignored past the third. + arguments: "any", +}) { + private $applePortalSessionService = inject( + "applePortalSessionService", + ); + private $buildController = inject("buildController"); + private $devicePlatformsConstants = inject( + "devicePlatformsConstants", + ); + private $errors = inject("errors"); + private $hostInfo = inject("hostInfo"); + private $itmsTransporterService = inject( + "itmsTransporterService", + ); + private $logger = inject("logger"); + private $options = inject("options"); + private $platformValidationService = inject( + "platformValidationService", + ); + private $projectData = inject("projectData"); + private $prompter = inject("prompter"); -export type IPublishIOSCommandServices = ReturnType< - typeof setupPublishIOSCommand ->; + constructor() { + super(); + this.$projectData.initializeProjectData(); + } -export function canExecutePublishIOSCommand( - context: PublishIOSCommandContext, - services: IPublishIOSCommandServices, -): boolean { - if (!services.$hostInfo.isDarwin) { - services.$errors.fail("iOS publishing is only available on macOS."); + public canExecute(): boolean { + if (!this.$hostInfo.isDarwin) { + this.$errors.fail("iOS publishing is only available on macOS."); + } + + if ( + !this.$platformValidationService.isPlatformSupportedForOS( + this.$devicePlatformsConstants.iOS, + this.$projectData, + ) + ) { + this.$errors.fail( + `Applications for platform ${this.$devicePlatformsConstants.iOS} can not be built on this OS`, + ); + } + + return true; } - if ( - !services.$platformValidationService.isPlatformSupportedForOS( - services.$devicePlatformsConstants.iOS, - services.$projectData, - ) - ) { - services.$errors.fail( - `Applications for platform ${services.$devicePlatformsConstants.iOS} can not be built on this OS`, + public async run(): Promise { + await this.$itmsTransporterService.validate( + this.options.appleApplicationSpecificPassword, ); - } - return true; -} + const username = + this.args[0] || + (await this.$prompter.getString("Apple ID", { allowEmpty: false })); -export async function runPublishIOSCommand( - context: PublishIOSCommandContext, - services: IPublishIOSCommandServices, -): Promise { - await services.$itmsTransporterService.validate( - context.options.appleApplicationSpecificPassword, - ); + const password = + this.args[1] || (await this.$prompter.getPassword("Apple ID password")); - const username = - context.args[0] || - (await services.$prompter.getString("Apple ID", { allowEmpty: false })); + const user = await this.createUserSession(username, password); - const password = - context.args[1] || - (await services.$prompter.getPassword("Apple ID password")); + const mobileProvisionIdentifier = this.options.provision ?? this.args[2]; - const user = await services.$applePortalSessionService.createUserSession( - { username, password }, - { - applicationSpecificPassword: - context.options.appleApplicationSpecificPassword, - sessionBase64: context.options.appleSessionBase64, - requireInteractiveConsole: true, - requireApplicationSpecificPassword: true, - }, - ); - if (!user.areCredentialsValid) { - services.$errors.fail( - `Invalid username and password combination. Used '${username}' as the username.`, - ); - } + let ipaFilePath = this.options.ipa ? path.resolve(this.options.ipa) : null; - const mobileProvisionIdentifier = - context.options.provision ?? context.args[2]; + if (!mobileProvisionIdentifier && !ipaFilePath) { + this.$logger.warn( + "No mobile provision identifier set. A default mobile provision will be used. You can set one in app/App_Resources/iOS/build.xcconfig", + ); + } - let ipaFilePath = context.options.ipa - ? path.resolve(context.options.ipa) - : null; + // The build data is spread off the parsed command line, so the flags the + // upload implies have to be set on the options service rather than on the + // context, which is a copy. + this.$options.release = true; - if (!mobileProvisionIdentifier && !ipaFilePath) { - services.$logger.warn( - "No mobile provision identifier set. A default mobile provision will be used. You can set one in app/App_Resources/iOS/build.xcconfig", - ); + if (!ipaFilePath) { + ipaFilePath = await this.buildIpa(mobileProvisionIdentifier); + } + + await this.$itmsTransporterService.upload({ + credentials: { username, password }, + user, + applicationSpecificPassword: + this.options.appleApplicationSpecificPassword, + ipaFilePath, + shouldExtractIpa: !!this.options.ipa, + verboseLogging: this.$logger.getLevel() === "TRACE", + teamId: this.options.teamId, + }); } - // The build data is spread off the parsed command line, so the flags the - // upload implies have to be set on the options service rather than on the - // context, which is a copy. - services.$options.release = true; + private async createUserSession(username: string, password: string) { + const user = await this.$applePortalSessionService.createUserSession( + { username, password }, + { + applicationSpecificPassword: + this.options.appleApplicationSpecificPassword, + sessionBase64: this.options.appleSessionBase64, + requireInteractiveConsole: true, + requireApplicationSpecificPassword: true, + }, + ); + if (!user.areCredentialsValid) { + this.$errors.fail( + `Invalid username and password combination. Used '${username}' as the username.`, + ); + } + + return user; + } - if (!ipaFilePath) { - const platform = services.$devicePlatformsConstants.iOS.toLowerCase(); + private async buildIpa(mobileProvisionIdentifier: string): Promise { + const platform = this.$devicePlatformsConstants.iOS.toLowerCase(); // No .ipa path provided, build .ipa on out own. if (mobileProvisionIdentifier) { // This is not very correct as if we build multiple targets we will try to sign all of them using the signing identity here. - services.$logger.info( + this.$logger.info( "Building .ipa with the selected mobile provision and/or certificate. " + mobileProvisionIdentifier, ); - services.$options.provision = mobileProvisionIdentifier; + this.$options.provision = mobileProvisionIdentifier; const buildData = new IOSBuildData( - services.$projectData.projectDir, + this.$projectData.projectDir, platform, - { ...services.$options.argv, buildForAppStore: true, watch: false }, + { ...this.$options.argv, buildForAppStore: true, watch: false }, ); - ipaFilePath = await services.$buildController.prepareAndBuild(buildData); + return await this.$buildController.prepareAndBuild(buildData); } else { - services.$logger.info( + this.$logger.info( "No .ipa, mobile provision or certificate set. Perfect! Now we'll build .xcarchive and let Xcode pick the distribution certificate and provisioning profile for you when exporting .ipa for AppStore submission.", ); const buildData = new IOSBuildData( - services.$projectData.projectDir, + this.$projectData.projectDir, platform, - { ...services.$options.argv, buildForAppStore: true, watch: false }, + { ...this.$options.argv, buildForAppStore: true, watch: false }, ); - ipaFilePath = await services.$buildController.prepareAndBuild(buildData); - services.$logger.info(`Export at: ${ipaFilePath}`); + const ipaFilePath = + await this.$buildController.prepareAndBuild(buildData); + this.$logger.info(`Export at: ${ipaFilePath}`); + return ipaFilePath; } } - - await services.$itmsTransporterService.upload({ - credentials: { username, password }, - user, - applicationSpecificPassword: - context.options.appleApplicationSpecificPassword, - ipaFilePath, - shouldExtractIpa: !!context.options.ipa, - verboseLogging: services.$logger.getLevel() === "TRACE", - teamId: context.options.teamId, - }); } - -export const publishIOSCommandDefinition = defineCommand({ - name: ["publish|ios", "appstore|upload"], - description: "Uploads a project to App Store Connect.", - options: publishIOSCommandOptions, - // Arguments have never been rejected here, only ignored past the third. - arguments: "any", - setup: setupPublishIOSCommand, - canExecute: canExecutePublishIOSCommand, - run: runPublishIOSCommand, -}); diff --git a/lib/commands/create-project.ts b/lib/commands/create-project.ts index 01067dcf05..321e60bb06 100644 --- a/lib/commands/create-project.ts +++ b/lib/commands/create-project.ts @@ -2,9 +2,9 @@ import * as path from "path"; import { color } from "../color"; import { booleanOption, - CommandContext, + Command, CommandOptionsSchema, - defineCommand, + CommandOptionValues, stringOption, } from "../common/define-command"; import { inject } from "../common/di"; @@ -27,7 +27,7 @@ const TABS_TEMPLATE_KEY = "Tabs"; const TABS_TEMPLATE_DESCRIPTION = "An app with pre-built pages that uses tabs for navigation"; -export const createProjectCommandOptions = { +const createProjectCommandOptions = { js: booleanOption(), ng: booleanOption(), react: booleanOption(), @@ -49,22 +49,6 @@ export const createProjectCommandOptions = { ignoreScripts: booleanOption(), } satisfies CommandOptionsSchema; -export type CreateProjectCommandContext = CommandContext< - typeof createProjectCommandOptions ->; - -export function setupCreateProjectCommand() { - return { - $projectService: inject("projectService"), - $logger: inject("logger"), - $prompter: inject("prompter"), - }; -} - -export type ICreateProjectCommandServices = ReturnType< - typeof setupCreateProjectCommand ->; - interface ITemplateChoice { key?: string; value: string; @@ -233,7 +217,7 @@ const flavorTemplates: { [flavorName: string]: () => ITemplateChoice[] } = { /** The template a flavor flag selects, without asking anything. */ function selectTemplateFromOptions( - options: CreateProjectCommandContext["options"], + options: CommandOptionValues, ): string { if (options["vision-ng"] || (options.vision && options.ng)) { return constants.RESERVED_TEMPLATE_NAMES["vision-ng"]; @@ -298,10 +282,10 @@ function selectTemplateFromOptions( } function interactiveFlavorSelection( - services: ICreateProjectCommandServices, + $prompter: IPrompter, adverb: string, ): Promise { - return services.$prompter.promptForDetailedChoice( + return $prompter.promptForDetailedChoice( `${adverb}, which style of NativeScript project would you like to use:`, [ { @@ -338,7 +322,8 @@ function interactiveFlavorSelection( } async function interactiveTemplateSelection( - services: ICreateProjectCommandServices, + $logger: ILogger, + $prompter: IPrompter, flavorSelection: string, adverb: string, ): Promise { @@ -348,15 +333,14 @@ async function interactiveTemplateSelection( : []; if (selectedFlavorTemplates.length > 1) { - services.$logger.info(); + $logger.info(); const templateChoices = selectedFlavorTemplates.map((template) => { return { key: template.key, description: template.description }; }); - const selectedTemplateKey = - await services.$prompter.promptForDetailedChoice( - `${adverb}, which template would you like to start from:`, - templateChoices, - ); + const selectedTemplateKey = await $prompter.promptForDetailedChoice( + `${adverb}, which template would you like to start from:`, + templateChoices, + ); return selectedFlavorTemplates.find((t) => t.key === selectedTemplateKey) .value; @@ -366,164 +350,169 @@ async function interactiveTemplateSelection( } async function interactiveFlavorAndTemplateSelection( - services: ICreateProjectCommandServices, + $logger: ILogger, + $prompter: IPrompter, flavorAdverb: string, templateAdverb: string, ): Promise { const selectedFlavor = await interactiveFlavorSelection( - services, + $prompter, flavorAdverb, ); - return interactiveTemplateSelection(services, selectedFlavor, templateAdverb); + return interactiveTemplateSelection( + $logger, + $prompter, + selectedFlavor, + templateAdverb, + ); } -export async function runCreateProjectCommand( - context: CreateProjectCommandContext, - services: ICreateProjectCommandServices, -): Promise { - const options = context.options; - const interactiveAdverbs = ["First", "Next", "Finally"]; - const getNextInteractiveAdverb = () => { - return interactiveAdverbs.shift() || "Next"; - }; - - let isInteractionIntroShown = false; - const printInteractiveCreationIntroIfNeeded = () => { - if (isInteractionIntroShown) { - return; - } - - isInteractionIntroShown = true; - services.$logger.info(); - services.$logger.printMarkdown(`# Let’s create a NativeScript app!`); - services.$logger.printMarkdown(` +export class CreateProjectCommand extends Command< + "create", + typeof createProjectCommandOptions, + ICreateProjectData +>({ + name: "create", + description: "Creates a new NativeScript project.", + options: createProjectCommandOptions, + arguments: [{ name: "projectName" }], + enableHooks: false, +}) { + private $projectService = inject("projectService"); + private $logger = inject("logger"); + private $prompter = inject("prompter"); + + public async run(): Promise { + const options = this.options; + const interactiveAdverbs = ["First", "Next", "Finally"]; + const getNextInteractiveAdverb = () => { + return interactiveAdverbs.shift() || "Next"; + }; + + let isInteractionIntroShown = false; + const printInteractiveCreationIntroIfNeeded = () => { + if (isInteractionIntroShown) { + return; + } + + isInteractionIntroShown = true; + this.$logger.info(); + this.$logger.printMarkdown(`# Let’s create a NativeScript app!`); + this.$logger.printMarkdown(` Answer the following questions to help us build the right app for you. (Note: you can skip this prompt next time using the --template option, or using --ng, --react, --solid, --svelte, --vue, --ts, or --js flags.) `); - }; - - if ( - (options.tsc || - options.ng || - options.vue || - options.react || - options.solid || - options.svelte || - options.js) && - options.template - ) { - context.fail( - "You cannot use a flavor option like --ng, --vue, --react, --solid, --svelte, --tsc and --js together with --template.", - ); - } + }; + + if ( + (options.tsc || + options.ng || + options.vue || + options.react || + options.solid || + options.svelte || + options.js) && + options.template + ) { + this.context.fail( + "You cannot use a flavor option like --ng, --vue, --react, --solid, --svelte, --tsc and --js together with --template.", + ); + } - let projectName = context.args[0]; - let selectedTemplate = selectTemplateFromOptions(options); + let projectName = this.args[0]; + let selectedTemplate = selectTemplateFromOptions(options); - if (!projectName && isInteractive()) { - printInteractiveCreationIntroIfNeeded(); - projectName = await services.$prompter.getString( - `${getNextInteractiveAdverb()}, what will be the name of your app?`, - { allowEmpty: false }, - ); - services.$logger.info(); - } + if (!projectName && isInteractive()) { + printInteractiveCreationIntroIfNeeded(); + projectName = await this.$prompter.getString( + `${getNextInteractiveAdverb()}, what will be the name of your app?`, + { allowEmpty: false }, + ); + this.$logger.info(); + } - projectName = await services.$projectService.validateProjectName({ - projectName: projectName, - force: options.force, - pathToProject: options.path, - }); - - if (!selectedTemplate && isInteractive()) { - printInteractiveCreationIntroIfNeeded(); - selectedTemplate = await interactiveFlavorAndTemplateSelection( - services, - getNextInteractiveAdverb(), - getNextInteractiveAdverb(), - ); - } + projectName = await this.$projectService.validateProjectName({ + projectName: projectName, + force: options.force, + pathToProject: options.path, + }); - return services.$projectService.createProject({ - projectName: projectName, - template: selectedTemplate, - appId: options.appid, - pathToProject: options.path, - // its already validated above - force: true, - ignoreScripts: options.ignoreScripts, - }); -} + if (!selectedTemplate && isInteractive()) { + printInteractiveCreationIntroIfNeeded(); + selectedTemplate = await interactiveFlavorAndTemplateSelection( + this.$logger, + this.$prompter, + getNextInteractiveAdverb(), + getNextInteractiveAdverb(), + ); + } -export function reportCreatedProject( - context: CreateProjectCommandContext, - createdProjectData: ICreateProjectData, - services: ICreateProjectCommandServices, -): void { - const { projectDir, projectName } = createdProjectData; - const relativePath = path.relative(process.cwd(), projectDir); - - const greyDollarSign = color.grey("$"); - services.$logger.clearScreen(); - let runDebugNotes: Array = []; - if ( - context.options.vision || - context.options["vision-ng"] || - context.options["vision-react"] || - context.options["vision-solid"] || - context.options["vision-svelte"] || - context.options["vision-vue"] - ) { - runDebugNotes = [ - `Run the project on Vision Pro with:`, - "", - ` ${greyDollarSign} ${color.green("ns run visionos --no-hmr")}`, - ]; - } else { - runDebugNotes = [ - `Run the project on multiple devices:`, - "", - ` ${greyDollarSign} ${color.green("ns run ios")}`, - ` ${greyDollarSign} ${color.green("ns run android")}`, - "", - "Debug the project with Chrome DevTools:", - "", - ` ${greyDollarSign} ${color.green("ns debug ios")}`, - ` ${greyDollarSign} ${color.green("ns debug android")}`, - ]; + return this.$projectService.createProject({ + projectName: projectName, + template: selectedTemplate, + appId: options.appid, + pathToProject: options.path, + // its already validated above + force: true, + ignoreScripts: options.ignoreScripts, + }); } - services.$logger.info( - [ + + public postRun(createdProjectData: ICreateProjectData): void { + const { projectDir, projectName } = createdProjectData; + const relativePath = path.relative(process.cwd(), projectDir); + + const greyDollarSign = color.grey("$"); + this.$logger.clearScreen(); + let runDebugNotes: Array = []; + if ( + this.options.vision || + this.options["vision-ng"] || + this.options["vision-react"] || + this.options["vision-solid"] || + this.options["vision-svelte"] || + this.options["vision-vue"] + ) { + runDebugNotes = [ + `Run the project on Vision Pro with:`, + "", + ` ${greyDollarSign} ${color.green("ns run visionos --no-hmr")}`, + ]; + } else { + runDebugNotes = [ + `Run the project on multiple devices:`, + "", + ` ${greyDollarSign} ${color.green("ns run ios")}`, + ` ${greyDollarSign} ${color.green("ns run android")}`, + "", + "Debug the project with Chrome DevTools:", + "", + ` ${greyDollarSign} ${color.green("ns debug ios")}`, + ` ${greyDollarSign} ${color.green("ns debug android")}`, + ]; + } + this.$logger.info( [ - color.green(`Project`), - color.cyan(projectName), - color.green(`was successfully created.`), - ].join(" "), - "", - `Now you can navigate to your project with ${color.cyan( - `cd ${relativePath}`, - )} and then:`, - "", - ...runDebugNotes, - ``, - `For more options consult the docs or run ${color.green("ns --help")}`, - "", - ].join("\n"), - ); - // todo: add back ns preview - // this.$logger.printMarkdown( - // `After that you can preview it on device by executing \`$ ns preview\`` - // ); + [ + color.green(`Project`), + color.cyan(projectName), + color.green(`was successfully created.`), + ].join(" "), + "", + `Now you can navigate to your project with ${color.cyan( + `cd ${relativePath}`, + )} and then:`, + "", + ...runDebugNotes, + ``, + `For more options consult the docs or run ${color.green("ns --help")}`, + "", + ].join("\n"), + ); + // todo: add back ns preview + // this.$logger.printMarkdown( + // `After that you can preview it on device by executing \`$ ns preview\`` + // ); + } } - -export const createProjectCommandDefinition = defineCommand({ - name: "create", - description: "Creates a new NativeScript project.", - options: createProjectCommandOptions, - arguments: [{ name: "projectName" }], - enableHooks: false, - setup: setupCreateProjectCommand, - run: runCreateProjectCommand, - postRun: reportCreatedProject, -}); diff --git a/lib/commands/embedding/embed.ts b/lib/commands/embedding/embed.ts index e0a245a0c3..111522c98f 100644 --- a/lib/commands/embedding/embed.ts +++ b/lib/commands/embedding/embed.ts @@ -1,16 +1,13 @@ import { resolve } from "path"; import { color } from "../../color"; -import { defineCommand } from "../../common/define-command"; -import { inject } from "../../common/di"; +import { IOptions } from "../../declarations"; +import { IProjectConfigService, IProjectData } from "../../definitions/project"; +import { Command } from "../../common/define-command"; import { IFileSystem } from "../../common/declarations"; -import { IProjectConfigService } from "../../definitions/project"; +import { inject } from "../../common/di"; +import { canExecuteCommand } from "../../common/services/command-definition-adapter"; import { platformArgument } from "../command-base"; -import { - canExecutePrepareCommand, - prepareCommandOptions, - runPrepareCommand, - setupPrepareCommand, -} from "../prepare"; +import { prepareCommandOptions, runPrepareCommand } from "../prepare"; function resolveHostProjectPath( projectDir: string, @@ -23,7 +20,7 @@ function resolveHostProjectPath( return resolve(hostProjectPath); } -export const embedCommandDefinition = defineCommand({ +export class EmbedCommand extends Command({ name: "embed", description: "Prepares the project so it can be embedded into a native host project.", @@ -33,45 +30,45 @@ export const embedCommandDefinition = defineCommand({ { name: "hostProjectPath" }, { name: "hostProjectModuleName" }, ], - setup(context) { - const services = setupPrepareCommand(); - const $projectConfigService = inject( - "projectConfigService", - ); - const platform = (context.args[0] || "").toLowerCase(); - // embed.., falling back to embed. - const configValue = (key: string): string => - $projectConfigService.getValue( - `embed.${platform}.${key}`, - $projectConfigService.getValue(`embed.${key}`), - ); +}) { + private $fs = inject("fs"); + private $logger = inject("logger"); + private $options = inject("options"); + private $projectConfigService = inject( + "projectConfigService", + ); + private $projectData = inject("projectData"); + + private platform = (this.args[0] || "").toLowerCase(); + private hostProjectPath = this.args[1] || this.configValue("hostProjectPath"); + private hostProjectModuleName = + this.args[2] || this.configValue("hostProjectModuleName"); + + constructor() { + super(); + this.$projectData.initializeProjectData(); + } - return { - ...services, - $fs: inject("fs"), - $logger: inject("logger"), - hostProjectPath: context.args[1] || configValue("hostProjectPath"), - hostProjectModuleName: - context.args[2] || configValue("hostProjectModuleName"), - }; - }, - async canExecute(context, services): Promise { - if (!(await canExecutePrepareCommand(context, services))) { + public async canExecute(): Promise { + // `prepare` takes the platform alone; the host project arguments are this + // command's own and it would reject them. + if (!(await canExecuteCommand("prepare", this.args.slice(0, 1)))) { return false; } - return !!services.hostProjectPath; - }, - async run(context, services): Promise { + return !!this.hostProjectPath; + } + + public async run(): Promise { const resolvedHostProjectPath = resolveHostProjectPath( - services.$projectData.projectDir, - services.hostProjectPath, + this.$projectData.projectDir, + this.hostProjectPath, ); - if (!services.$fs.exists(resolvedHostProjectPath)) { - services.$logger.error( + if (!this.$fs.exists(resolvedHostProjectPath)) { + this.$logger.error( `The host project path ${color.yellow( - services.hostProjectPath, + this.hostProjectPath, )} (resolved to: ${color.styleText( ["yellow", "dim"], resolvedHostProjectPath, @@ -80,11 +77,19 @@ export const embedCommandDefinition = defineCommand({ return; } - services.$options.hostProjectPath = resolvedHostProjectPath; - if (services.hostProjectModuleName) { - services.$options.hostProjectModuleName = services.hostProjectModuleName; + this.$options.hostProjectPath = resolvedHostProjectPath; + if (this.hostProjectModuleName) { + this.$options.hostProjectModuleName = this.hostProjectModuleName; } - await runPrepareCommand(context, services); - }, -}); + await runPrepareCommand(this.context); + } + + /** embed.., falling back to embed.. */ + private configValue(key: string): string { + return this.$projectConfigService.getValue( + `embed.${this.platform}.${key}`, + this.$projectConfigService.getValue(`embed.${key}`), + ); + } +} diff --git a/lib/commands/plugin/build-plugin.ts b/lib/commands/plugin/build-plugin.ts index 56eb6327b8..9c43357b14 100644 --- a/lib/commands/plugin/build-plugin.ts +++ b/lib/commands/plugin/build-plugin.ts @@ -7,9 +7,8 @@ import { } from "../../definitions/android-plugin-migrator"; import { IErrors, IFileSystem } from "../../common/declarations"; import { - CommandContext, + Command, CommandOptionsSchema, - defineCommand, stringOption, } from "../../common/define-command"; import { inject } from "../../common/di"; @@ -21,108 +20,88 @@ const buildPluginCommandOptions = { gradleArgs: stringOption(), } satisfies CommandOptionsSchema; -export type BuildPluginCommandContext = CommandContext< - typeof buildPluginCommandOptions ->; - -export function setupBuildPluginCommand(context: BuildPluginCommandContext) { - return { - pluginProjectPath: path.resolve(context.options.path || "."), - $androidPluginBuildService: inject( - "androidPluginBuildService", - ), - $errors: inject("errors"), - $logger: inject("logger"), - $fs: inject("fs"), - $tempService: inject("tempService"), - }; -} - -export type IBuildPluginCommandServices = ReturnType< - typeof setupBuildPluginCommand ->; +export class BuildPluginCommand extends Command({ + name: "plugin|build", + description: + "Builds the Android parts of a NativeScript plugin into an `.aar`.", + options: buildPluginCommandOptions, + arguments: "any", +}) { + private $androidPluginBuildService = inject( + "androidPluginBuildService", + ); + private $errors = inject("errors"); + private $logger = inject("logger"); + private $fs = inject("fs"); + private $tempService = inject("tempService"); + + private pluginProjectPath = path.resolve(this.options.path || "."); + + public async canExecute(): Promise { + if ( + !this.$fs.exists( + path.join( + this.pluginProjectPath, + constants.PLATFORMS_DIR_NAME, + "android", + ), + ) + ) { + this.$errors.fail( + "No plugin found at the current directory, or the plugin does not need to have its platforms/android components built into an `.aar`.", + ); + } -export async function canExecuteBuildPluginCommand( - context: BuildPluginCommandContext, - services: IBuildPluginCommandServices, -): Promise { - if ( - !services.$fs.exists( - path.join( - services.pluginProjectPath, - constants.PLATFORMS_DIR_NAME, - "android", - ), - ) - ) { - services.$errors.fail( - "No plugin found at the current directory, or the plugin does not need to have its platforms/android components built into an `.aar`.", - ); + return true; } - return true; -} - -export async function runBuildPluginCommand( - context: BuildPluginCommandContext, - services: IBuildPluginCommandServices, -): Promise { - const platformsAndroidPath = path.join( - services.pluginProjectPath, - constants.PLATFORMS_DIR_NAME, - "android", - ); - let pluginName = ""; + public async run(): Promise { + const platformsAndroidPath = path.join( + this.pluginProjectPath, + constants.PLATFORMS_DIR_NAME, + "android", + ); + let pluginName = ""; - const pluginPackageJsonPath = path.join( - services.pluginProjectPath, - constants.PACKAGE_JSON_FILE_NAME, - ); + const pluginPackageJsonPath = path.join( + this.pluginProjectPath, + constants.PACKAGE_JSON_FILE_NAME, + ); - if (services.$fs.exists(pluginPackageJsonPath)) { - const packageJsonContents = services.$fs.readJson(pluginPackageJsonPath); + if (this.$fs.exists(pluginPackageJsonPath)) { + const packageJsonContents = this.$fs.readJson(pluginPackageJsonPath); - if (packageJsonContents && packageJsonContents["name"]) { - pluginName = packageJsonContents["name"]; + if (packageJsonContents && packageJsonContents["name"]) { + pluginName = packageJsonContents["name"]; + } } - } - - const tempAndroidProject = - await services.$tempService.mkdirSync("android-project"); - - const options: IPluginBuildOptions = { - gradlePath: context.options.gradlePath, - gradleArgs: context.options.gradleArgs, - aarOutputDir: platformsAndroidPath, - platformsAndroidDirPath: platformsAndroidPath, - pluginName: pluginName, - tempPluginDirPath: tempAndroidProject, - }; - const androidPluginBuildResult = - await services.$androidPluginBuildService.buildAar(options); - - if (androidPluginBuildResult) { - services.$logger.info( - `${pluginName} successfully built aar at ${platformsAndroidPath}.${EOL}Temporary Android project can be found at ${tempAndroidProject}.`, - ); - } + const tempAndroidProject = + await this.$tempService.mkdirSync("android-project"); + + const options: IPluginBuildOptions = { + gradlePath: this.options.gradlePath, + gradleArgs: this.options.gradleArgs, + aarOutputDir: platformsAndroidPath, + platformsAndroidDirPath: platformsAndroidPath, + pluginName: pluginName, + tempPluginDirPath: tempAndroidProject, + }; + + const androidPluginBuildResult = + await this.$androidPluginBuildService.buildAar(options); + + if (androidPluginBuildResult) { + this.$logger.info( + `${pluginName} successfully built aar at ${platformsAndroidPath}.${EOL}Temporary Android project can be found at ${tempAndroidProject}.`, + ); + } - const migratedIncludeGradle = - services.$androidPluginBuildService.migrateIncludeGradle(options); + const migratedIncludeGradle = + this.$androidPluginBuildService.migrateIncludeGradle(options); - if (migratedIncludeGradle) { - services.$logger.info(`${pluginName} include gradle updated.`); + if (migratedIncludeGradle) { + this.$logger.info(`${pluginName} include gradle updated.`); + } } } - -export const buildPluginCommandDefinition = defineCommand({ - name: "plugin|build", - description: - "Builds the Android parts of a NativeScript plugin into an `.aar`.", - options: buildPluginCommandOptions, - arguments: "any", - setup: setupBuildPluginCommand, - canExecute: canExecuteBuildPluginCommand, - run: runBuildPluginCommand, -}); diff --git a/lib/commands/plugin/create-plugin.ts b/lib/commands/plugin/create-plugin.ts index d164ab3cc2..62ee77059e 100644 --- a/lib/commands/plugin/create-plugin.ts +++ b/lib/commands/plugin/create-plugin.ts @@ -3,9 +3,8 @@ import { isInteractive } from "../../common/helpers"; import { INodePackageManager } from "../../declarations"; import { IErrors, IFileSystem, IChildProcess } from "../../common/declarations"; import { - CommandContext, + Command, CommandOptionsSchema, - defineCommand, stringOption, } from "../../common/define-command"; import { inject } from "../../common/di"; @@ -31,239 +30,207 @@ const createPluginCommandOptions = { includeAngularDemo: stringOption(), } satisfies CommandOptionsSchema; -export type CreatePluginCommandContext = CommandContext< - typeof createPluginCommandOptions ->; - -export function setupCreatePluginCommand() { - return { - $errors: inject("errors"), - $terminalSpinnerService: inject( - "terminalSpinnerService", - ), - $logger: inject("logger"), - $pacoteService: inject("pacoteService"), - $fs: inject("fs"), - $childProcess: inject("childProcess"), - $prompter: inject("prompter"), - $packageManager: inject("packageManager"), - }; -} - -export type ICreatePluginCommandServices = ReturnType< - typeof setupCreatePluginCommand ->; - -function ensurePackageDir( - services: ICreatePluginCommandServices, - projectDir: string, -): void { - services.$fs.createDirectory(projectDir); +export class CreatePluginCommand extends Command({ + name: "plugin|create", + description: "Creates a new project for a NativeScript plugin.", + options: createPluginCommandOptions, + arguments: "any", +}) { + private $errors = inject("errors"); + private $terminalSpinnerService = inject( + "terminalSpinnerService", + ); + private $logger = inject("logger"); + private $pacoteService = inject("pacoteService"); + private $fs = inject("fs"); + private $childProcess = inject("childProcess"); + private $prompter = inject("prompter"); + private $packageManager = inject("packageManager"); + + public canExecute(): boolean { + if (!this.args[0]) { + this.$errors.failWithHelp("You must specify the plugin repository name."); + } - if (services.$fs.exists(projectDir) && !services.$fs.isEmptyDir(projectDir)) { - services.$errors.fail(PATH_ALREADY_EXISTS_MESSAGE_TEMPLATE, projectDir); + return true; } -} -async function downloadPackage( - services: ICreatePluginCommandServices, - selectedTemplate: string, - projectDir: string, -): Promise { - if (selectedTemplate) { - services.$logger.printMarkdown( - "Make sure your custom template is compatible with the Plugin Seed at https://github.com/NativeScript/nativescript-plugin-seed/", - ); - } else { - services.$logger.printMarkdown( - "Downloading the latest version of NativeScript Plugin Seed...", + public async run(): Promise { + const pluginRepoName = this.args[0]; + const pathToProject = this.options.path; + const selectedTemplate = this.options.template; + const selectedPath = path.resolve(pathToProject || "."); + const projectDir = path.join(selectedPath, pluginRepoName); + + // Must be out of try catch block, because will throw error if folder alredy exists and we don't want to delete it. + this.ensurePackageDir(projectDir); + + try { + await this.downloadPackage(selectedTemplate, projectDir); + await this.setupSeed(projectDir, pluginRepoName); + } catch (err) { + // The call to ensurePackageDir() above will throw error if folder alredy exists, so it is safe to delete here. + this.$fs.deleteDirectory(projectDir); + throw err; + } + + this.$logger.printMarkdown( + "Solution for `%s` was successfully created.", + pluginRepoName, ); } - const spinner = services.$terminalSpinnerService.createSpinner(); - const packageToInstall = - selectedTemplate || - "https://github.com/NativeScript/nativescript-plugin-seed/archive/master.tar.gz"; - try { - spinner.start(); - await services.$pacoteService.extractPackage(packageToInstall, projectDir); - } finally { - spinner.stop(); - } -} + private ensurePackageDir(projectDir: string): void { + this.$fs.createDirectory(projectDir); -async function getGitHubUsername( - services: ICreatePluginCommandServices, - gitHubUsername: string, -): Promise { - if (!gitHubUsername) { - gitHubUsername = "NativeScriptDeveloper"; - if (isInteractive()) { - gitHubUsername = await services.$prompter.getString(USER_MESSAGE, { - allowEmpty: false, - defaultAction: () => { - return gitHubUsername; - }, - }); + if (this.$fs.exists(projectDir) && !this.$fs.isEmptyDir(projectDir)) { + this.$errors.fail(PATH_ALREADY_EXISTS_MESSAGE_TEMPLATE, projectDir); } } - return gitHubUsername; -} - -async function getPluginNameSource( - services: ICreatePluginCommandServices, - pluginNameSource: string, - pluginRepoName: string, -): Promise { - if (!pluginNameSource) { - // remove nativescript- prefix for naming plugin files - const prefix = "nativescript-"; - pluginNameSource = pluginRepoName.toLowerCase().startsWith(prefix) - ? pluginRepoName.slice(prefix.length, pluginRepoName.length) - : pluginRepoName; - if (isInteractive()) { - pluginNameSource = await services.$prompter.getString(NAME_MESSAGE, { - allowEmpty: false, - defaultAction: () => { - return pluginNameSource; - }, - }); + private async downloadPackage( + selectedTemplate: string, + projectDir: string, + ): Promise { + if (selectedTemplate) { + this.$logger.printMarkdown( + "Make sure your custom template is compatible with the Plugin Seed at https://github.com/NativeScript/nativescript-plugin-seed/", + ); + } else { + this.$logger.printMarkdown( + "Downloading the latest version of NativeScript Plugin Seed...", + ); } - } - - return pluginNameSource; -} -async function getShouldIncludeDemoResult( - services: ICreatePluginCommandServices, - includeDemoOption: string, - message: string, -): Promise { - let shouldIncludeDemo = !!includeDemoOption; - if (!includeDemoOption && isInteractive()) { - shouldIncludeDemo = await services.$prompter.confirm(message, () => { - return true; - }); + const spinner = this.$terminalSpinnerService.createSpinner(); + const packageToInstall = + selectedTemplate || + "https://github.com/NativeScript/nativescript-plugin-seed/archive/master.tar.gz"; + try { + spinner.start(); + await this.$pacoteService.extractPackage(packageToInstall, projectDir); + } finally { + spinner.stop(); + } } - return shouldIncludeDemo ? "y" : "n"; -} - -async function setupSeed( - context: CreatePluginCommandContext, - services: ICreatePluginCommandServices, - projectDir: string, - pluginRepoName: string, -): Promise { - services.$logger.printMarkdown( - "Executing initial plugin configuration script...", - ); + private async getGitHubUsername(gitHubUsername: string): Promise { + if (!gitHubUsername) { + gitHubUsername = "NativeScriptDeveloper"; + if (isInteractive()) { + gitHubUsername = await this.$prompter.getString(USER_MESSAGE, { + allowEmpty: false, + defaultAction: () => { + return gitHubUsername; + }, + }); + } + } - const config = context.options; - const spinner = services.$terminalSpinnerService.createSpinner(); - const cwd = path.join(projectDir, "src"); - try { - spinner.start(); - const npmOptions: any = { silent: true }; - await services.$packageManager.install(cwd, cwd, npmOptions); - } finally { - spinner.stop(); + return gitHubUsername; } - const gitHubUsername = await getGitHubUsername(services, config.username); - const pluginNameSource = await getPluginNameSource( - services, - config.pluginName, - pluginRepoName, - ); - const includeTypescriptDemo = await getShouldIncludeDemoResult( - services, - config.includeTypeScriptDemo, - INCLUDE_TYPESCRIPT_DEMO_MESSAGE, - ); - const includeAngularDemo = await getShouldIncludeDemoResult( - services, - config.includeAngularDemo, - INCLUDE_ANGULAR_DEMO_MESSAGE, - ); + private async getPluginNameSource( + pluginNameSource: string, + pluginRepoName: string, + ): Promise { + if (!pluginNameSource) { + // remove nativescript- prefix for naming plugin files + const prefix = "nativescript-"; + pluginNameSource = pluginRepoName.toLowerCase().startsWith(prefix) + ? pluginRepoName.slice(prefix.length, pluginRepoName.length) + : pluginRepoName; + if (isInteractive()) { + pluginNameSource = await this.$prompter.getString(NAME_MESSAGE, { + allowEmpty: false, + defaultAction: () => { + return pluginNameSource; + }, + }); + } + } - if ( - !isInteractive() && - (!config.username || - !config.pluginName || - !config.includeAngularDemo || - !config.includeTypeScriptDemo) - ) { - services.$logger.printMarkdown( - "Using default values for plugin creation options since your shell is not interactive.", - ); + return pluginNameSource; } - // run postclone script manually and kill it if it takes more than 10 sec - const pathToPostCloneScript = path.join("scripts", "postclone"); - const params = [ - pathToPostCloneScript, - `gitHubUsername=${gitHubUsername}`, - `pluginName=${pluginNameSource}`, - "initGit=y", - `includeTypeScriptDemo=${includeTypescriptDemo}`, - `includeAngularDemo=${includeAngularDemo}`, - ]; + private async getShouldIncludeDemoResult( + includeDemoOption: string, + message: string, + ): Promise { + let shouldIncludeDemo = !!includeDemoOption; + if (!includeDemoOption && isInteractive()) { + shouldIncludeDemo = await this.$prompter.confirm(message, () => { + return true; + }); + } - const outputScript = await services.$childProcess.spawnFromEvent( - process.execPath, - params, - "close", - { stdio: "inherit", cwd, timeout: 10000 }, - ); - if (outputScript && outputScript.stdout) { - services.$logger.printMarkdown(outputScript.stdout); + return shouldIncludeDemo ? "y" : "n"; } -} - -export async function runCreatePluginCommand( - context: CreatePluginCommandContext, - services: ICreatePluginCommandServices, -): Promise { - const pluginRepoName = context.args[0]; - const pathToProject = context.options.path; - const selectedTemplate = context.options.template; - const selectedPath = path.resolve(pathToProject || "."); - const projectDir = path.join(selectedPath, pluginRepoName); - // Must be out of try catch block, because will throw error if folder alredy exists and we don't want to delete it. - ensurePackageDir(services, projectDir); + private async setupSeed( + projectDir: string, + pluginRepoName: string, + ): Promise { + this.$logger.printMarkdown( + "Executing initial plugin configuration script...", + ); - try { - await downloadPackage(services, selectedTemplate, projectDir); - await setupSeed(context, services, projectDir, pluginRepoName); - } catch (err) { - // The call to ensurePackageDir() above will throw error if folder alredy exists, so it is safe to delete here. - services.$fs.deleteDirectory(projectDir); - throw err; - } + const config = this.options; + const spinner = this.$terminalSpinnerService.createSpinner(); + const cwd = path.join(projectDir, "src"); + try { + spinner.start(); + const npmOptions: any = { silent: true }; + await this.$packageManager.install(cwd, cwd, npmOptions); + } finally { + spinner.stop(); + } - services.$logger.printMarkdown( - "Solution for `%s` was successfully created.", - pluginRepoName, - ); -} + const gitHubUsername = await this.getGitHubUsername(config.username); + const pluginNameSource = await this.getPluginNameSource( + config.pluginName, + pluginRepoName, + ); + const includeTypescriptDemo = await this.getShouldIncludeDemoResult( + config.includeTypeScriptDemo, + INCLUDE_TYPESCRIPT_DEMO_MESSAGE, + ); + const includeAngularDemo = await this.getShouldIncludeDemoResult( + config.includeAngularDemo, + INCLUDE_ANGULAR_DEMO_MESSAGE, + ); -export const createPluginCommandDefinition = defineCommand({ - name: "plugin|create", - description: "Creates a new project for a NativeScript plugin.", - options: createPluginCommandOptions, - arguments: "any", - setup: setupCreatePluginCommand, - canExecute(context, services): boolean { - if (!context.args[0]) { - services.$errors.failWithHelp( - "You must specify the plugin repository name.", + if ( + !isInteractive() && + (!config.username || + !config.pluginName || + !config.includeAngularDemo || + !config.includeTypeScriptDemo) + ) { + this.$logger.printMarkdown( + "Using default values for plugin creation options since your shell is not interactive.", ); } - return true; - }, - run: runCreatePluginCommand, -}); + // run postclone script manually and kill it if it takes more than 10 sec + const pathToPostCloneScript = path.join("scripts", "postclone"); + const params = [ + pathToPostCloneScript, + `gitHubUsername=${gitHubUsername}`, + `pluginName=${pluginNameSource}`, + "initGit=y", + `includeTypeScriptDemo=${includeTypescriptDemo}`, + `includeAngularDemo=${includeAngularDemo}`, + ]; + + const outputScript = await this.$childProcess.spawnFromEvent( + process.execPath, + params, + "close", + { stdio: "inherit", cwd, timeout: 10000 }, + ); + if (outputScript && outputScript.stdout) { + this.$logger.printMarkdown(outputScript.stdout); + } + } +} diff --git a/lib/commands/post-install.ts b/lib/commands/post-install.ts index c7cea7d480..2731c152dd 100644 --- a/lib/commands/post-install.ts +++ b/lib/commands/post-install.ts @@ -6,83 +6,70 @@ import { IHostInfo, ISettingsService, } from "../common/declarations"; -import { CommandContext, defineCommand } from "../common/define-command"; +import { Command } from "../common/define-command"; import { inject } from "../common/di"; import { doesCurrentNpmCommandMatch } from "../common/helpers"; -export function setupPostInstallCliCommand() { - return { - $fs: inject("fs"), - $commandsService: inject("commandsService"), - $helpService: inject("helpService"), - $settingsService: inject("settingsService"), - $analyticsService: inject("analyticsService"), - $logger: inject("logger"), - $hostInfo: inject("hostInfo"), - }; -} - -export type IPostInstallCliCommandServices = ReturnType< - typeof setupPostInstallCliCommand ->; +export class PostInstallCliCommand extends Command({ + name: "post-install-cli", + description: "Completes the CLI installation.", + disableAnalytics: true, +}) { + private $fs = inject("fs"); + private $commandsService = inject("commandsService"); + private $helpService = inject("helpService"); + private $settingsService = inject("settingsService"); + private $analyticsService = inject("analyticsService"); + private $logger = inject("logger"); + private $hostInfo = inject("hostInfo"); -export async function runPostInstallCliCommand( - context: CommandContext, - services: IPostInstallCliCommandServices, -): Promise { - const isRunningWithSudoUser = !!process.env.SUDO_USER; + public async run(): Promise { + const isRunningWithSudoUser = !!process.env.SUDO_USER; - if (!services.$hostInfo.isWindows) { - // when running under 'sudo' we create a working dir with wrong owner (root) and - // it is no longer accessible for the user initiating the installation - // patch the owner here - if (isRunningWithSudoUser) { - // TODO: Check if this is the correct place, probably we should set this at the end of the command. - await services.$fs.setCurrentUserAsOwner( - services.$settingsService.getProfileDir(), - process.env.SUDO_USER, - ); + if (!this.$hostInfo.isWindows) { + // when running under 'sudo' we create a working dir with wrong owner (root) and + // it is no longer accessible for the user initiating the installation + // patch the owner here + if (isRunningWithSudoUser) { + // TODO: Check if this is the correct place, probably we should set this at the end of the command. + await this.$fs.setCurrentUserAsOwner( + this.$settingsService.getProfileDir(), + process.env.SUDO_USER, + ); + } } - } - const canExecutePostInstallTask = - !isRunningWithSudoUser || doesCurrentNpmCommandMatch([/^--unsafe-perm$/]); + const canExecutePostInstallTask = + !isRunningWithSudoUser || doesCurrentNpmCommandMatch([/^--unsafe-perm$/]); - if (canExecutePostInstallTask) { - await services.$helpService.generateHtmlPages(); + if (canExecutePostInstallTask) { + await this.$helpService.generateHtmlPages(); - // Explicitly ask for confirmation of usage-reporting: - await services.$analyticsService.checkConsent(); - await services.$commandsService.tryExecuteCommand("autocomplete", []); + // Explicitly ask for confirmation of usage-reporting: + await this.$analyticsService.checkConsent(); + await this.$commandsService.tryExecuteCommand("autocomplete", []); + } } -} -export function reportSuccessfulInstallation( - services: IPostInstallCliCommandServices, -): void { - services.$logger.info(""); - services.$logger.info( - color.styleText( - ["green", "bold"], - "You have successfully installed the NativeScript CLI!", - ), - ); - services.$logger.info(""); - services.$logger.info("Your next step is to create a new project:"); - services.$logger.info(color.styleText(["green", "bold"], "ns create")); + public postRun(): void { + this.reportSuccessfulInstallation(); + } - services.$logger.info(""); - services.$logger.printMarkdown( - "If you have any questions, check Stack Overflow: `https://stackoverflow.com/questions/tagged/nativescript` and our public Discord channel: `https://nativescript.org/discord`", - ); -} + private reportSuccessfulInstallation(): void { + this.$logger.info(""); + this.$logger.info( + color.styleText( + ["green", "bold"], + "You have successfully installed the NativeScript CLI!", + ), + ); + this.$logger.info(""); + this.$logger.info("Your next step is to create a new project:"); + this.$logger.info(color.styleText(["green", "bold"], "ns create")); -export const postInstallCliCommandDefinition = defineCommand({ - name: "post-install-cli", - description: "Completes the CLI installation.", - disableAnalytics: true, - setup: setupPostInstallCliCommand, - run: runPostInstallCliCommand, - postRun: (context, result, services) => - reportSuccessfulInstallation(services), -}); + this.$logger.info(""); + this.$logger.printMarkdown( + "If you have any questions, check Stack Overflow: `https://stackoverflow.com/questions/tagged/nativescript` and our public Discord channel: `https://nativescript.org/discord`", + ); + } +} diff --git a/lib/commands/preview.ts b/lib/commands/preview.ts index c70d667a6d..0ef938950b 100644 --- a/lib/commands/preview.ts +++ b/lib/commands/preview.ts @@ -4,9 +4,8 @@ import { color } from "../color"; import { IChildProcess, IErrors } from "../common/declarations"; import { booleanOption, - CommandContext, + Command, CommandOptionsSchema, - defineCommand, } from "../common/define-command"; import { inject } from "../common/di"; import { PackageManagers } from "../constants"; @@ -19,37 +18,40 @@ const previewCommandOptions = { disableNpmInstall: booleanOption(), } satisfies CommandOptionsSchema; -export type PreviewCommandContext = CommandContext< - typeof previewCommandOptions ->; +export class PreviewCommand extends Command({ + name: "preview", + description: "Runs your project with the NativeScript Preview CLI.", + options: previewCommandOptions, + // Arguments have never been rejected here, only ignored: they reach the + // preview CLI through the raw argv instead. + arguments: "any", + allowUnknownOptions: true, +}) { + private $childProcess = inject("childProcess"); + private $errors = inject("errors"); + private $logger = inject("logger"); + private $packageManager = inject("packageManager"); + private $projectData = inject("projectData"); -export function setupPreviewCommand() { - return { - $childProcess: inject("childProcess"), - $errors: inject("errors"), - $logger: inject("logger"), - $packageManager: inject("packageManager"), - $projectData: inject("projectData"), - }; -} + public async run(): Promise { + if (!this.options.disableNpmInstall) { + await this.installLatestPreviewCLI(); + } -export type IPreviewCommandServices = ReturnType; + const previewCLIPath = this.getPreviewCLIPath(); -function getPreviewCLIPath(services: IPreviewCommandServices): string { - return resolvePackagePath(PREVIEW_CLI_PACKAGE, { - paths: [services.$projectData.projectDir], - }); -} + if (!previewCLIPath) { + await this.failMissingPreviewCLI(); + } + + const previewCLIBinPath = path.resolve(previewCLIPath, "./dist/index.js"); + this.spawnPreviewCLI(previewCLIBinPath); + } -export async function runPreviewCommand( - context: PreviewCommandContext, - services: IPreviewCommandServices, -): Promise { - if (!context.options.disableNpmInstall) { - // ensure latest is installed - await services.$packageManager.install( + private async installLatestPreviewCLI(): Promise { + await this.$packageManager.install( `${PREVIEW_CLI_PACKAGE}@latest`, - services.$projectData.projectDir, + this.$projectData.projectDir, { "save-dev": true, "save-exact": true, @@ -57,11 +59,15 @@ export async function runPreviewCommand( ); } - const previewCLIPath = getPreviewCLIPath(services); + private getPreviewCLIPath(): string { + return resolvePackagePath(PREVIEW_CLI_PACKAGE, { + paths: [this.$projectData.projectDir], + }); + } - if (!previewCLIPath) { + private async failMissingPreviewCLI(): Promise { const packageManagerName = - await services.$packageManager.getPackageManagerName(); + await this.$packageManager.getPackageManagerName(); let installCommand = ""; switch (packageManagerName) { @@ -79,7 +85,7 @@ export async function runPreviewCommand( installCommand = "npm install --save-dev @nativescript/preview-cli"; break; } - services.$logger.info( + this.$logger.info( [ `Uhh ohh, no Preview CLI found.`, "", @@ -97,33 +103,21 @@ export async function runPreviewCommand( ].join("\n"), ); - services.$errors.fail("Running preview failed."); + this.$errors.fail("Running preview failed."); } - const previewCLIBinPath = path.resolve(previewCLIPath, "./dist/index.js"); - - // The preview CLI takes the command line verbatim, including flags this CLI - // does not know, so the raw process arguments are what it gets rather than - // anything the command layer parsed. - const commandIndex = process.argv.indexOf("preview"); - const commandArgs = process.argv.slice(commandIndex + 1); - services.$childProcess.spawn( - process.execPath, - [previewCLIBinPath, ...commandArgs], - { - stdio: "inherit", - }, - ); + private spawnPreviewCLI(previewCLIBinPath: string): void { + // The preview CLI takes the command line verbatim, including flags this CLI + // does not know, so the raw process arguments are what it gets rather than + // anything the command layer parsed. + const commandIndex = process.argv.indexOf("preview"); + const commandArgs = process.argv.slice(commandIndex + 1); + this.$childProcess.spawn( + process.execPath, + [previewCLIBinPath, ...commandArgs], + { + stdio: "inherit", + }, + ); + } } - -export const previewCommandDefinition = defineCommand({ - name: "preview", - description: "Runs your project with the NativeScript Preview CLI.", - options: previewCommandOptions, - // Arguments have never been rejected here, only ignored: they reach the - // preview CLI through the raw argv instead. - arguments: "any", - allowUnknownOptions: true, - setup: setupPreviewCommand, - run: runPreviewCommand, -}); diff --git a/lib/commands/test-init.ts b/lib/commands/test-init.ts index 224e9e6e8f..dc070f4e2d 100644 --- a/lib/commands/test-init.ts +++ b/lib/commands/test-init.ts @@ -9,8 +9,8 @@ import { import { INodePackageManager, IOptions } from "../declarations"; import { IPluginsService } from "../definitions/plugins"; import { + Command, CommandOptionsSchema, - defineCommand, stringOption, } from "../common/define-command"; import { inject } from "../common/di"; @@ -31,163 +31,121 @@ const testInitCommandOptions = { framework: stringOption(), } satisfies CommandOptionsSchema; -function setupTestInitCommand() { - const services = { - $errors: inject("errors"), - $fs: inject("fs"), - $logger: inject("logger"), - $options: inject("options"), - $packageManager: inject("packageManager"), - $pluginsService: inject("pluginsService"), - $projectData: inject("projectData"), - $prompter: inject("prompter"), - $resources: inject("resources"), - $testInitializationService: inject( - "testInitializationService", - ), - }; - services.$projectData.initializeProjectData(); - - return services; -} - -type ITestInitCommandServices = ReturnType; - -/** - * Android blocks cleartext traffic by default (API 28+), which would - * reject the runner's ws:// connection to the host. Scope the exception - * to the emulator loopback alias and adb-reverse loopback only. - */ -function ensureAndroidNetworkSecurityConfig( - services: ITestInitCommandServices, - bufferedLogs: string[], -): void { - const manifestPath = path.join( - services.$projectData.appResourcesDirectoryPath, - "Android", - "src", - "main", - "AndroidManifest.xml", - ); - if (!services.$fs.exists(manifestPath)) { - bufferedLogs.push( - color.yellow( - "Could not locate App_Resources/Android/src/main/AndroidManifest.xml. For Android test runs, allow cleartext traffic to 10.0.2.2 and 127.0.0.1 via a network security config.", - ), - ); - return; - } - - const manifestContent = services.$fs.readText(manifestPath); - if (manifestContent.indexOf("networkSecurityConfig") !== -1) { - bufferedLogs.push( - color.yellow( - "AndroidManifest.xml already sets android:networkSecurityConfig — make sure it permits cleartext traffic to 10.0.2.2 and 127.0.0.1 for test runs.", - ), - ); - return; - } - - const xmlDirectory = path.join( - services.$projectData.appResourcesDirectoryPath, - "Android", - "src", - "main", - "res", - "xml", - ); - services.$fs.ensureDirectoryExists(xmlDirectory); - const securityConfigPath = path.join(xmlDirectory, "network_security.xml"); - if (!services.$fs.exists(securityConfigPath)) { - services.$fs.copyFile( - services.$resources.resolvePath("test/network_security.xml"), - securityConfigPath, - ); - bufferedLogs.push( - `Added ${color.yellow("App_Resources/Android/src/main/res/xml/network_security.xml")}`, - ); - } - - services.$fs.writeFile( - manifestPath, - manifestContent.replace( - / { - const projectDir = services.$projectData.projectDir; +}) { + private $errors = inject("errors"); + private $fs = inject("fs"); + private $logger = inject("logger"); + private $options = inject("options"); + private $packageManager = inject("packageManager"); + private $pluginsService = inject("pluginsService"); + private $projectData = inject("projectData"); + private $prompter = inject("prompter"); + private $resources = inject("resources"); + private $testInitializationService = inject( + "testInitializationService", + ); - const frameworkToInstall = - context.options.framework || - (await services.$prompter.promptForChoice( - "Select testing framework:", - TESTING_FRAMEWORKS, - )); - if (TESTING_FRAMEWORKS.indexOf(frameworkToInstall) === -1) { - services.$errors.failWithHelp( - `Unknown or unsupported unit testing framework: ${frameworkToInstall}.`, + constructor() { + super(); + this.$projectData.initializeProjectData(); + } + + /** + * Android blocks cleartext traffic by default (API 28+), which would + * reject the runner's ws:// connection to the host. Scope the exception + * to the emulator loopback alias and adb-reverse loopback only. + */ + private ensureAndroidNetworkSecurityConfig(bufferedLogs: string[]): void { + const manifestPath = path.join( + this.$projectData.appResourcesDirectoryPath, + "Android", + "src", + "main", + "AndroidManifest.xml", + ); + if (!this.$fs.exists(manifestPath)) { + bufferedLogs.push( + color.yellow( + "Could not locate App_Resources/Android/src/main/AndroidManifest.xml. For Android test runs, allow cleartext traffic to 10.0.2.2 and 127.0.0.1 via a network security config.", + ), ); + return; } - const projectFilesExtension = - services.$projectData.projectType === ProjectTypes.TsFlavorName || - services.$projectData.projectType === ProjectTypes.NgFlavorName - ? ".ts" - : ".js"; + const manifestContent = this.$fs.readText(manifestPath); + if (manifestContent.indexOf("networkSecurityConfig") !== -1) { + bufferedLogs.push( + color.yellow( + "AndroidManifest.xml already sets android:networkSecurityConfig — make sure it permits cleartext traffic to 10.0.2.2 and 127.0.0.1 for test runs.", + ), + ); + return; + } - let modulesToInstall: IDependencyInformation[] = []; - try { - modulesToInstall = - services.$testInitializationService.getDependencies(frameworkToInstall); - } catch (err) { - services.$errors.fail( - `Unable to install the unit testing dependencies. Error: '${err.message}'`, + const xmlDirectory = path.join( + this.$projectData.appResourcesDirectoryPath, + "Android", + "src", + "main", + "res", + "xml", + ); + this.$fs.ensureDirectoryExists(xmlDirectory); + const securityConfigPath = path.join(xmlDirectory, "network_security.xml"); + if (!this.$fs.exists(securityConfigPath)) { + this.$fs.copyFile( + this.$resources.resolvePath("test/network_security.xml"), + securityConfigPath, + ); + bufferedLogs.push( + `Added ${color.yellow("App_Resources/Android/src/main/res/xml/network_security.xml")}`, ); } - modulesToInstall = modulesToInstall.filter( - (moduleToInstall) => - !moduleToInstall.projectType || - moduleToInstall.projectType === projectFilesExtension, + this.$fs.writeFile( + manifestPath, + manifestContent.replace( + / { for (const mod of modulesToInstall) { let moduleToInstall = mod.name; moduleToInstall += `@${mod.version}`; - await services.$packageManager.install(moduleToInstall, projectDir, { + 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, - disableNpmInstall: services.$options.disableNpmInstall, - frameworkPath: services.$options.frameworkPath, - ignoreScripts: services.$options.ignoreScripts, - path: services.$options.path, + 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 = services.$fs.readJson( - modulePackageJsonPath, - ); + const modulePackageJsonContent = this.$fs.readJson(modulePackageJsonPath); const modulePeerDependencies = modulePackageJsonContent.peerDependencies || {}; const modulePeerDependenciesMeta = modulePackageJsonContent.peerDependenciesMeta || {}; - const projectPackageJson = services.$fs.readJson( + const projectPackageJson = this.$fs.readJson( path.join(projectDir, "package.json"), ); const installedProjectDependencies = { @@ -224,50 +182,90 @@ export const testInitCommandDefinition = defineCommand({ // catch errors when a peerDependency is already installed // e.g karma is installed; karma-jasmine depends on karma and will try to install it again try { - await services.$packageManager.install( + await this.$packageManager.install( `${peerDependency}@${dependencyVersion}`, projectDir, { "save-dev": true, "save-exact": true, disableNpmInstall: false, - frameworkPath: services.$options.frameworkPath, - ignoreScripts: services.$options.ignoreScripts, - path: services.$options.path, + frameworkPath: this.$options.frameworkPath, + ignoreScripts: this.$options.ignoreScripts, + path: this.$options.path, }, ); } catch (e) { - services.$logger.error(e.message); + this.$logger.error(e.message); } } } + } + + public async run(): Promise { + const projectDir = this.$projectData.projectDir; + + const frameworkToInstall = + this.options.framework || + (await this.$prompter.promptForChoice( + "Select testing framework:", + TESTING_FRAMEWORKS, + )); + if (TESTING_FRAMEWORKS.indexOf(frameworkToInstall) === -1) { + this.$errors.failWithHelp( + `Unknown or unsupported unit testing framework: ${frameworkToInstall}.`, + ); + } + + const projectFilesExtension = + this.$projectData.projectType === ProjectTypes.TsFlavorName || + this.$projectData.projectType === ProjectTypes.NgFlavorName + ? ".ts" + : ".js"; + + let modulesToInstall: IDependencyInformation[] = []; + try { + modulesToInstall = + this.$testInitializationService.getDependencies(frameworkToInstall); + } catch (err) { + this.$errors.fail( + `Unable to install the unit testing dependencies. Error: '${err.message}'`, + ); + } + + modulesToInstall = modulesToInstall.filter( + (moduleToInstall) => + !moduleToInstall.projectType || + moduleToInstall.projectType === projectFilesExtension, + ); + + await this.installModules(modulesToInstall, projectDir); const isVitest = frameworkToInstall === "vitest"; if (!isVitest) { // The Karma client only exists in the v4 line — v5+ is Vitest-only, so // an unpinned install would break these setups once v5 is `latest`. - await services.$pluginsService.add( + await this.$pluginsService.add( "@nativescript/unit-test-runner@^4.0.0", - services.$projectData, + this.$projectData, ); } - services.$logger.clearScreen(); + this.$logger.clearScreen(); const bufferedLogs = []; - const testsDir = path.join(services.$projectData.appDirectoryPath, "tests"); + const testsDir = path.join(this.$projectData.appDirectoryPath, "tests"); const projectTestsDir = path.relative( - services.$projectData.projectDir, + this.$projectData.projectDir, testsDir, ); const relativeTestsDir = path.relative( - services.$projectData.appDirectoryPath, + this.$projectData.appDirectoryPath, testsDir, ); let shouldCreateSampleTests = true; - if (services.$fs.exists(testsDir)) { + if (this.$fs.exists(testsDir)) { const specFilenamePattern = `.spec${projectFilesExtension}`; bufferedLogs.push( color.yellow( @@ -281,18 +279,18 @@ export const testInitCommandDefinition = defineCommand({ shouldCreateSampleTests = false; } - services.$fs.ensureDirectoryExists(testsDir); + this.$fs.ensureDirectoryExists(testsDir); if (isVitest) { - const vitestConfigResourcePath = services.$resources.resolvePath( + const vitestConfigResourcePath = this.$resources.resolvePath( "test/vitest.config.mts", ); - services.$fs.copyFile( + this.$fs.copyFile( vitestConfigResourcePath, path.join(projectDir, "vitest.config.mts"), ); bufferedLogs.push(`Added/replaced ${color.yellow("vitest.config.mts")}`); - ensureAndroidNetworkSecurityConfig(services, bufferedLogs); + this.ensureAndroidNetworkSecurityConfig(bufferedLogs); } else { const frameworks = [frameworkToInstall] .concat(karmaConfigAdditionalFrameworks[frameworkToInstall] || []) @@ -301,18 +299,17 @@ export const testInitCommandDefinition = defineCommand({ const testFiles = `'${fromWindowsRelativePathToUnix( relativeTestsDir, )}/**/*${projectFilesExtension}'`; - const karmaConfTemplate = - services.$resources.readText("test/karma.conf.js"); + const karmaConfTemplate = this.$resources.readText("test/karma.conf.js"); const karmaConf = _.template(karmaConfTemplate)({ frameworks, testFiles, - basePath: services.$projectData.getAppDirectoryRelativePath(), + basePath: this.$projectData.getAppDirectoryRelativePath(), }); - services.$fs.writeFile(path.join(projectDir, "karma.conf.js"), karmaConf); + this.$fs.writeFile(path.join(projectDir, "karma.conf.js"), karmaConf); } - const exampleFilePath = services.$resources.resolvePath( + const exampleFilePath = this.$resources.resolvePath( `test/example.${frameworkToInstall}${projectFilesExtension}`, ); const targetExampleTestPath = path.join( @@ -320,8 +317,8 @@ export const testInitCommandDefinition = defineCommand({ `example.spec${projectFilesExtension}`, ); - if (shouldCreateSampleTests && services.$fs.exists(exampleFilePath)) { - services.$fs.copyFile(exampleFilePath, targetExampleTestPath); + if (shouldCreateSampleTests && this.$fs.exists(exampleFilePath)) { + this.$fs.copyFile(exampleFilePath, targetExampleTestPath); const targetExampleTestRelativePath = path.relative( projectDir, targetExampleTestPath, @@ -332,18 +329,18 @@ export const testInitCommandDefinition = defineCommand({ } // test main entry - const testMainResourcesPath = services.$resources.resolvePath( + const testMainResourcesPath = this.$resources.resolvePath( isVitest ? `test/test-main.vitest${projectFilesExtension}` : `test/test-main${projectFilesExtension}`, ); const testMainPath = path.join( - services.$projectData.appDirectoryPath, + this.$projectData.appDirectoryPath, `test${projectFilesExtension}`, ); - if (!services.$fs.exists(testMainPath)) { - services.$fs.copyFile(testMainResourcesPath, testMainPath); + if (!this.$fs.exists(testMainPath)) { + this.$fs.copyFile(testMainResourcesPath, testMainPath); const testMainRelativePath = path.relative(projectDir, testMainPath); bufferedLogs.push( `Main test entrypoint created: ${color.yellow(testMainRelativePath)}`, @@ -351,14 +348,14 @@ export const testInitCommandDefinition = defineCommand({ } if (!isVitest || projectFilesExtension === ".ts") { - const testTsConfigTemplate = services.$resources.readText( + const testTsConfigTemplate = this.$resources.readText( "test/tsconfig.spec.json", ); const testTsConfig = _.template(testTsConfigTemplate)({ - basePath: services.$projectData.getAppDirectoryRelativePath(), + basePath: this.$projectData.getAppDirectoryRelativePath(), }); - services.$fs.writeFile( + this.$fs.writeFile( path.join(projectDir, "tsconfig.spec.json"), testTsConfig, ); @@ -405,7 +402,7 @@ export const testInitCommandDefinition = defineCommand({ "", ]; - services.$logger.info( + this.$logger.info( [ [ color.green(`Tests using`), @@ -418,5 +415,5 @@ export const testInitCommandDefinition = defineCommand({ ...closingNotes, ].join("\n"), ); - }, -}); + } +} diff --git a/lib/commands/typings.ts b/lib/commands/typings.ts index d8c5d9175c..0850282161 100644 --- a/lib/commands/typings.ts +++ b/lib/commands/typings.ts @@ -5,9 +5,8 @@ import { PromptObject } from "prompts"; import { color } from "../color"; import { IChildProcess, IFileSystem, IHostInfo } from "../common/declarations"; import { - CommandContext, + Command, CommandOptionsSchema, - defineCommand, stringOption, } from "../common/define-command"; import { inject } from "../common/di"; @@ -21,266 +20,236 @@ const typingsCommandOptions = { jar: stringOption(), } satisfies CommandOptionsSchema; -export type TypingsCommandContext = CommandContext< - typeof typingsCommandOptions ->; - -export function setupTypingsCommand() { - return { - $childProcess: inject("childProcess"), - $fs: inject("fs"), - $hostInfo: inject("hostInfo"), - $logger: inject("logger"), - $mobileHelper: inject("mobileHelper"), - $options: inject("options"), - $projectData: inject("projectData"), - $prompter: inject("prompter"), - $staticConfig: inject("staticConfig"), - }; -} - -export type ITypingsCommandServices = ReturnType; - -async function resolveGradleDependencies( - services: ITypingsCommandServices, - target: string, -) { - const gradleHome = path.resolve( - process.env.GRADLE_USER_HOME ?? path.join(homedir(), `/.gradle`), - ); - const gradleFiles = path.resolve(gradleHome, "caches/modules-2/files-2.1/"); - - if (!services.$fs.exists(gradleFiles)) { - services.$logger.warn("No gradle files found"); - return; +export class TypingsCommand extends Command({ + name: "typings", + description: "Generates typings for the native platform APIs.", + options: typingsCommandOptions, + // Only the first argument is read; the rest are gradle targets this command + // takes off the raw argv, so the policy must not reject them. + arguments: "any", +}) { + private $childProcess = inject("childProcess"); + private $fs = inject("fs"); + private $hostInfo = inject("hostInfo"); + private $logger = inject("logger"); + private $mobileHelper = inject("mobileHelper"); + private $options = inject("options"); + private $projectData = inject("projectData"); + private $prompter = inject("prompter"); + private $staticConfig = inject("staticConfig"); + + public canExecute(): boolean { + this.$mobileHelper.validatePlatformName(this.args[0]); + return true; } - const pattern = `${target.replaceAll(":", "/")}/**/*.{jar,aar}`; + public async run(): Promise { + const platform = this.args[0]; + let result; + if (this.$mobileHelper.isAndroidPlatform(platform)) { + result = await this.handleAndroidTypings(); + } else if (this.$mobileHelper.isiOSPlatform(platform)) { + result = await this.handleiOSTypings(); + } + let typingsFolder = "./typings"; + if (this.options.copyTo) { + this.$fs.copyFile( + path.resolve(this.$projectData.projectDir, "typings"), + this.options.copyTo, + ); + typingsFolder = this.options.copyTo; + } - const items = []; - for await (const item of glob(pattern, { - cwd: gradleFiles, - })) { - const [group, artifact, version, sha1, file] = item.split(path.sep); - items.push({ - id: sha1 + version, - group, - artifact, - version, - sha1, - file, - path: path.resolve(gradleFiles, item), - }); + if (result !== false) { + this.$logger.info( + "Typings have been generated in the following directory:", + typingsFolder, + ); + } } - if (items.length === 0) { - services.$logger.warn("No files found"); - return []; - } + private async resolveGradleDependencies(target: string) { + const gradleHome = path.resolve( + process.env.GRADLE_USER_HOME ?? path.join(homedir(), `/.gradle`), + ); + const gradleFiles = path.resolve(gradleHome, "caches/modules-2/files-2.1/"); - services.$logger.clearScreen(); + if (!this.$fs.exists(gradleFiles)) { + this.$logger.warn("No gradle files found"); + return; + } - const choices = await services.$prompter.promptForChoice( - `Select dependencies to generate typings for (${color.greenBright( - target, - )})`, - items - .sort((a, b) => { - if (a.artifact < b.artifact) return -1; - if (a.artifact > b.artifact) return 1; + const pattern = `${target.replaceAll(":", "/")}/**/*.{jar,aar}`; + + const items = []; + for await (const item of glob(pattern, { + cwd: gradleFiles, + })) { + const [group, artifact, version, sha1, file] = item.split(path.sep); + items.push({ + id: sha1 + version, + group, + artifact, + version, + sha1, + file, + path: path.resolve(gradleFiles, item), + }); + } - return a.version.localeCompare(b.version, undefined, { - numeric: true, - sensitivity: "base", - }); - }) - .map((item) => { - return { - title: `${color.white(item.group)}:${color.greenBright( - item.artifact, - )}:${color.yellow(item.version)} - ${color.styleText( - ["cyanBright", "bold"], - item.file, - )}`, - value: item.id, - }; - }), - true, - { - optionsPerPage: process.stdout.rows - 6, // 6 lines are taken up by the instructions - } as Partial, - ); + if (items.length === 0) { + this.$logger.warn("No files found"); + return []; + } - services.$logger.clearScreen(); + this.$logger.clearScreen(); + + const choices = await this.$prompter.promptForChoice( + `Select dependencies to generate typings for (${color.greenBright( + target, + )})`, + items + .sort((a, b) => { + if (a.artifact < b.artifact) return -1; + if (a.artifact > b.artifact) return 1; + + return a.version.localeCompare(b.version, undefined, { + numeric: true, + sensitivity: "base", + }); + }) + .map((item) => { + return { + title: `${color.white(item.group)}:${color.greenBright( + item.artifact, + )}:${color.yellow(item.version)} - ${color.styleText( + ["cyanBright", "bold"], + item.file, + )}`, + value: item.id, + }; + }), + true, + { + optionsPerPage: process.stdout.rows - 6, // 6 lines are taken up by the instructions + } as Partial, + ); - return items - .filter((item) => choices.includes(item.id)) - .map((item) => item.path); -} + this.$logger.clearScreen(); -async function handleAndroidTypings( - context: TypingsCommandContext, - services: ITypingsCommandServices, -) { - // The gradle targets are positional arguments this command reads off the - // raw argv rather than declaring, so that they keep working alongside the - // --jar and --aar flags. - const targets = services.$options.argv._.slice(2) ?? []; - const paths: string[] = []; + return items + .filter((item) => choices.includes(item.id)) + .map((item) => item.path); + } - if (targets.length) { - for (const target of targets) { - try { - paths.push(...(await resolveGradleDependencies(services, target))); - } catch (err) { - services.$logger.trace( - `Failed to resolve gradle dependencies for target "${target}"`, - err, - ); + private async handleAndroidTypings() { + // The gradle targets are positional arguments this command reads off the + // raw argv rather than declaring, so that they keep working alongside the + // --jar and --aar flags. + const targets = this.$options.argv._.slice(2) ?? []; + const paths: string[] = []; + + if (targets.length) { + for (const target of targets) { + try { + paths.push(...(await this.resolveGradleDependencies(target))); + } catch (err) { + this.$logger.trace( + `Failed to resolve gradle dependencies for target "${target}"`, + err, + ); + } } } - } - if (!paths.length && !(context.options.jar || context.options.aar)) { - services.$logger.warn( - [ - "No .jar or .aar file specified. Please specify at least one of the following:", - " - path to .jar file with --jar ", - " - path to .aar file with --aar ", - ].join("\n"), - ); - return false; - } - - services.$fs.ensureDirectoryExists( - path.resolve(services.$projectData.projectDir, "typings", "android"), - ); + if (!paths.length && !(this.options.jar || this.options.aar)) { + this.$logger.warn( + [ + "No .jar or .aar file specified. Please specify at least one of the following:", + " - path to .jar file with --jar ", + " - path to .aar file with --aar ", + ].join("\n"), + ); + return false; + } - const dtsGeneratorPath = path.resolve( - services.$projectData.platformsDir, - "android", - "build-tools", - "dts-generator.jar", - ); - if (!services.$fs.exists(dtsGeneratorPath)) { - services.$logger.warn( - "No platforms folder found, preparing project now...", + this.$fs.ensureDirectoryExists( + path.resolve(this.$projectData.projectDir, "typings", "android"), ); - await services.$childProcess.spawnFromEvent( - services.$hostInfo.isWindows ? "ns.cmd" : "ns", - ["prepare", "android"], - "exit", - { stdio: "inherit", shell: services.$hostInfo.isWindows }, - ); - } - const asArray = (input: string | string[]) => { - if (!input) { - return []; + const dtsGeneratorPath = path.resolve( + this.$projectData.platformsDir, + "android", + "build-tools", + "dts-generator.jar", + ); + if (!this.$fs.exists(dtsGeneratorPath)) { + this.$logger.warn("No platforms folder found, preparing project now..."); + await this.$childProcess.spawnFromEvent( + this.$hostInfo.isWindows ? "ns.cmd" : "ns", + ["prepare", "android"], + "exit", + { stdio: "inherit", shell: this.$hostInfo.isWindows }, + ); } - if (typeof input === "string") { - return [input]; - } + const asArray = (input: string | string[]) => { + if (!input) { + return []; + } - return input; - }; + if (typeof input === "string") { + return [input]; + } - const inputs: string[] = [ - ...asArray(context.options.jar), - ...asArray(context.options.aar), - ...paths, - ]; + return input; + }; - await services.$childProcess.spawnFromEvent( - "java", - [ - "-jar", - dtsGeneratorPath, - "-input", - ...inputs, - "-output", - path.resolve(services.$projectData.projectDir, "typings", "android"), - ], - "exit", - { stdio: "inherit" }, - ); -} + const inputs: string[] = [ + ...asArray(this.options.jar), + ...asArray(this.options.aar), + ...paths, + ]; -async function handleiOSTypings( - context: TypingsCommandContext, - services: ITypingsCommandServices, -) { - if (context.options.filter !== undefined) { - services.$logger.warn("--filter flag is not supported yet."); + await this.$childProcess.spawnFromEvent( + "java", + [ + "-jar", + dtsGeneratorPath, + "-input", + ...inputs, + "-output", + path.resolve(this.$projectData.projectDir, "typings", "android"), + ], + "exit", + { stdio: "inherit" }, + ); } - services.$fs.ensureDirectoryExists( - path.resolve(services.$projectData.projectDir, "typings", "ios"), - ); - - await services.$childProcess.spawnFromEvent( - "node", - [services.$staticConfig.cliBinPath, "build", "ios"], - "exit", - { - env: { - ...process.env, - TNS_TYPESCRIPT_DECLARATIONS_PATH: path.resolve( - services.$projectData.projectDir, - "typings", - "ios", - ), - }, - stdio: "inherit", - }, - ); -} - -export function canExecuteTypingsCommand( - context: TypingsCommandContext, - services: ITypingsCommandServices, -): boolean { - services.$mobileHelper.validatePlatformName(context.args[0]); - return true; -} + private async handleiOSTypings() { + if (this.options.filter !== undefined) { + this.$logger.warn("--filter flag is not supported yet."); + } -export async function runTypingsCommand( - context: TypingsCommandContext, - services: ITypingsCommandServices, -): Promise { - const platform = context.args[0]; - let result; - if (services.$mobileHelper.isAndroidPlatform(platform)) { - result = await handleAndroidTypings(context, services); - } else if (services.$mobileHelper.isiOSPlatform(platform)) { - result = await handleiOSTypings(context, services); - } - let typingsFolder = "./typings"; - if (context.options.copyTo) { - services.$fs.copyFile( - path.resolve(services.$projectData.projectDir, "typings"), - context.options.copyTo, + this.$fs.ensureDirectoryExists( + path.resolve(this.$projectData.projectDir, "typings", "ios"), ); - typingsFolder = context.options.copyTo; - } - if (result !== false) { - services.$logger.info( - "Typings have been generated in the following directory:", - typingsFolder, + await this.$childProcess.spawnFromEvent( + "node", + [this.$staticConfig.cliBinPath, "build", "ios"], + "exit", + { + env: { + ...process.env, + TNS_TYPESCRIPT_DECLARATIONS_PATH: path.resolve( + this.$projectData.projectDir, + "typings", + "ios", + ), + }, + stdio: "inherit", + }, ); } } - -export const typingsCommandDefinition = defineCommand({ - name: "typings", - description: "Generates typings for the native platform APIs.", - options: typingsCommandOptions, - // Only the first argument is read; the rest are gradle targets this command - // takes off the raw argv, so the policy must not reject them. - arguments: "any", - setup: setupTypingsCommand, - canExecute: canExecuteTypingsCommand, - run: runTypingsCommand, -}); diff --git a/lib/commands/update-platform.ts b/lib/commands/update-platform.ts index 7eb4bd017b..eec8fd1f0e 100644 --- a/lib/commands/update-platform.ts +++ b/lib/commands/update-platform.ts @@ -10,92 +10,76 @@ import { ICheckEnvironmentRequirementsInput, } from "../definitions/platform"; import { IErrors } from "../common/declarations"; -import { CommandContext, defineCommand } from "../common/define-command"; +import { Command } from "../common/define-command"; import { inject } from "../common/di"; -export function setupUpdatePlatformCommand() { - const services = { - $errors: inject("errors"), - $options: inject("options"), - $platformEnvironmentRequirements: inject( - "platformEnvironmentRequirements", - ), - $platformCommandHelper: inject( - "platformCommandHelper", - ), - $platformValidationService: inject( - "platformValidationService", - ), - $projectData: inject("projectData"), - }; - services.$projectData.initializeProjectData(); - - return services; -} - -export type IUpdatePlatformCommandServices = ReturnType< - typeof setupUpdatePlatformCommand ->; +export class UpdatePlatformCommand extends Command({ + name: "platform|update", + description: "Updates the NativeScript runtime for the specified platform.", + arguments: "any", +}) { + private $errors = inject("errors"); + private $options = inject("options"); + private $platformEnvironmentRequirements = + inject("platformEnvironmentRequirements"); + private $platformCommandHelper = inject( + "platformCommandHelper", + ); + private $platformValidationService = inject( + "platformValidationService", + ); + private $projectData = inject("projectData"); -export async function canExecuteUpdatePlatformCommand( - context: CommandContext, - services: IUpdatePlatformCommandServices, -): Promise { - const args = context.args; - if (!args || args.length === 0) { - services.$errors.failWithHelp( - "No platform specified. Please specify platforms to update.", - ); + constructor() { + super(); + this.$projectData.initializeProjectData(); } - _.each(args, (arg) => { - const platform = arg.split("@")[0]; - services.$platformValidationService.validatePlatform( - platform, - services.$projectData, - ); - }); + public async canExecute(): Promise { + const args = this.args; + if (!args || args.length === 0) { + this.$errors.failWithHelp( + "No platform specified. Please specify platforms to update.", + ); + } - for (const arg of args) { - const [platform, versionToBeInstalled] = arg.split("@"); - const checkEnvironmentRequirementsInput: ICheckEnvironmentRequirementsInput = - { + _.each(args, (arg) => { + const platform = arg.split("@")[0]; + this.$platformValidationService.validatePlatform( platform, - options: services.$options, - }; - // If version is not specified, we know the command will install the latest compatible Android runtime. - // The latest compatible Android runtime supports Java version, so we do not need to pass it here. - // Passing projectDir to the @nativescript/doctor validation will cause it to check the runtime from the current package.json - // So in this case, where we do not want to validate the runtime, just do not pass both projectDir and runtimeVersion. - if (versionToBeInstalled) { - checkEnvironmentRequirementsInput.projectDir = - services.$projectData.projectDir; - checkEnvironmentRequirementsInput.runtimeVersion = versionToBeInstalled; + this.$projectData, + ); + }); + + for (const arg of args) { + const [platform, versionToBeInstalled] = arg.split("@"); + const checkEnvironmentRequirementsInput: ICheckEnvironmentRequirementsInput = + { + platform, + options: this.$options, + }; + // If version is not specified, we know the command will install the latest compatible Android runtime. + // The latest compatible Android runtime supports Java version, so we do not need to pass it here. + // Passing projectDir to the @nativescript/doctor validation will cause it to check the runtime from the current package.json + // So in this case, where we do not want to validate the runtime, just do not pass both projectDir and runtimeVersion. + if (versionToBeInstalled) { + checkEnvironmentRequirementsInput.projectDir = + this.$projectData.projectDir; + checkEnvironmentRequirementsInput.runtimeVersion = versionToBeInstalled; + } + + await this.$platformEnvironmentRequirements.checkEnvironmentRequirements( + checkEnvironmentRequirementsInput, + ); } - await services.$platformEnvironmentRequirements.checkEnvironmentRequirements( - checkEnvironmentRequirementsInput, - ); + return true; } - return true; -} - -export async function runUpdatePlatformCommand( - context: CommandContext, - services: IUpdatePlatformCommandServices, -): Promise { - await services.$platformCommandHelper.updatePlatforms( - context.args, - services.$projectData, - ); + public async run(): Promise { + await this.$platformCommandHelper.updatePlatforms( + this.args, + this.$projectData, + ); + } } - -export const updatePlatformCommandDefinition = defineCommand({ - name: "platform|update", - description: "Updates the NativeScript runtime for the specified platform.", - arguments: "any", - setup: setupUpdatePlatformCommand, - canExecute: canExecuteUpdatePlatformCommand, - run: runUpdatePlatformCommand, -}); diff --git a/lib/common/bootstrap.ts b/lib/common/bootstrap.ts index 644146c909..ba51125a76 100644 --- a/lib/common/bootstrap.ts +++ b/lib/common/bootstrap.ts @@ -314,11 +314,8 @@ registerBuiltInCommand< () => require("./commands/proxy/proxy-get").proxyGetCommandDefinition, ); registerBuiltInCommand< - typeof import("./commands/proxy/proxy-set").proxySetCommandDefinition ->( - "proxy|set", - () => require("./commands/proxy/proxy-set").proxySetCommandDefinition, -); + typeof import("./commands/proxy/proxy-set").ProxySetCommand +>("proxy|set", () => require("./commands/proxy/proxy-set").ProxySetCommand); registerBuiltInCommand< typeof import("./commands/proxy/proxy-clear").proxyClearCommandDefinition >( diff --git a/lib/common/commands/device/list-devices.ts b/lib/common/commands/device/list-devices.ts index 81feb6e834..24ed93670d 100644 --- a/lib/common/commands/device/list-devices.ts +++ b/lib/common/commands/device/list-devices.ts @@ -17,26 +17,12 @@ const listDevicesCommandOptions = { json: booleanOption(), } satisfies CommandOptionsSchema; -export type ListDevicesCommandContext = CommandContext< +type ListDevicesCommandContext = CommandContext< typeof listDevicesCommandOptions >; -export function setupListDevicesCommand() { - return { - $devicesService: inject("devicesService"), - $emulatorHelper: inject("emulatorHelper"), - $errors: inject("errors"), - $logger: inject("logger"), - $mobileHelper: inject("mobileHelper"), - }; -} - -export type IListDevicesCommandServices = ReturnType< - typeof setupListDevicesCommand ->; - function printEmulators( - services: IListDevicesCommandServices, + $logger: ILogger, emulators: Mobile.IDeviceInfo[], ): void { const table: any = createTable( @@ -61,14 +47,22 @@ function printEmulators( ]); } - services.$logger.info(table.toString()); + $logger.info(table.toString()); } -export async function runListDevicesCommand( +async function listDevices( context: ListDevicesCommandContext, - services: IListDevicesCommandServices, platformFilter: string, ): Promise { + const $devicesService = + context.injector.get("devicesService"); + const $emulatorHelper = + context.injector.get("emulatorHelper"); + const $errors = context.injector.get("errors"); + const $logger = context.injector.get("logger"); + const $mobileHelper = + context.injector.get("mobileHelper"); + const devices: { available?: any[]; devices: any[]; @@ -77,32 +71,31 @@ export async function runListDevicesCommand( }; if (context.options.availableDevices) { - const platform = - services.$mobileHelper.normalizePlatformName(platformFilter); + const platform = $mobileHelper.normalizePlatformName(platformFilter); if (!platform && platformFilter) { - services.$errors.fail( + $errors.fail( `${platformFilter} is not a valid device platform. The valid platforms are ${formatListOfNames( - services.$mobileHelper.platformNames, + $mobileHelper.platformNames, )}`, ); } - const availableEmulatorsOutput = - await services.$devicesService.getEmulatorImages({ platform }); - const emulators = - services.$emulatorHelper.getEmulatorsFromAvailableEmulatorsOutput( - availableEmulatorsOutput, - ); + const availableEmulatorsOutput = await $devicesService.getEmulatorImages({ + platform, + }); + const emulators = $emulatorHelper.getEmulatorsFromAvailableEmulatorsOutput( + availableEmulatorsOutput, + ); devices.available = emulators; if (!context.options.json) { - services.$logger.info(color.bold("\n Available emulators")); - printEmulators(services, emulators); + $logger.info(color.bold("\n Available emulators")); + printEmulators($logger, emulators); } } let index = 1; - await services.$devicesService.initialize({ + await $devicesService.initialize({ platform: platformFilter, deviceId: null, skipInferPlatform: true, @@ -112,7 +105,7 @@ export async function runListDevicesCommand( }); if (!context.options.json) { - services.$logger.info(color.bold("\n Connected devices & emulators")); + $logger.info(color.bold("\n Connected devices & emulators")); } const table: any = createTable( @@ -148,16 +141,16 @@ export async function runListDevicesCommand( }; } - await services.$devicesService.execute(action, undefined, { + await $devicesService.execute(action, undefined, { allowNoDevices: true, }); if (context.options.json) { - return services.$logger.info(JSON.stringify(devices, null, 2)); + return $logger.info(JSON.stringify(devices, null, 2)); } if (table.length) { - services.$logger.info(table.toString()); + $logger.info(table.toString()); } } @@ -167,10 +160,8 @@ export class ListDevicesCommand extends Command({ options: listDevicesCommandOptions, arguments: [{ name: "platform" }], }) { - private services = setupListDevicesCommand(); - public run(): Promise { - return runListDevicesCommand(this.context, this.services, this.args[0]); + return listDevices(this.context, this.args[0]); } } @@ -185,17 +176,12 @@ const defineListPlatformDevicesCommand = ( description: "Lists the connected devices and emulators for one platform.", options: listDevicesCommandOptions, arguments: "none", - setup() { - const $devicePlatformsConstants = - inject("devicePlatformsConstants"); - - return { - ...setupListDevicesCommand(), - platform: $devicePlatformsConstants[listedPlatform], - }; - }, - run(context, services): Promise { - return runListDevicesCommand(context, services, services.platform); + run(context): Promise { + const platform = inject( + "devicePlatformsConstants", + )[listedPlatform]; + + return listDevices(context, platform); }, }); diff --git a/lib/common/commands/proxy/proxy-set.ts b/lib/common/commands/proxy/proxy-set.ts index 0edf26251b..cea5c96280 100644 --- a/lib/common/commands/proxy/proxy-set.ts +++ b/lib/common/commands/proxy/proxy-set.ts @@ -1,24 +1,21 @@ import { EOL, platform } from "os"; -import { parse } from "url"; +import { parse, UrlWithStringQuery } from "url"; import { HttpProtocolToPort } from "../../constants"; import { IErrors, IHostInfo, IProxyLibSettings, + IProxyService, IPrompterQuestion, } from "../../declarations"; import { booleanOption, - CommandContext, + Command, CommandOptionsSchema, - defineCommand, } from "../../define-command"; import { inject } from "../../di"; import { isInteractive } from "../../helpers"; -import { - injectProxyCommandServices, - tryTrackProxyCommandUsage, -} from "./proxy-base"; +import { tryTrackProxyCommandUsage } from "./proxy-base"; const { getCredentialsFromAuth } = require("proxy-lib/lib/utils"); const proxySetCommandName = "proxy|set"; @@ -27,22 +24,6 @@ const proxySetCommandOptions = { insecure: booleanOption(), } satisfies CommandOptionsSchema; -export type ProxySetCommandContext = CommandContext< - typeof proxySetCommandOptions ->; - -export function setupProxySetCommand() { - return { - ...injectProxyCommandServices(), - $errors: inject("errors"), - $hostInfo: inject("hostInfo"), - $prompter: inject("prompter"), - $staticConfig: inject("staticConfig"), - }; -} - -export type IProxySetCommandServices = ReturnType; - function isPasswordRequired(username: string, password: string): boolean { return !!(username && !password); } @@ -55,151 +36,178 @@ function getInvalidPortMessage(port: number): string { return `Specified port ${port} is not valid. Please enter a value between 1 and 65535.`; } -async function getPortFromUserInput( - services: IProxySetCommandServices, -): Promise { - const schemaName = "port"; - const schema: IPrompterQuestion = { - message: "Port", - type: "text", - name: schemaName, - validate: (value: any) => { - return !value || !isValidPort(value) - ? getInvalidPortMessage(value) - : true; - }, - }; - - const prompterResult = await services.$prompter.get([schema]); - return parseInt(prompterResult[schemaName]); -} - -export async function runProxySetCommand( - context: ProxySetCommandContext, - services: IProxySetCommandServices, -): Promise { - let urlString = context.args[0]; - let username = context.args[1]; - let password = context.args[2]; +export class ProxySetCommand extends Command({ + name: proxySetCommandName, + description: "Configures a proxy for the CLI to use.", + options: proxySetCommandOptions, + arguments: [{ name: "url" }, { name: "username" }, { name: "password" }], + disableAnalytics: true, +}) { + private $logger = inject("logger"); + private $proxyService = inject("proxyService"); + private $errors = inject("errors"); + private $hostInfo = inject("hostInfo"); + private $prompter = inject("prompter"); + private $staticConfig = inject("staticConfig"); + + public async run(): Promise { + let username = this.args[1]; + let password = this.args[2]; + + const { urlString, urlObj } = await this.resolveUrl(this.args[0]); + + let port = + (urlObj.port && +urlObj.port) || HttpProtocolToPort[urlObj.protocol]; + const noPort = !port || !isValidPort(port); + + const credentials = this.resolveCredentials( + urlObj.auth || "", + username, + password, + ); + username = credentials.username; + password = credentials.password; - const noUrl = !urlString; - if (noUrl) { if (!isInteractive()) { - services.$errors.failWithHelp( - "Console is not interactive - you need to supply all command parameters.", + if (noPort) { + this.$errors.fail( + `The port you have specified (${port || "none"}) is not valid.`, + ); + } else if (isPasswordRequired(username, password)) { + this.$errors.failWithHelp( + "Console is not interactive - you need to supply all command parameters.", + ); + } + } + + if (noPort) { + if (port) { + this.$logger.warn(getInvalidPortMessage(port)); + } + + port = await this.getPortFromUserInput(); + } + + if (!username) { + this.$logger.info( + "In case your proxy requires authentication, please specify username and password. If authentication is not required, just leave it empty.", ); - } else { - urlString = await services.$prompter.getString("Url", { - allowEmpty: false, + username = await this.$prompter.getString("Username", { + defaultAction: () => "", }); } - } - let urlObj = parse(urlString); - if ((!urlObj.protocol || !urlObj.hostname) && !isInteractive()) { - services.$errors.fail( - "The url you have entered is invalid please enter a valid url containing a valid protocol and hostname.", - ); - } + if (isPasswordRequired(username, password)) { + password = await this.$prompter.getPassword("Password"); + } - while (!urlObj.protocol || !urlObj.hostname) { - services.$logger.warn( - "The url you have entered is invalid please enter a valid url containing a valid protocol and hostname.", - ); - urlString = await services.$prompter.getString("Url", { - allowEmpty: false, + await this.saveSettings({ + proxyUrl: urlString, + username, + password, + rejectUnauthorized: !this.options.insecure, }); - urlObj = parse(urlString); } - let port = - (urlObj.port && +urlObj.port) || HttpProtocolToPort[urlObj.protocol]; - const noPort = !port || !isValidPort(port); - const authCredentials = getCredentialsFromAuth(urlObj.auth || ""); - if ( - (username && - authCredentials.username && - username !== authCredentials.username) || - (password && - authCredentials.password && - password !== authCredentials.password) - ) { - services.$errors.fail( - "The credentials you have provided in the url address mismatch those passed as command line arguments.", - ); - } - username = username || authCredentials.username; - password = password || authCredentials.password; + private async resolveUrl( + urlString: string, + ): Promise<{ urlString: string; urlObj: UrlWithStringQuery }> { + const noUrl = !urlString; + if (noUrl) { + if (!isInteractive()) { + this.$errors.failWithHelp( + "Console is not interactive - you need to supply all command parameters.", + ); + } else { + urlString = await this.$prompter.getString("Url", { + allowEmpty: false, + }); + } + } - if (!isInteractive()) { - if (noPort) { - services.$errors.fail( - `The port you have specified (${port || "none"}) is not valid.`, + let urlObj = parse(urlString); + if ((!urlObj.protocol || !urlObj.hostname) && !isInteractive()) { + this.$errors.fail( + "The url you have entered is invalid please enter a valid url containing a valid protocol and hostname.", ); - } else if (isPasswordRequired(username, password)) { - services.$errors.failWithHelp( - "Console is not interactive - you need to supply all command parameters.", + } + + while (!urlObj.protocol || !urlObj.hostname) { + this.$logger.warn( + "The url you have entered is invalid please enter a valid url containing a valid protocol and hostname.", ); + urlString = await this.$prompter.getString("Url", { + allowEmpty: false, + }); + urlObj = parse(urlString); } + + return { urlString, urlObj }; } - if (noPort) { - if (port) { - services.$logger.warn(getInvalidPortMessage(port)); + private resolveCredentials( + auth: string, + username: string, + password: string, + ): { username: string; password: string } { + const authCredentials = getCredentialsFromAuth(auth); + if ( + (username && + authCredentials.username && + username !== authCredentials.username) || + (password && + authCredentials.password && + password !== authCredentials.password) + ) { + this.$errors.fail( + "The credentials you have provided in the url address mismatch those passed as command line arguments.", + ); } - port = await getPortFromUserInput(services); + return { + username: username || authCredentials.username, + password: password || authCredentials.password, + }; } - if (!username) { - services.$logger.info( - "In case your proxy requires authentication, please specify username and password. If authentication is not required, just leave it empty.", - ); - username = await services.$prompter.getString("Username", { - defaultAction: () => "", - }); + private async getPortFromUserInput(): Promise { + const schemaName = "port"; + const schema: IPrompterQuestion = { + message: "Port", + type: "text", + name: schemaName, + validate: (value: any) => { + return !value || !isValidPort(value) + ? getInvalidPortMessage(value) + : true; + }, + }; + + const prompterResult = await this.$prompter.get([schema]); + return parseInt(prompterResult[schemaName]); } - if (isPasswordRequired(username, password)) { - password = await services.$prompter.getPassword("Password"); - } + private async saveSettings(settings: IProxyLibSettings): Promise { + if (!this.$hostInfo.isWindows) { + this.$logger.warn( + `Note that storing credentials is not supported on ${platform()} yet.`, + ); + } - const settings: IProxyLibSettings = { - proxyUrl: urlString, - username, - password, - rejectUnauthorized: !context.options.insecure, - }; + const clientName = this.$staticConfig.CLIENT_NAME.toLowerCase(); + const messageNote = + (clientName === "tns" + ? "Note that 'npm' and 'Gradle' need to be configured separately to work with a proxy." + : "Note that `npm` needs to be configured separately to work with a proxy.") + + EOL; - if (!services.$hostInfo.isWindows) { - services.$logger.warn( - `Note that storing credentials is not supported on ${platform()} yet.`, + this.$logger.warn( + `${messageNote}Run '${clientName} proxy set --help' for more information.`, ); - } - const clientName = services.$staticConfig.CLIENT_NAME.toLowerCase(); - const messageNote = - (clientName === "tns" - ? "Note that 'npm' and 'Gradle' need to be configured separately to work with a proxy." - : "Note that `npm` needs to be configured separately to work with a proxy.") + - EOL; - - services.$logger.warn( - `${messageNote}Run '${clientName} proxy set --help' for more information.`, - ); - - await services.$proxyService.setCache(settings); - services.$logger.info(`Successfully setup proxy.${EOL}`); - services.$logger.info(await services.$proxyService.getInfo()); - await tryTrackProxyCommandUsage(services, proxySetCommandName); + await this.$proxyService.setCache(settings); + this.$logger.info(`Successfully setup proxy.${EOL}`); + this.$logger.info(await this.$proxyService.getInfo()); + await tryTrackProxyCommandUsage(this.$logger, proxySetCommandName); + } } - -export const proxySetCommandDefinition = defineCommand({ - name: proxySetCommandName, - description: "Configures a proxy for the CLI to use.", - options: proxySetCommandOptions, - arguments: [{ name: "url" }, { name: "username" }, { name: "password" }], - disableAnalytics: true, - setup: setupProxySetCommand, - run: runProxySetCommand, -}); diff --git a/test/commands/post-install.ts b/test/commands/post-install.ts index d39cb11327..4a7b31f216 100644 --- a/test/commands/post-install.ts +++ b/test/commands/post-install.ts @@ -1,6 +1,6 @@ import { Yok } from "../../lib/common/yok"; import { assert } from "chai"; -import { postInstallCliCommandDefinition } from "../../lib/commands/post-install"; +import { PostInstallCliCommand } from "../../lib/commands/post-install"; import { registerCommand } from "../../lib/common/services/command-definition-adapter"; import { SettingsService } from "../../lib/common/test/unit-tests/stubs"; import { IInjector } from "../../lib/common/definitions/yok"; @@ -47,7 +47,7 @@ const createTestInjector = (): IInjector => { testInjector.register("settingsService", SettingsService); runInInjectionContext(testInjector, () => - registerCommand(postInstallCliCommandDefinition), + registerCommand(PostInstallCliCommand), ); testInjector.register("hostInfo", {}); diff --git a/test/platform-commands.ts b/test/platform-commands.ts index 4bc0be599b..bcdf340861 100644 --- a/test/platform-commands.ts +++ b/test/platform-commands.ts @@ -1,8 +1,8 @@ import * as yok from "../lib/common/yok"; import * as stubs from "./stubs"; -import { addPlatformCommandDefinition } from "../lib/commands/add-platform"; +import { AddPlatformCommand } from "../lib/commands/add-platform"; import { removePlatformCommandDefinition } from "../lib/commands/remove-platform"; -import { updatePlatformCommandDefinition } from "../lib/commands/update-platform"; +import { UpdatePlatformCommand } from "../lib/commands/update-platform"; import { PlatformCleanCommand } from "../lib/commands/platform-clean"; import { registerCommand } from "../lib/common/services/command-definition-adapter"; import * as StaticConfigLib from "../lib/config"; @@ -162,13 +162,13 @@ function createTestInjector() { testInjector.register("sysInfo", {}); testInjector.register("commands-service", CommandsServiceLib.CommandsService); runInInjectionContext(testInjector, () => - registerCommand(addPlatformCommandDefinition), + registerCommand(AddPlatformCommand), ); runInInjectionContext(testInjector, () => registerCommand(removePlatformCommandDefinition), ); runInInjectionContext(testInjector, () => - registerCommand(updatePlatformCommandDefinition), + registerCommand(UpdatePlatformCommand), ); runInInjectionContext(testInjector, () => registerCommand(PlatformCleanCommand), diff --git a/test/plugin-create.ts b/test/plugin-create.ts index 6c533a0ea8..0a5f66bfea 100644 --- a/test/plugin-create.ts +++ b/test/plugin-create.ts @@ -1,7 +1,7 @@ import { Yok } from "../lib/common/yok"; import * as stubs from "./stubs"; import { - createPluginCommandDefinition, + CreatePluginCommand, INCLUDE_ANGULAR_DEMO_MESSAGE, INCLUDE_TYPESCRIPT_DEMO_MESSAGE, NAME_MESSAGE, @@ -74,7 +74,7 @@ function createTestInjector() { }); runInInjectionContext(testInjector, () => - registerCommand(createPluginCommandDefinition), + registerCommand(CreatePluginCommand), ); return testInjector; diff --git a/test/project-commands.ts b/test/project-commands.ts index 567d1294dc..a7b8d49379 100644 --- a/test/project-commands.ts +++ b/test/project-commands.ts @@ -1,6 +1,6 @@ import { Yok } from "../lib/common/yok"; import * as stubs from "./stubs"; -import { createProjectCommandDefinition } from "../lib/commands/create-project"; +import { CreateProjectCommand } from "../lib/commands/create-project"; import { registerCommand } from "../lib/common/services/command-definition-adapter"; import { StringCommandParameter } from "../lib/common/command-params"; import { setIsInteractive } from "../lib/common/helpers"; @@ -171,7 +171,7 @@ function createTestInjector() { template: undefined, }); runInInjectionContext(testInjector, () => - registerCommand(createProjectCommandDefinition), + registerCommand(CreateProjectCommand), ); testInjector.register("stringParameter", StringCommandParameter); testInjector.register("prompter", PrompterStub); diff --git a/test/tns-appstore-upload.ts b/test/tns-appstore-upload.ts index e6ca259ec3..4773ce8a8c 100644 --- a/test/tns-appstore-upload.ts +++ b/test/tns-appstore-upload.ts @@ -1,4 +1,4 @@ -import { publishIOSCommandDefinition } from "../lib/commands/appstore-upload"; +import { PublishIOSCommand } from "../lib/commands/appstore-upload"; import { registerCommand } from "../lib/common/services/command-definition-adapter"; import { Injector } from "../lib/common/di"; import { @@ -104,7 +104,7 @@ class AppStore { } runInInjectionContext((this.injector), () => - registerCommand({ ...publishIOSCommandDefinition, name: "appstore" }), + registerCommand({ ...PublishIOSCommand.definition, name: "appstore" }), ); this.injector.register("projectDataService", ProjectDataServiceStub); From 0b8d65bcd4e06a27ee572ced9f4544c981c62431 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 14 Sep 2026 19:37:21 -0300 Subject: [PATCH 08/17] refactor(commands): inline the simple command definitions A simple command is now one defineCommand call with its handlers written inline, where ctx is typed by inference: the exported setupX/runX/canExecuteX functions and the IXServices and XCommandContext aliases nothing else read are gone. Handlers inject what they use at their own top, before the first await. No command hands another a bag of services any more. injectPlatformCommandServices and the setupX bundles are deleted; the shared platform checks take the context and resolve through ctx.injector. A setup that survives is side-effect only - the eager initializeProjectData that has to land ahead of the arguments policy. --- lib/commands/apple-login.ts | 84 +++---- lib/commands/build.ts | 112 ++++----- lib/commands/clean.ts | 212 +++++++--------- lib/commands/command-base.ts | 110 +++----- lib/commands/config.ts | 82 +++--- lib/commands/debug.ts | 221 ++++++++--------- lib/commands/deploy.ts | 47 ++-- .../extensibility/install-extension.ts | 34 +-- lib/commands/extensibility/list-extensions.ts | 30 +-- .../extensibility/uninstall-extension.ts | 30 +-- lib/commands/fonts.ts | 44 ++-- lib/commands/generate-assets.ts | 46 ++-- lib/commands/generate-help.ts | 8 +- lib/commands/generate.ts | 13 +- lib/commands/hooks/common.ts | 55 ++-- lib/commands/hooks/hooks-lock.ts | 68 +++-- lib/commands/hooks/hooks.ts | 83 +++---- lib/commands/info.ts | 8 +- lib/commands/install.ts | 109 ++++---- lib/commands/list-platforms.ts | 57 ++--- lib/commands/migrate.ts | 46 ++-- lib/commands/native-add.ts | 168 ++++++------- lib/commands/open.ts | 145 ++++------- lib/commands/plugin/add-plugin.ts | 76 +++--- lib/commands/plugin/list-plugins.ts | 46 ++-- lib/commands/plugin/remove-plugin.ts | 89 +++---- lib/commands/plugin/update-plugin.ts | 107 ++++---- lib/commands/prepare.ts | 56 ++--- lib/commands/remove-platform.ts | 86 +++---- lib/commands/resources/resources-update.ts | 97 ++++---- lib/commands/run.ts | 234 +++++++++--------- lib/commands/setup.ts | 7 +- lib/commands/start.ts | 8 +- lib/commands/test.ts | 216 ++++++++-------- lib/commands/widget.ts | 36 ++- lib/common/commands/analytics.ts | 56 ++--- lib/common/commands/autocompletion.ts | 83 ++++--- .../commands/device/device-log-stream.ts | 82 +++--- lib/common/commands/device/get-file.ts | 93 +++---- .../commands/device/list-applications.ts | 73 +++--- lib/common/commands/device/list-files.ts | 89 +++---- lib/common/commands/device/put-file.ts | 93 +++---- lib/common/commands/device/run-application.ts | 71 ++---- .../commands/device/stop-application.ts | 53 ++-- .../commands/device/uninstall-application.ts | 43 +--- lib/common/commands/doctor.ts | 22 +- lib/common/commands/generate-messages.ts | 17 +- lib/common/commands/help.ts | 84 +++---- lib/common/commands/package-manager-get.ts | 27 +- lib/common/commands/package-manager-set.ts | 32 +-- lib/common/commands/post-install.ts | 9 +- lib/common/commands/preuninstall.ts | 55 ++-- lib/common/commands/proxy/proxy-base.ts | 25 +- lib/common/commands/proxy/proxy-clear.ts | 20 +- lib/common/commands/proxy/proxy-get.ts | 18 +- 55 files changed, 1642 insertions(+), 2273 deletions(-) diff --git a/lib/commands/apple-login.ts b/lib/commands/apple-login.ts index 5f87cea262..4002080743 100644 --- a/lib/commands/apple-login.ts +++ b/lib/commands/apple-login.ts @@ -1,59 +1,43 @@ import { IErrors } from "../common/declarations"; -import { CommandContext, defineCommand } from "../common/define-command"; +import { defineCommand } from "../common/define-command"; import { inject } from "../common/di"; import { IApplePortalSessionService } from "../services/apple-portal/definitions"; -export type AppleLoginCommandContext = CommandContext; - -export function setupAppleLoginCommand() { - return { - $applePortalSessionService: inject( - "applePortalSessionService", - ), - $errors: inject("errors"), - $logger: inject("logger"), - $prompter: inject("prompter"), - }; -} - -export type IAppleLoginCommandServices = ReturnType< - typeof setupAppleLoginCommand ->; - -export async function runAppleLoginCommand( - context: AppleLoginCommandContext, - services: IAppleLoginCommandServices, -): Promise { - let username = context.args[0]; - if (!username) { - username = await services.$prompter.getString("Apple ID", { - allowEmpty: false, - }); - } - - let password = context.args[1]; - if (!password) { - password = await services.$prompter.getPassword("Apple ID password"); - } - - const user = await services.$applePortalSessionService.createUserSession({ - username, - password, - }); - if (!user.areCredentialsValid) { - services.$errors.fail( - `Invalid username and password combination. Used '${username}' as the username.`, - ); - } - - const output = Buffer.from(user.userSessionCookie).toString("base64"); - services.$logger.info(output); -} - export const appleLoginCommandDefinition = defineCommand({ name: "apple-login", description: "Logs in to an Apple account and prints the session cookie.", arguments: [{ name: "appleId" }, { name: "password" }], - setup: setupAppleLoginCommand, - run: runAppleLoginCommand, + async run(context) { + const $applePortalSessionService = inject( + "applePortalSessionService", + ); + const $errors = inject("errors"); + const $logger = inject("logger"); + const $prompter = inject("prompter"); + + let username = context.args[0]; + if (!username) { + username = await $prompter.getString("Apple ID", { + allowEmpty: false, + }); + } + + let password = context.args[1]; + if (!password) { + password = await $prompter.getPassword("Apple ID password"); + } + + const user = await $applePortalSessionService.createUserSession({ + username, + password, + }); + if (!user.areCredentialsValid) { + $errors.fail( + `Invalid username and password combination. Used '${username}' as the username.`, + ); + } + + const output = Buffer.from(user.userSessionCookie).toString("base64"); + $logger.info(output); + }, }); diff --git a/lib/commands/build.ts b/lib/commands/build.ts index 29ae6f753e..59c25a5e5b 100644 --- a/lib/commands/build.ts +++ b/lib/commands/build.ts @@ -2,15 +2,16 @@ import { ANDROID_RELEASE_BUILD_ERROR_MESSAGE, AndroidAppBundleMessages, } from "../constants"; -import { - canExecuteCommandBase, - injectPlatformCommandServices, - validatePlatformOptions, -} from "./command-base"; +import { canExecuteCommandBase, validatePlatformOptions } from "./command-base"; import { hasValidAndroidSigning } from "../common/helpers"; -import { IAndroidBundleValidatorHelper } from "../declarations"; +import { + IAndroidBundleValidatorHelper, + IOptions, + IPlatformValidationService, +} from "../declarations"; import { IBuildController, IBuildDataService } from "../definitions/build"; import { IMigrateController } from "../definitions/migrate"; +import { IProjectData } from "../definitions/project"; import { IErrors } from "../common/declarations"; import { booleanOption, @@ -48,87 +49,82 @@ const defineBuildCommand = ( description: "Builds the project for the selected target platform.", options: buildCommandOptions, arguments: "none", - setup() { - const devicePlatformsConstants = inject( - "devicePlatformsConstants", + async canExecute(context): Promise { + const $devicePlatformsConstants = + inject("devicePlatformsConstants"); + const $errors = inject("errors"); + const $migrateController = + inject("migrateController"); + const $platformValidationService = inject( + "platformValidationService", ); - const platform = devicePlatformsConstants[buildPlatform]; - const isAndroid = devicePlatformsConstants.isAndroid(platform); - const services = { - ...injectPlatformCommandServices(), - platform, - isAndroid, - $errors: inject("errors"), - $logger: inject("logger"), - $buildController: inject("buildController"), - $buildDataService: inject("buildDataService"), - $migrateController: inject("migrateController"), - // Only the android build checks the runtime version. - $androidBundleValidatorHelper: isAndroid - ? inject( - "androidBundleValidatorHelper", - ) - : null, - }; - services.$projectData.initializeProjectData(); - - return services; - }, - async canExecute(context, services): Promise { - const { platform } = services; + const $projectData = inject("projectData"); + const platform = $devicePlatformsConstants[buildPlatform]; + const isAndroid = $devicePlatformsConstants.isAndroid(platform); + // Only the android build checks the runtime version. + const $androidBundleValidatorHelper = isAndroid + ? inject("androidBundleValidatorHelper") + : null; + $projectData.initializeProjectData(); if (!context.options.force) { - await services.$migrateController.validate({ - projectDir: services.$projectData.projectDir, + await $migrateController.validate({ + projectDir: $projectData.projectDir, platforms: [platform], }); } - if (services.isAndroid) { - services.$androidBundleValidatorHelper.validateRuntimeVersion( - services.$projectData, - ); + if (isAndroid) { + $androidBundleValidatorHelper.validateRuntimeVersion($projectData); } else if ( - !services.$platformValidationService.isPlatformSupportedForOS( + !$platformValidationService.isPlatformSupportedForOS( platform, - services.$projectData, + $projectData, ) ) { - services.$errors.fail( + $errors.fail( `Applications for platform ${platform} can not be built on this OS`, ); } - if (!(await canExecuteCommandBase(services, platform))) { + if (!(await canExecuteCommandBase(context, platform))) { return false; } if ( - services.isAndroid && + isAndroid && context.options.release && !hasValidAndroidSigning(context.options) ) { - services.$errors.failWithHelp(ANDROID_RELEASE_BUILD_ERROR_MESSAGE); + $errors.failWithHelp(ANDROID_RELEASE_BUILD_ERROR_MESSAGE); } - return validatePlatformOptions(services, platform); + return validatePlatformOptions(context, platform); }, - async run(context, services): Promise { - const buildData = services.$buildDataService.getBuildData( - services.$projectData.projectDir, - services.platform.toLowerCase(), - services.$options, + async run(context): Promise { + const $buildController = inject("buildController"); + const $buildDataService = inject("buildDataService"); + const $devicePlatformsConstants = + inject("devicePlatformsConstants"); + const $logger = inject("logger"); + const $options = inject("options"); + const $projectData = inject("projectData"); + const platform = $devicePlatformsConstants[buildPlatform]; + const isAndroid = $devicePlatformsConstants.isAndroid(platform); + $projectData.initializeProjectData(); + + const buildData = $buildDataService.getBuildData( + $projectData.projectDir, + platform.toLowerCase(), + $options, ); - const outputPath = - await services.$buildController.prepareAndBuild(buildData); + const outputPath = await $buildController.prepareAndBuild(buildData); - if (services.isAndroid && context.options.aab) { - services.$logger.info( - AndroidAppBundleMessages.ANDROID_APP_BUNDLE_DOCS_MESSAGE, - ); + if (isAndroid && context.options.aab) { + $logger.info(AndroidAppBundleMessages.ANDROID_APP_BUNDLE_DOCS_MESSAGE); if (context.options.release) { - services.$logger.info( + $logger.info( AndroidAppBundleMessages.ANDROID_APP_BUNDLE_PUBLISH_DOCS_MESSAGE, ); } diff --git a/lib/commands/clean.ts b/lib/commands/clean.ts index 28ccee4be9..c4ca161f33 100644 --- a/lib/commands/clean.ts +++ b/lib/commands/clean.ts @@ -86,32 +86,8 @@ const cleanCommandOptions = { json: booleanOption(), } satisfies CommandOptionsSchema; -export type CleanCommandContext = CommandContext; - -export function setupCleanCommand() { - return { - $childProcess: inject("childProcess"), - $logger: inject("logger"), - $projectCleanupService: inject( - "projectCleanupService", - ), - $projectConfigService: inject( - "projectConfigService", - ), - $projectData: inject("projectData"), - $projectService: inject("projectService"), - $prompter: inject("prompter"), - $staticConfig: inject("staticConfig"), - $terminalSpinnerService: inject( - "terminalSpinnerService", - ), - }; -} - -export type ICleanCommandServices = ReturnType; - async function getNSProjectPathsInDirectory( - services: ICleanCommandServices, + $logger: ILogger, dir = process.cwd(), ): Promise { let nsDirs: string[] = []; @@ -124,11 +100,7 @@ async function getNSProjectPathsInDirectory( const dirents = await readdir(dir, { withFileTypes: true }).catch( (err): any[] => { - services.$logger.trace( - 'Failed to read directory "%s". Error is:', - dir, - err, - ); + $logger.trace('Failed to read directory "%s". Error is:', dir, err); return []; }, ); @@ -162,17 +134,21 @@ async function getNSProjectPathsInDirectory( } async function cleanMultipleProjects( - context: CleanCommandContext, - services: ICleanCommandServices, + context: CommandContext, spinner: ITerminalSpinner, ) { + const $childProcess = context.injector.get("childProcess"); + const $logger = context.injector.get("logger"); + const $prompter = context.injector.get("prompter"); + const $staticConfig = context.injector.get("staticConfig"); + if (!isInteractive() || context.options.json) { // interactive terminal is required, and we can't output json in an interactive command. - services.$logger.warn("No project found in the current directory."); + $logger.warn("No project found in the current directory."); return; } - const shouldScan = await services.$prompter.confirm( + const shouldScan = await $prompter.confirm( "No project found in the current directory. Would you like to scan for all projects in sub-directories instead?", ); @@ -181,7 +157,7 @@ async function cleanMultipleProjects( } spinner.start("Scanning for projects... Please wait."); - const paths = await getNSProjectPathsInDirectory(services); + const paths = await getNSProjectPathsInDirectory($logger); spinner.succeed(`Found ${paths.length} projects.`); let computed = 0; @@ -200,9 +176,9 @@ async function cleanMultipleProjects( await promiseMap( paths, (p) => { - return services.$childProcess + return $childProcess .exec( - `node ${services.$staticConfig.cliBinPath} clean --dry-run --json --disable-analytics`, + `node ${$staticConfig.cliBinPath} clean --dry-run --json --disable-analytics`, { cwd: p, }, @@ -212,11 +188,7 @@ async function cleanMultipleProjects( return Object.values(paths).reduce((a, b) => a + b, 0); }) .catch((err) => { - services.$logger.trace( - "Failed to get project size for %s, Error is:", - p, - err, - ); + $logger.trace("Failed to get project size for %s, Error is:", p, err); return -1; }) .then((size) => { @@ -235,13 +207,13 @@ async function cleanMultipleProjects( spinner.clear(); spinner.stop(); - services.$logger.clearScreen(); + $logger.clearScreen(); const totalSize = Array.from(projects.values()) .filter((s) => s > 0) .reduce((a, b) => a + b, 0); - const pathsToClean = await services.$prompter.promptForChoice( + const pathsToClean = await $prompter.promptForChoice( `Found ${ projects.size } cleanable project(s) with a total size of: ${color.green( @@ -266,7 +238,7 @@ async function cleanMultipleProjects( optionsPerPage: process.stdout.rows - 6, // 6 lines are taken up by the instructions } as Partial, ); - services.$logger.clearScreen(); + $logger.clearScreen(); spinner.warn( `This will run "${color.yellow( @@ -278,7 +250,7 @@ async function cleanMultipleProjects( ); spinner.warn(`This action cannot be undone!`); - let confirmed = await services.$prompter.confirm( + let confirmed = await $prompter.confirm( "Are you sure you want to clean the selected projects?", ); if (!confirmed) { @@ -295,9 +267,9 @@ async function cleanMultipleProjects( `Cleaning ${color.cyan(currentPath)}... ${i + 1}/${pathsToClean.length}`, ); - const ok = await services.$childProcess + const ok = await $childProcess .exec( - `node ${services.$staticConfig.cliBinPath} clean ${ + `node ${$staticConfig.cliBinPath} clean ${ context.options.dryRun ? "--dry-run" : "" } --json --disable-analytics`, { @@ -309,11 +281,7 @@ async function cleanMultipleProjects( return cleanupRes.ok; }) .catch((err) => { - services.$logger.trace( - 'Failed to clean project "%s"', - currentPath, - err, - ); + $logger.trace('Failed to clean project "%s"', currentPath, err); return false; }); @@ -343,82 +311,88 @@ async function cleanMultipleProjects( } } -export async function runCleanCommand( - context: CleanCommandContext, - services: ICleanCommandServices, -): Promise { - const isDryRun = context.options.dryRun ?? false; - const isJSON = context.options.json ?? false; +export const cleanCommandDefinition = defineCommand({ + name: "clean", + description: "Cleans the project's build artefacts and dependencies.", + options: cleanCommandOptions, + arguments: "none", + async run(context): Promise { + const $projectCleanupService = inject( + "projectCleanupService", + ); + const $projectConfigService = inject( + "projectConfigService", + ); + const $projectData = inject("projectData"); + const $projectService = inject("projectService"); + const $terminalSpinnerService = inject( + "terminalSpinnerService", + ); - const spinner = services.$terminalSpinnerService.createSpinner({ - isSilent: isJSON, - }); + const isDryRun = context.options.dryRun ?? false; + const isJSON = context.options.json ?? false; - if (!services.$projectService.isValidNativeScriptProject()) { - return cleanMultipleProjects(context, services, spinner); - } + const spinner = $terminalSpinnerService.createSpinner({ + isSilent: isJSON, + }); - spinner.start("Cleaning project...\n"); + if (!$projectService.isValidNativeScriptProject()) { + return cleanMultipleProjects(context, spinner); + } - let pathsToClean = [ - constants.HOOKS_DIR_NAME, - services.$projectData.getBuildRelativeDirectoryPath(), - constants.NODE_MODULES_FOLDER_NAME, - ]; + spinner.start("Cleaning project...\n"); - try { - const overridePathsToClean = - services.$projectConfigService.getValue("cli.pathsToClean"); - const additionalPaths = services.$projectConfigService.getValue( - "cli.additionalPathsToClean", - ); + let pathsToClean = [ + constants.HOOKS_DIR_NAME, + $projectData.getBuildRelativeDirectoryPath(), + constants.NODE_MODULES_FOLDER_NAME, + ]; - // allow overriding default paths to clean - if (Array.isArray(overridePathsToClean)) { - pathsToClean = overridePathsToClean; - } + try { + const overridePathsToClean = + $projectConfigService.getValue("cli.pathsToClean"); + const additionalPaths = $projectConfigService.getValue( + "cli.additionalPathsToClean", + ); - if (Array.isArray(additionalPaths)) { - pathsToClean.push(...additionalPaths); + // allow overriding default paths to clean + if (Array.isArray(overridePathsToClean)) { + pathsToClean = overridePathsToClean; + } + + if (Array.isArray(additionalPaths)) { + pathsToClean.push(...additionalPaths); + } + } catch (err) { + // ignore } - } catch (err) { - // ignore - } - const res = await services.$projectCleanupService.clean(pathsToClean, { - dryRun: isDryRun, - silent: isJSON, - stats: isJSON, - }); + const res = await $projectCleanupService.clean(pathsToClean, { + dryRun: isDryRun, + silent: isJSON, + stats: isJSON, + }); - if (res.stats && isJSON) { - console.log( - JSON.stringify( - { - ok: res.ok, - dryRun: isDryRun, - stats: Object.fromEntries(res.stats.entries()), - }, - null, - 2, - ), - ); - - return; - } + if (res.stats && isJSON) { + console.log( + JSON.stringify( + { + ok: res.ok, + dryRun: isDryRun, + stats: Object.fromEntries(res.stats.entries()), + }, + null, + 2, + ), + ); - if (res.ok) { - spinner.succeed("Project successfully cleaned."); - } else { - spinner.fail(color.red("Project unsuccessfully cleaned.")); - } -} + return; + } -export const cleanCommandDefinition = defineCommand({ - name: "clean", - description: "Cleans the project's build artefacts and dependencies.", - options: cleanCommandOptions, - arguments: "none", - setup: setupCleanCommand, - run: runCleanCommand, + if (res.ok) { + spinner.succeed("Project successfully cleaned."); + } else { + spinner.fail(color.red("Project unsuccessfully cleaned.")); + } + }, }); diff --git a/lib/commands/command-base.ts b/lib/commands/command-base.ts index 48d0d2a9a6..e09dbfdf97 100644 --- a/lib/commands/command-base.ts +++ b/lib/commands/command-base.ts @@ -2,41 +2,20 @@ import { IProjectData, IValidatePlatformOutput } from "../definitions/project"; import { IOptions, IPlatformValidationService } from "../declarations"; import { IPlatformsDataService } from "../definitions/platform"; import { - ICommandParameter, ICanExecuteCommandOptions, INotConfiguredEnvOptions, } from "../common/definitions/commands"; -import { ArgumentSpec } from "../common/define-command"; -import { inject, Injector } from "../common/di"; +import { ArgumentSpec, CommandContext } from "../common/define-command"; +import { Injector } from "../common/di"; -/** Callable from `setup` and from `canExecute` before their first `await`. */ -export function injectPlatformCommandServices() { - return { - $options: inject("options"), - $platformsDataService: inject( - "platformsDataService", - ), - $platformValidationService: inject( - "platformValidationService", - ), - $projectData: inject("projectData"), - }; -} - -/** - * What the platform-validation helpers below need. A command definition's - * `setup` returns this shape (see `injectPlatformCommandServices`), so its - * result can be handed straight to them. - */ -export type IPlatformCommandServices = ReturnType< - typeof injectPlatformCommandServices ->; +/** The part of a command context these helpers read. */ +type PlatformCommandContext = Pick, "injector">; /** * The declarative form of `$platformCommandParameter`. Initializing the * project data is what makes the platform check possible, so it stays part of - * validating the argument instead of moving to `setup`, which the adapter runs - * only after argument enforcement. + * validating the argument instead of moving to the command's own handlers, + * which the adapter runs only after argument enforcement. */ export function validatePlatformArgument( targetInjector: Injector, @@ -59,33 +38,38 @@ export const platformArgument: ArgumentSpec = { }; export function validatePlatformOptions( - services: IPlatformCommandServices, + context: PlatformCommandContext, platform: string, ): Promise { - return services.$platformValidationService.validateOptions( - services.$options.provision, - services.$options.teamId, - services.$projectData, - platform, - ); + const $options = context.injector.get("options"); + const $projectData = context.injector.get("projectData"); + + return context.injector + .get("platformValidationService") + .validateOptions( + $options.provision, + $options.teamId, + $projectData, + platform, + ); } async function validatePlatformBase( - services: IPlatformCommandServices, + context: PlatformCommandContext, platform: string, notConfiguredEnvOptions: INotConfiguredEnvOptions, ): Promise { - const platformData = services.$platformsDataService.getPlatformData( - platform, - services.$projectData, - ); - const platformProjectService = platformData.platformProjectService; - const result = await platformProjectService.validate( - services.$projectData, - services.$options, + const $options = context.injector.get("options"); + const $projectData = context.injector.get("projectData"); + const platformData = context.injector + .get("platformsDataService") + .getPlatformData(platform, $projectData); + + return platformData.platformProjectService.validate( + $projectData, + $options, notConfiguredEnvOptions, ); - return result; } function hasUsableEnvironment( @@ -99,12 +83,12 @@ function hasUsableEnvironment( } export async function canExecuteCommandBase( - services: IPlatformCommandServices, + context: PlatformCommandContext, platform: string, options: ICanExecuteCommandOptions = {}, ): Promise { const validatePlatformOutput = await validatePlatformBase( - services, + context, platform, options.notConfiguredEnvOptions, ); @@ -112,40 +96,8 @@ export async function canExecuteCommandBase( let result = canExecute; if (canExecute && options.validateOptions) { - result = await validatePlatformOptions(services, platform); + result = await validatePlatformOptions(context, platform); } return result; } - -/** - * @deprecated Nothing extends this any more; the exported functions beside it carry - * the same behaviour for definitions. - */ -export abstract class ValidatePlatformCommandBase { - constructor( - protected $options: IOptions, - protected $platformsDataService: IPlatformsDataService, - protected $platformValidationService: IPlatformValidationService, - protected $projectData: IProjectData, - ) {} - - abstract allowedParameters: ICommandParameter[]; - abstract execute(args: string[]): Promise; - - public canExecuteCommandBase( - platform: string, - options?: ICanExecuteCommandOptions, - ): Promise { - return canExecuteCommandBase( - { - $options: this.$options, - $platformsDataService: this.$platformsDataService, - $platformValidationService: this.$platformValidationService, - $projectData: this.$projectData, - }, - platform, - options, - ); - } -} diff --git a/lib/commands/config.ts b/lib/commands/config.ts index 41792dce04..d2ff838d02 100644 --- a/lib/commands/config.ts +++ b/lib/commands/config.ts @@ -5,20 +5,6 @@ import { CommandContext, defineCommand } from "../common/define-command"; import { inject } from "../common/di"; import { color } from "../color"; -export function injectConfigCommandServices() { - return { - $projectConfigService: inject( - "projectConfigService", - ), - $logger: inject("logger"), - $errors: inject("errors"), - }; -} - -export type IConfigCommandServices = ReturnType< - typeof injectConfigCommandServices ->; - function getValueString(value: SupportedConfigValues, depth = 0): string { const indent = () => " ".repeat(depth); if (typeof value === "object") { @@ -50,12 +36,11 @@ function getConvertedValue(v: any): any { } } -export function requireConfigKey( - context: CommandContext, - services: IConfigCommandServices, -): void { +function requireConfigKey(context: CommandContext): void { if (!context.args[0]) { - services.$errors.failWithHelp("You must specify a key. Eg: ios.id"); + context.injector + .get("errors") + .failWithHelp("You must specify a key. Eg: ios.id"); } } @@ -63,13 +48,17 @@ export const configListCommandDefinition = defineCommand({ name: "config|*list", description: "Prints the project configuration.", arguments: "none", - setup: injectConfigCommandServices, - async run(context, services): Promise { + async run(): Promise { + const $projectConfigService = inject( + "projectConfigService", + ); + const $logger = inject("logger"); + try { - const config = services.$projectConfigService.readConfig(); - services.$logger.info(getValueString(config as SupportedConfigValues)); + const config = $projectConfigService.readConfig(); + $logger.info(getValueString(config as SupportedConfigValues)); } catch (error) { - services.$logger.info("Failed to read config. Error is: ", error); + $logger.info("Failed to read config. Error is: ", error); } }, }); @@ -78,17 +67,21 @@ export const configGetCommandDefinition = defineCommand({ name: "config|get", description: "Prints the value the project configuration holds for a key.", arguments: "any", - setup: injectConfigCommandServices, - async canExecute(context, services): Promise { - requireConfigKey(context, services); + async canExecute(context): Promise { + requireConfigKey(context); return true; }, - async run(context, services): Promise { + async run(context): Promise { + const $projectConfigService = inject( + "projectConfigService", + ); + const $logger = inject("logger"); + try { const [key] = context.args; - const current = services.$projectConfigService.getValue(key); - services.$logger.info(current); + const current = $projectConfigService.getValue(key); + $logger.info(current); } catch (err) { // ignore } @@ -99,21 +92,28 @@ export const configSetCommandDefinition = defineCommand({ name: "config|set", description: "Sets a value in the project configuration.", arguments: "any", - setup: injectConfigCommandServices, - async canExecute(context, services): Promise { - requireConfigKey(context, services); + async canExecute(context): Promise { + const $errors = inject("errors"); + + requireConfigKey(context); if (!context.args[1]) { - services.$errors.failWithHelp("You must specify a value."); + $errors.failWithHelp("You must specify a value."); } return true; }, - async run(context, services): Promise { + async run(context): Promise { + const $projectConfigService = inject( + "projectConfigService", + ); + const $logger = inject("logger"); + const $errors = inject("errors"); + const [key, value] = context.args; - const current = services.$projectConfigService.getValue(key); + const current = $projectConfigService.getValue(key); if (current && typeof current === "object") { - services.$errors.fail( + $errors.fail( `Unable to change object values. Please update individual values instead.\nEg: ns config set android.codeCache true`, ); } @@ -124,17 +124,17 @@ export const configSetCommandDefinition = defineCommand({ const currentDisplay = current ? color.yellow(current) : ""; const updatedDisplay = color.cyan(convertedValue); - services.$logger.info( + $logger.info( `${existingKey ? "Updating" : "Setting"} ${keyDisplay}${ existingKey ? ` from ${currentDisplay} ` : " " }to ${updatedDisplay}`, ); try { - await services.$projectConfigService.setValue(key, convertedValue); - services.$logger.info("Done"); + await $projectConfigService.setValue(key, convertedValue); + $logger.info("Done"); } catch (error) { - services.$logger.info("Could not update conifg. Error is: ", error); + $logger.info("Could not update conifg. Error is: ", error); } }, }); diff --git a/lib/commands/debug.ts b/lib/commands/debug.ts index 0cfe058dc4..aa57de1fd7 100644 --- a/lib/commands/debug.ts +++ b/lib/commands/debug.ts @@ -11,6 +11,7 @@ import { import { inject } from "../common/di"; import { hasValidAndroidSigning } from "../common/helpers"; import { ANDROID_APP_BUNDLE_SIGNING_ERROR_MESSAGE } from "../constants"; +import { IOptions, IPlatformValidationService } from "../declarations"; import { ICleanupService } from "../definitions/cleanup-service"; import { IDebugController, @@ -18,6 +19,7 @@ import { IDebugOptions, } from "../definitions/debug"; import { IMigrateController } from "../definitions/migrate"; +import { IProjectData } from "../definitions/project"; import { SystemWarningsSeverity } from "../definitions/system-warnings"; import { IKeyShortcutService, @@ -25,10 +27,7 @@ import { restartShortcut, watcherShortcut, } from "../services/key-shortcuts"; -import { - canExecuteCommandBase, - injectPlatformCommandServices, -} from "./command-base"; +import { canExecuteCommandBase } from "./command-base"; import * as _ from "lodash"; /** Which `$devicePlatformsConstants` entry a command debugs. */ @@ -50,98 +49,103 @@ const debugCommandOptions = { keyStoreAliasPassword: stringOption(), } satisfies CommandOptionsSchema; -export type DebugCommandContext = CommandContext; - -export function setupDebugCommand(debugPlatform: DebugPlatform) { - const $devicePlatformsConstants = inject( - "devicePlatformsConstants", - ); - - return { - ...injectPlatformCommandServices(), - platform: $devicePlatformsConstants[debugPlatform], - $cleanupService: inject("cleanupService"), - $debugController: inject("debugController"), - $debugDataService: inject("debugDataService"), - $devicePlatformsConstants, - $devicesService: inject("devicesService"), - $errors: inject("errors"), - $liveSyncCommandHelper: inject( - "liveSyncCommandHelper", - ), - $migrateController: inject("migrateController"), - }; -} - -export type IDebugCommandServices = ReturnType; +type DebugCommandContext = CommandContext; -export async function canExecuteDebugCommand( +async function canExecuteDebugCommand( context: DebugCommandContext, - services: IDebugCommandServices, + debugPlatform: DebugPlatform, ): Promise { + const $cleanupService = + context.injector.get("cleanupService"); + const $devicePlatformsConstants = + context.injector.get( + "devicePlatformsConstants", + ); + const $errors = context.injector.get("errors"); + const $migrateController = + context.injector.get("migrateController"); + const $platformValidationService = + context.injector.get( + "platformValidationService", + ); + const $projectData = context.injector.get("projectData"); + const platform = $devicePlatformsConstants[debugPlatform]; + // Keeping the cleanup process alive is what makes a debugger able to stay // attached, so it must not happen before the platform-specific checks that // run ahead of this function have had their chance to fail the command. - services.$cleanupService.setShouldDispose(false); + $cleanupService.setShouldDispose(false); if (!context.options.force) { - await services.$migrateController.validate({ - projectDir: services.$projectData.projectDir, - platforms: [services.platform], + await $migrateController.validate({ + projectDir: $projectData.projectDir, + platforms: [platform], }); } if ( - !services.$platformValidationService.isPlatformSupportedForOS( - services.platform, - services.$projectData, - ) + !$platformValidationService.isPlatformSupportedForOS(platform, $projectData) ) { - services.$errors.fail( - `Applications for platform ${services.platform} can not be built on this OS`, + $errors.fail( + `Applications for platform ${platform} can not be built on this OS`, ); } if (context.options.release) { - services.$errors.failWithHelp( - "--release flag is not applicable to this command.", - ); + $errors.failWithHelp("--release flag is not applicable to this command."); } - return canExecuteCommandBase(services, services.platform, { + return canExecuteCommandBase(context, platform, { validateOptions: true, }); } -export async function runDebugCommand( +async function runDebugCommand( context: DebugCommandContext, - services: IDebugCommandServices, + debugPlatform: DebugPlatform, ): Promise { - await services.$devicesService.initialize({ - platform: services.platform, + const $debugController = + context.injector.get("debugController"); + const $debugDataService = + context.injector.get("debugDataService"); + const $devicePlatformsConstants = + context.injector.get( + "devicePlatformsConstants", + ); + const $devicesService = + context.injector.get("devicesService"); + const $liveSyncCommandHelper = context.injector.get( + "liveSyncCommandHelper", + ); + const $options = context.injector.get("options"); + const $projectData = context.injector.get("projectData"); + const platform = $devicePlatformsConstants[debugPlatform]; + $projectData.initializeProjectData(); + + await $devicesService.initialize({ + platform, deviceId: context.options.device, emulator: context.options.emulator, skipDeviceDetectionInterval: true, }); - const selectedDeviceForDebug = - await services.$devicesService.pickSingleDevice({ - onlyEmulators: context.options.emulator, - onlyDevices: context.options.forDevice, - deviceId: context.options.device, - }); + const selectedDeviceForDebug = await $devicesService.pickSingleDevice({ + onlyEmulators: context.options.emulator, + onlyDevices: context.options.forDevice, + deviceId: context.options.device, + }); if (context.options.start) { // The debug services read the whole parsed command line, including flags // no command declares, so the raw argv is what they get. - const debugOptions = _.cloneDeep(services.$options.argv); - const debugData = services.$debugDataService.getDebugData( + const debugOptions = _.cloneDeep($options.argv); + const debugData = $debugDataService.getDebugData( selectedDeviceForDebug.deviceInfo.identifier, - services.$projectData, + $projectData, debugOptions, ); - await services.$debugController.printDebugInformation( - await services.$debugController.startDebug(debugData), + await $debugController.printDebugInformation( + await $debugController.startDebug(debugData), ); return; } @@ -157,9 +161,9 @@ export async function runDebugCommand( ...additional, }); - await services.$liveSyncCommandHelper.executeLiveSyncOperation( + await $liveSyncCommandHelper.executeLiveSyncOperation( [selectedDeviceForDebug], - services.platform, + platform, liveSyncOptions({}), ); @@ -174,9 +178,9 @@ export async function runDebugCommand( const restartDebugSession = ( forceRebuildNativeApp: boolean = false, ): Promise => - services.$liveSyncCommandHelper.executeLiveSyncOperation( + $liveSyncCommandHelper.executeLiveSyncOperation( [selectedDeviceForDebug], - services.platform, + platform, liveSyncOptions(>{ restartLiveSync: true, ...(forceRebuildNativeApp ? { forceRebuildNativeApp: true } : {}), @@ -200,30 +204,6 @@ export async function runDebugCommand( } } -const setupDebugApplePlatformCommand = - (debugPlatform: "iOS" | "visionOS") => () => { - const services = { - ...setupDebugCommand(debugPlatform), - $sysInfo: inject("sysInfo"), - }; - services.$projectData.initializeProjectData(); - - // Do not dispose ios-device-lib, so the process will remain alive and the debug application (NativeScript Inspector or Chrome DevTools) will be able to connect to the socket. - // In case we dispose ios-device-lib, the socket will be closed and the code will fail when the debug application tries to read/send data to device socket. - // That's why the `$ ns debug ios --justlaunch` command will not release the terminal. - // In case we do not set it to false, the dispose will be called once the command finishes its execution, which will prevent the debugging. - inject("iosDeviceOperations").setShouldDispose(false); - inject( - "iOSSimulatorLogProvider", - ).setShouldDispose(false); - - return services; - }; - -type IDebugApplePlatformCommandServices = ReturnType< - ReturnType ->; - function isValidTimeoutOption(timeout: string): boolean { if (!timeout) { return true; @@ -252,43 +232,61 @@ const defineApplePlatformDebugCommand = ( options: debugCommandOptions, // Arguments have never been rejected here, only ignored. arguments: "any", - setup: setupDebugApplePlatformCommand(debugPlatform), - async canExecute( - context: DebugCommandContext, - services: IDebugApplePlatformCommandServices, - ): Promise { + async canExecute(context): Promise { + const $devicePlatformsConstants = + inject("devicePlatformsConstants"); + const $errors = inject("errors"); + const $platformValidationService = inject( + "platformValidationService", + ); + const $projectData = inject("projectData"); + const $sysInfo = inject("sysInfo"); + const platform = $devicePlatformsConstants[debugPlatform]; + $projectData.initializeProjectData(); + + // Do not dispose ios-device-lib, so the process will remain alive and the debug application (NativeScript Inspector or Chrome DevTools) will be able to connect to the socket. + // In case we dispose ios-device-lib, the socket will be closed and the code will fail when the debug application tries to read/send data to device socket. + // That's why the `$ ns debug ios --justlaunch` command will not release the terminal. + // In case we do not set it to false, the dispose will be called once the command finishes its execution, which will prevent the debugging. + inject("iosDeviceOperations").setShouldDispose( + false, + ); + inject( + "iOSSimulatorLogProvider", + ).setShouldDispose(false); + if ( - !services.$platformValidationService.isPlatformSupportedForOS( - services.platform, - services.$projectData, + !$platformValidationService.isPlatformSupportedForOS( + platform, + $projectData, ) ) { - services.$errors.fail( - `Applications for platform ${services.platform} can not be built on this OS`, + $errors.fail( + `Applications for platform ${platform} can not be built on this OS`, ); } if (!isValidTimeoutOption(context.options.timeout)) { - services.$errors.fail( + $errors.fail( `Timeout option specifies the seconds NativeScript CLI will wait to find the inspector socket port from device's logs. Must be a number.`, ); } if (context.options.inspector) { - const macOSWarning = await services.$sysInfo.getMacOSWarningMessage(); + const macOSWarning = await $sysInfo.getMacOSWarningMessage(); if ( macOSWarning && macOSWarning.severity === SystemWarningsSeverity.high ) { - services.$errors.fail( + $errors.fail( `You cannot use NativeScript Inspector on this OS. To use it, please update your OS.`, ); } } - return canExecuteDebugCommand(context, services); + return canExecuteDebugCommand(context, debugPlatform); }, - run: runDebugCommand, + run: (context) => runDebugCommand(context, debugPlatform), }); export const iosDebugCommand = defineApplePlatformDebugCommand( @@ -306,24 +304,19 @@ export const androidDebugCommand = defineCommand({ description: "Debugs your project on a connected Android device or emulator.", options: debugCommandOptions, arguments: "any", - setup(): IDebugCommandServices { - const services = setupDebugCommand("Android"); - services.$projectData.initializeProjectData(); + async canExecute(context): Promise { + const $errors = inject("errors"); + const $projectData = inject("projectData"); + $projectData.initializeProjectData(); - return services; - }, - async canExecute( - context: DebugCommandContext, - services: IDebugCommandServices, - ): Promise { - const canExecuteBase = await canExecuteDebugCommand(context, services); + const canExecuteBase = await canExecuteDebugCommand(context, "Android"); if (canExecuteBase) { if (context.options.aab && !hasValidAndroidSigning(context.options)) { - services.$errors.failWithHelp(ANDROID_APP_BUNDLE_SIGNING_ERROR_MESSAGE); + $errors.failWithHelp(ANDROID_APP_BUNDLE_SIGNING_ERROR_MESSAGE); } } return canExecuteBase; }, - run: runDebugCommand, + run: (context) => runDebugCommand(context, "Android"), }); diff --git a/lib/commands/deploy.ts b/lib/commands/deploy.ts index d96166600f..acda8ab0ef 100644 --- a/lib/commands/deploy.ts +++ b/lib/commands/deploy.ts @@ -2,14 +2,11 @@ import { ANDROID_RELEASE_BUILD_ERROR_MESSAGE, ANDROID_APP_BUNDLE_SIGNING_ERROR_MESSAGE, } from "../constants"; -import { - canExecuteCommandBase, - injectPlatformCommandServices, - platformArgument, -} from "./command-base"; +import { canExecuteCommandBase, platformArgument } from "./command-base"; import { DeployCommandHelper } from "../helpers/deploy-command-helper"; import { hasValidAndroidSigning } from "../common/helpers"; import { IMigrateController } from "../definitions/migrate"; +import { IProjectData } from "../definitions/project"; import { IErrors } from "../common/declarations"; import { booleanOption, @@ -36,24 +33,18 @@ export const deployCommandDefinition = defineCommand({ description: "Builds and deploys the project to a connected device.", options: deployCommandOptions, arguments: [platformArgument], - setup() { - const services = { - ...injectPlatformCommandServices(), - $errors: inject("errors"), - $mobileHelper: inject("mobileHelper"), - $deployCommandHelper: inject("deployCommandHelper"), - $migrateController: inject("migrateController"), - }; - services.$projectData.initializeProjectData(); + async canExecute(context): Promise { + const $errors = inject("errors"); + const $migrateController = inject("migrateController"); + const $mobileHelper = inject("mobileHelper"); + const $projectData = inject("projectData"); + $projectData.initializeProjectData(); - return services; - }, - async canExecute(context, services): Promise { const platform = context.args[0]; if (!context.options.force) { - await services.$migrateController.validate({ - projectDir: services.$projectData.projectDir, + await $migrateController.validate({ + projectDir: $projectData.projectDir, platforms: [platform], }); } @@ -63,22 +54,28 @@ export const deployCommandDefinition = defineCommand({ } if ( - services.$mobileHelper.isAndroidPlatform(platform) && + $mobileHelper.isAndroidPlatform(platform) && (context.options.release || context.options.aab) && !hasValidAndroidSigning(context.options) ) { if (context.options.release) { - services.$errors.failWithHelp(ANDROID_RELEASE_BUILD_ERROR_MESSAGE); + $errors.failWithHelp(ANDROID_RELEASE_BUILD_ERROR_MESSAGE); } else { - services.$errors.failWithHelp(ANDROID_APP_BUNDLE_SIGNING_ERROR_MESSAGE); + $errors.failWithHelp(ANDROID_APP_BUNDLE_SIGNING_ERROR_MESSAGE); } } - return canExecuteCommandBase(services, platform, { + return canExecuteCommandBase(context, platform, { validateOptions: true, }); }, - async run(context, services): Promise { - await services.$deployCommandHelper.deploy(context.args[0]); + async run(context): Promise { + const $deployCommandHelper = inject( + "deployCommandHelper", + ); + const $projectData = inject("projectData"); + $projectData.initializeProjectData(); + + await $deployCommandHelper.deploy(context.args[0]); }, }); diff --git a/lib/commands/extensibility/install-extension.ts b/lib/commands/extensibility/install-extension.ts index 6133e4e5de..20483bf22e 100644 --- a/lib/commands/extensibility/install-extension.ts +++ b/lib/commands/extensibility/install-extension.ts @@ -2,19 +2,6 @@ import { defineCommand } from "../../common/define-command"; import { inject } from "../../common/di"; import { IExtensibilityService } from "../../common/definitions/extensibility"; -export function setupInstallExtensionCommand() { - return { - $extensibilityService: inject( - "extensibilityService", - ), - $logger: inject("logger"), - }; -} - -export type IInstallExtensionCommandServices = ReturnType< - typeof setupInstallExtensionCommand ->; - export const installExtensionCommandDefinition = defineCommand({ name: "extension|install", description: "Installs the specified extension.", @@ -26,22 +13,21 @@ export const installExtensionCommandDefinition = defineCommand({ "You have to provide a valid name for extension that you want to install.", }, ], - setup: setupInstallExtensionCommand, - async run( - context, - services: IInstallExtensionCommandServices, - ): Promise { - const extensionData = await services.$extensibilityService.installExtension( + async run(context): Promise { + const $extensibilityService = inject( + "extensibilityService", + ); + const $logger = inject("logger"); + + const extensionData = await $extensibilityService.installExtension( context.args[0], ); - services.$logger.info( + $logger.info( `Successfully installed extension ${extensionData.extensionName}.`, ); - await services.$extensibilityService.loadExtension( - extensionData.extensionName, - ); - services.$logger.info( + await $extensibilityService.loadExtension(extensionData.extensionName); + $logger.info( `Successfully loaded extension ${extensionData.extensionName}.`, ); }, diff --git a/lib/commands/extensibility/list-extensions.ts b/lib/commands/extensibility/list-extensions.ts index 9dec66c7c1..35d7ab81c8 100644 --- a/lib/commands/extensibility/list-extensions.ts +++ b/lib/commands/extensibility/list-extensions.ts @@ -4,36 +4,26 @@ import { inject } from "../../common/di"; import { IExtensibilityService } from "../../common/definitions/extensibility"; import * as helpers from "../../common/helpers"; -export function setupListExtensionsCommand() { - return { - $extensibilityService: inject( - "extensibilityService", - ), - $logger: inject("logger"), - }; -} - -export type IListExtensionsCommandServices = ReturnType< - typeof setupListExtensionsCommand ->; - export const listExtensionsCommandDefinition = defineCommand({ name: "extension|*list", description: "Lists all installed extensions.", - setup: setupListExtensionsCommand, - run(context, services: IListExtensionsCommandServices): void { - const installedExtensions = - services.$extensibilityService.getInstalledExtensions(); + run(): void { + const $extensibilityService = inject( + "extensibilityService", + ); + const $logger = inject("logger"); + + const installedExtensions = $extensibilityService.getInstalledExtensions(); if (_.keys(installedExtensions).length) { - services.$logger.info("Installed extensions:"); + $logger.info("Installed extensions:"); const data = _.map(installedExtensions, (version, name) => { return [name, version]; }); const table = helpers.createTable(["Name", "Version"], data); - services.$logger.info(table.toString()); + $logger.info(table.toString()); } else { - services.$logger.info("No extensions installed."); + $logger.info("No extensions installed."); } }, }); diff --git a/lib/commands/extensibility/uninstall-extension.ts b/lib/commands/extensibility/uninstall-extension.ts index 1701865848..369de3ad07 100644 --- a/lib/commands/extensibility/uninstall-extension.ts +++ b/lib/commands/extensibility/uninstall-extension.ts @@ -2,19 +2,6 @@ import { defineCommand } from "../../common/define-command"; import { inject } from "../../common/di"; import { IExtensibilityService } from "../../common/definitions/extensibility"; -export function setupUninstallExtensionCommand() { - return { - $extensibilityService: inject( - "extensibilityService", - ), - $logger: inject("logger"), - }; -} - -export type IUninstallExtensionCommandServices = ReturnType< - typeof setupUninstallExtensionCommand ->; - export const uninstallExtensionCommandDefinition = defineCommand({ name: "extension|uninstall", description: "Uninstalls the specified extension.", @@ -26,15 +13,14 @@ export const uninstallExtensionCommandDefinition = defineCommand({ "You have to provide a valid name for extension that you want to uninstall.", }, ], - setup: setupUninstallExtensionCommand, - async run( - context, - services: IUninstallExtensionCommandServices, - ): Promise { - const extensionName = context.args[0]; - await services.$extensibilityService.uninstallExtension(extensionName); - services.$logger.info( - `Successfully uninstalled extension ${extensionName}`, + async run(context): Promise { + const $extensibilityService = inject( + "extensibilityService", ); + const $logger = inject("logger"); + + const extensionName = context.args[0]; + await $extensibilityService.uninstallExtension(extensionName); + $logger.info(`Successfully uninstalled extension ${extensionName}`); }, }); diff --git a/lib/commands/fonts.ts b/lib/commands/fonts.ts index edfed4dc55..289f899ba5 100644 --- a/lib/commands/fonts.ts +++ b/lib/commands/fonts.ts @@ -7,49 +7,43 @@ import * as fontFinder from "font-finder"; import { createTable } from "../common/helpers"; import * as path from "path"; -export function setupFontsCommand() { - const services = { - $projectData: inject("projectData"), - $fs: inject("fs"), - $logger: inject("logger"), - $projectConfigService: inject( - "projectConfigService", - ), - }; - services.$projectData.initializeProjectData(); - - return services; -} - -export type IFontsCommandServices = ReturnType; - export const fontsCommandDefinition = defineCommand({ name: "fonts", description: "Lists the custom fonts the project bundles.", arguments: "none", - setup: setupFontsCommand, - async run(context, services): Promise { + // In setup, not run: it lands ahead of the arguments policy, so being + // outside a project is what a bad invocation reports first. + setup(): void { + inject("projectData").initializeProjectData(); + }, + async run(): Promise { + const $projectData = inject("projectData"); + const $fs = inject("fs"); + const $logger = inject("logger"); + const $projectConfigService = inject( + "projectConfigService", + ); const supportedExtensions = [".ttf", ".otf"]; const defaultFontsFolderPaths = [ path.join( - services.$projectConfigService.getValue("appPath") ?? "", + $projectConfigService.getValue("appPath") ?? "", constants.FONTS_DIR, ), path.join(constants.APP_FOLDER_NAME, constants.FONTS_DIR), path.join(constants.SRC_DIR, constants.FONTS_DIR), - ].map((entry) => path.resolve(services.$projectData.projectDir, entry)); + ].map((entry) => path.resolve($projectData.projectDir, entry)); const fontsFolderPath = defaultFontsFolderPaths.find((entry) => - services.$fs.exists(entry), + $fs.exists(entry), ); if (!fontsFolderPath) { - services.$logger.warn("No fonts folder found."); + $logger.warn("No fonts folder found."); return; } - const files = services.$fs + const files = $fs .readDirectory(fontsFolderPath) .map((entry) => path.parse(entry)) .filter((entry) => { @@ -57,7 +51,7 @@ export const fontsCommandDefinition = defineCommand({ }); if (!files.length) { - services.$logger.warn("No custom fonts found."); + $logger.warn("No custom fonts found."); return; } @@ -71,6 +65,6 @@ export const fontsCommandDefinition = defineCommand({ ]); } - services.$logger.info(table.toString()); + $logger.info(table.toString()); }, }); diff --git a/lib/commands/generate-assets.ts b/lib/commands/generate-assets.ts index 559f5911db..c7ce072d81 100644 --- a/lib/commands/generate-assets.ts +++ b/lib/commands/generate-assets.ts @@ -5,12 +5,12 @@ import { defineCommand, stringOption, } from "../common/define-command"; -import { inject } from "../common/di"; import { IAssetsGenerationService, IResourceGenerationData, } from "../declarations"; import { IProjectData } from "../definitions/project"; +import { inject } from "../common/di"; /** Which set of assets a command generates from the source image. */ type GeneratedAssets = "icons" | "splashes"; @@ -26,39 +26,21 @@ const generators: Record< splashes: (service, data) => service.generateSplashScreens(data), }; -export const generateAssetsCommandOptions = { +const generateAssetsCommandOptions = { background: stringOption(), } satisfies CommandOptionsSchema; -export type GenerateAssetsCommandContext = CommandContext< - typeof generateAssetsCommandOptions ->; - -export function setupGenerateAssetsCommand(assets: GeneratedAssets) { - const services = { - assets, - $assetsGenerationService: inject( - "assetsGenerationService", - ), - $projectData: inject("projectData"), - }; - services.$projectData.initializeProjectData(); - - return services; -} - -export type IGenerateAssetsCommandServices = ReturnType< - typeof setupGenerateAssetsCommand ->; - -export function runGenerateAssetsCommand( - context: GenerateAssetsCommandContext, - services: IGenerateAssetsCommandServices, +function runGenerateAssetsCommand( + context: CommandContext, + assets: GeneratedAssets, ): Promise { - return generators[services.assets](services.$assetsGenerationService, { + const $assetsGenerationService = + context.injector.get("assetsGenerationService"); + const $projectData = context.injector.get("projectData"); + return generators[assets]($assetsGenerationService, { imagePath: context.args[0], background: context.options.background, - projectDir: services.$projectData.projectDir, + projectDir: $projectData.projectDir, }); } @@ -79,8 +61,12 @@ const defineGenerateAssetsCommand = ( "You have to provide path to image to generate other images based on it.", }, ], - setup: () => setupGenerateAssetsCommand(assets), - run: runGenerateAssetsCommand, + // In setup, not run: it lands ahead of the arguments policy, so being + // outside a project is what a missing image path reports first. + setup(): void { + inject("projectData").initializeProjectData(); + }, + run: (context) => runGenerateAssetsCommand(context, assets), }); export const generateIconsCommand = defineGenerateAssetsCommand( diff --git a/lib/commands/generate-help.ts b/lib/commands/generate-help.ts index ca0c64ae79..476b185388 100644 --- a/lib/commands/generate-help.ts +++ b/lib/commands/generate-help.ts @@ -6,10 +6,8 @@ export const generateHelpCommandDefinition = defineCommand({ name: "dev-generate-help", description: "Generates the HTML help pages from the man pages.", arguments: "none", - setup: () => ({ - $helpService: inject("helpService"), - }), - run(context, services): Promise { - return services.$helpService.generateHtmlPages(); + run(): Promise { + const $helpService = inject("helpService"); + return $helpService.generateHtmlPages(); }, }); diff --git a/lib/commands/generate.ts b/lib/commands/generate.ts index 29caa6ece4..70c173a8ef 100644 --- a/lib/commands/generate.ts +++ b/lib/commands/generate.ts @@ -7,18 +7,17 @@ export const generateCommandDefinition = defineCommand({ name: "generate", description: "Executes a schematic in the project.", arguments: "any", - setup: () => ({ - $logger: inject("logger"), - $errors: inject("errors"), - }), - async run(context, services): Promise { + async run(): Promise { + const $logger = inject("logger"); + const $errors = inject("errors"); + try { - services.$logger.info( + $logger.info( "If you have ideas for this command, please discuss at https://nativescript.org/discord", ); // await run(this.executionOptions); } catch (error) { - services.$errors.fail(error.message); + $errors.fail(error.message); } }, }); diff --git a/lib/commands/hooks/common.ts b/lib/commands/hooks/common.ts index 8cfef75424..51d9e00cbd 100644 --- a/lib/commands/hooks/common.ts +++ b/lib/commands/hooks/common.ts @@ -1,7 +1,6 @@ -import { IProjectData } from "../../definitions/project"; -import { IPluginData, IPluginsService } from "../../definitions/plugins"; +import { IPluginData } from "../../definitions/plugins"; import { IErrors, IFileSystem } from "../../common/declarations"; -import { inject } from "../../common/di"; +import { CommandContext } from "../../common/define-command"; import path = require("path"); import * as crypto from "crypto"; @@ -16,24 +15,6 @@ export interface OutputPlugin { hooks: OutputHook[]; } -/** Callable from `setup` and from `canExecute` before their first `await`. */ -export function injectHooksCommandServices() { - const services = { - $pluginsService: inject("pluginsService"), - $projectData: inject("projectData"), - $errors: inject("errors"), - $fs: inject("fs"), - $logger: inject("logger"), - }; - services.$projectData.initializeProjectData(); - - return services; -} - -export type IHooksCommandServices = ReturnType< - typeof injectHooksCommandServices ->; - export function getPluginsWithHooks(plugins: IPluginData[]): IPluginData[] { const pluginsWithHooks: IPluginData[] = []; for (const plugin of plugins) { @@ -46,18 +27,22 @@ export function getPluginsWithHooks(plugins: IPluginData[]): IPluginData[] { } export async function verifyHooksLock( - services: IHooksCommandServices, + context: CommandContext, plugins: IPluginData[], hooksLockPath: string, ): Promise { + const $errors = context.injector.get("errors"); + const $fs = context.injector.get("fs"); + const $logger = context.injector.get("logger"); + let lockFileContent: string; let hooksLock: OutputPlugin[]; try { - lockFileContent = services.$fs.readText(hooksLockPath, "utf8"); + lockFileContent = $fs.readText(hooksLockPath, "utf8"); hooksLock = JSON.parse(lockFileContent); } catch (err) { - services.$errors.fail( + $errors.fail( `❌ Failed to read or parse ${LOCK_FILE_NAME} at ${hooksLockPath}`, ); } @@ -78,7 +63,7 @@ export async function verifyHooksLock( const pluginLockHooks = lockMap.get(plugin.name); if (!pluginLockHooks) { - services.$logger.error( + $logger.error( `❌ Plugin '${plugin.name}' not found in ${LOCK_FILE_NAME}`, ); isValid = false; @@ -89,7 +74,7 @@ export async function verifyHooksLock( const expectedHash = pluginLockHooks.get(hook.type); if (!expectedHash) { - services.$logger.error( + $logger.error( `❌ Missing hook '${hook.type}' for plugin '${plugin.name}' in ${LOCK_FILE_NAME}`, ); isValid = false; @@ -99,11 +84,9 @@ export async function verifyHooksLock( let fileContent: string | Buffer; try { - fileContent = services.$fs.readFile( - path.join(plugin.fullPath, hook.script), - ); + fileContent = $fs.readFile(path.join(plugin.fullPath, hook.script)); } catch (err) { - services.$logger.error( + $logger.error( `❌ Cannot read script file '${hook.script}' for hook '${hook.type}' in plugin '${plugin.name}'`, ); isValid = false; @@ -116,21 +99,19 @@ export async function verifyHooksLock( .digest("hex"); if (actualHash !== expectedHash) { - services.$logger.error( + $logger.error( `❌ Hash mismatch for '${hook.script}' (${hook.type} in ${plugin.name}):`, ); - services.$logger.error(` Expected: ${expectedHash}`); - services.$logger.error(` Actual: ${actualHash}`); + $logger.error(` Expected: ${expectedHash}`); + $logger.error(` Actual: ${actualHash}`); isValid = false; } } } if (isValid) { - services.$logger.info( - "✅ All hooks verified successfully. No issues found.", - ); + $logger.info("✅ All hooks verified successfully. No issues found."); } else { - services.$errors.fail("❌ One or more hooks failed verification."); + $errors.fail("❌ One or more hooks failed verification."); } } diff --git a/lib/commands/hooks/hooks-lock.ts b/lib/commands/hooks/hooks-lock.ts index 18142e8a92..7272aec725 100644 --- a/lib/commands/hooks/hooks-lock.ts +++ b/lib/commands/hooks/hooks-lock.ts @@ -1,11 +1,12 @@ -import { IPluginData } from "../../definitions/plugins"; -import { defineCommand } from "../../common/define-command"; +import { IProjectData } from "../../definitions/project"; +import { IPluginData, IPluginsService } from "../../definitions/plugins"; +import { IErrors, IFileSystem } from "../../common/declarations"; +import { CommandContext, defineCommand } from "../../common/define-command"; +import { inject } from "../../common/di"; import path = require("path"); import * as crypto from "crypto"; import { getPluginsWithHooks, - IHooksCommandServices, - injectHooksCommandServices, LOCK_FILE_NAME, OutputHook, OutputPlugin, @@ -13,10 +14,13 @@ import { } from "./common"; async function writeHooksLockFile( - services: IHooksCommandServices, + context: CommandContext, plugins: IPluginData[], outputDir: string, ): Promise { + const $errors = context.injector.get("errors"); + const $fs = context.injector.get("fs"); + const $logger = context.injector.get("logger"); const output: OutputPlugin[] = []; for (const plugin of plugins) { @@ -24,7 +28,7 @@ async function writeHooksLockFile( for (const hook of plugin.nativescript?.hooks || []) { try { - const fileContent = services.$fs.readFile( + const fileContent = $fs.readFile( path.join(plugin.fullPath, hook.script), ); const hash = crypto @@ -37,7 +41,7 @@ async function writeHooksLockFile( hash, }); } catch (err) { - services.$logger.warn( + $logger.warn( `Warning: Failed to read script '${hook.script}' for plugin '${plugin.name}'. Skipping this hook.`, ); continue; @@ -50,10 +54,10 @@ async function writeHooksLockFile( const filePath = path.resolve(outputDir, LOCK_FILE_NAME); try { - services.$fs.writeFile(filePath, JSON.stringify(output, null, 2), "utf8"); - services.$logger.info(`✅ ${LOCK_FILE_NAME} written to: ${filePath}`); + $fs.writeFile(filePath, JSON.stringify(output, null, 2), "utf8"); + $logger.info(`✅ ${LOCK_FILE_NAME} written to: ${filePath}`); } catch (err) { - services.$errors.fail(`❌ Failed to write ${LOCK_FILE_NAME}: ${err}`); + $errors.fail(`❌ Failed to write ${LOCK_FILE_NAME}: ${err}`); } } @@ -62,20 +66,26 @@ export const hooksLockCommandDefinition = defineCommand({ description: "Records a hash of every plugin hook in the project's lock file.", arguments: "any", - setup: injectHooksCommandServices, - async run(context, services): Promise { + // In setup, not run: it lands ahead of the arguments policy, so being + // outside a project is what a bad invocation reports first. + setup(): void { + inject("projectData").initializeProjectData(); + }, + async run(context): Promise { + const $pluginsService = inject("pluginsService"); + const $projectData = inject("projectData"); + const $logger = inject("logger"); + const plugins: IPluginData[] = - await services.$pluginsService.getAllInstalledPlugins( - services.$projectData, - ); + await $pluginsService.getAllInstalledPlugins($projectData); if (plugins && plugins.length > 0) { await writeHooksLockFile( - services, + context, getPluginsWithHooks(plugins), - services.$projectData.projectDir, + $projectData.projectDir, ); } else { - services.$logger.info("No plugins with hooks found."); + $logger.info("No plugins with hooks found."); } }, }); @@ -85,20 +95,26 @@ export const hooksVerifyCommandDefinition = defineCommand({ description: "Checks every plugin hook against the hashes in the project's lock file.", arguments: "any", - setup: injectHooksCommandServices, - async run(context, services): Promise { + // In setup, not run: it lands ahead of the arguments policy, so being + // outside a project is what a bad invocation reports first. + setup(): void { + inject("projectData").initializeProjectData(); + }, + async run(context): Promise { + const $pluginsService = inject("pluginsService"); + const $projectData = inject("projectData"); + const $logger = inject("logger"); + const plugins: IPluginData[] = - await services.$pluginsService.getAllInstalledPlugins( - services.$projectData, - ); + await $pluginsService.getAllInstalledPlugins($projectData); if (plugins && plugins.length > 0) { await verifyHooksLock( - services, + context, getPluginsWithHooks(plugins), - path.join(services.$projectData.projectDir, LOCK_FILE_NAME), + path.join($projectData.projectDir, LOCK_FILE_NAME), ); } else { - services.$logger.info("No plugins with hooks found."); + $logger.info("No plugins with hooks found."); } }, }); diff --git a/lib/commands/hooks/hooks.ts b/lib/commands/hooks/hooks.ts index afe451f6d9..313f9c8153 100644 --- a/lib/commands/hooks/hooks.ts +++ b/lib/commands/hooks/hooks.ts @@ -1,21 +1,15 @@ -import { IPluginData } from "../../definitions/plugins"; +import { IProjectData } from "../../definitions/project"; +import { IPluginData, IPluginsService } from "../../definitions/plugins"; +import { IErrors, IFileSystem } from "../../common/declarations"; import { CommandContext, defineCommand } from "../../common/define-command"; +import { inject } from "../../common/di"; import path = require("path"); import { HOOKS_DIR_NAME } from "../../constants"; import { createTable } from "../../common/helpers"; import nsHooks = require("@nativescript/hook"); -import { - getPluginsWithHooks, - IHooksCommandServices, - injectHooksCommandServices, - LOCK_FILE_NAME, - verifyHooksLock, -} from "./common"; +import { getPluginsWithHooks, LOCK_FILE_NAME, verifyHooksLock } from "./common"; -function listHooks( - services: IHooksCommandServices, - pluginsWithHooks: IPluginData[], -): void { +function listHooks($logger: ILogger, pluginsWithHooks: IPluginData[]): void { const headers: string[] = ["Plugin", "HookName", "HookPath"]; const hookDataData: string[][] = pluginsWithHooks.flatMap((plugin) => plugin.nativescript.hooks.map((hook: { type: string; script: string }) => { @@ -23,31 +17,26 @@ function listHooks( }), ); const hookDataTable: any = createTable(headers, hookDataData); - services.$logger.info("Hooks:"); - services.$logger.info(hookDataTable.toString()); + $logger.info("Hooks:"); + $logger.info(hookDataTable.toString()); } async function installHooks( - services: IHooksCommandServices, + context: CommandContext, + projectDir: string, pluginsWithHooks: IPluginData[], ): Promise { - const hooksDir = path.join(services.$projectData.projectDir, HOOKS_DIR_NAME); + const $fs = context.injector.get("fs"); + const hooksDir = path.join(projectDir, HOOKS_DIR_NAME); + const hooksLockPath = path.join(projectDir, LOCK_FILE_NAME); - if ( - services.$fs.exists( - path.join(services.$projectData.projectDir, LOCK_FILE_NAME), - ) - ) { - await verifyHooksLock( - services, - pluginsWithHooks, - path.join(services.$projectData.projectDir, LOCK_FILE_NAME), - ); + if ($fs.exists(hooksLockPath)) { + await verifyHooksLock(context, pluginsWithHooks, hooksLockPath); } if (pluginsWithHooks.length === 0) { - if (!services.$fs.exists(hooksDir)) { - services.$fs.createDirectory(hooksDir); + if (!$fs.exists(hooksDir)) { + $fs.createDirectory(hooksDir); } } for (const plugin of pluginsWithHooks) { @@ -55,31 +44,35 @@ async function installHooks( } } -export async function runHooksCommand( - services: IHooksCommandServices, +async function runHooksCommand( + context: CommandContext, isList: boolean, ): Promise { + const $pluginsService = + context.injector.get("pluginsService"); + const $projectData = context.injector.get("projectData"); + $projectData.initializeProjectData(); + const plugins: IPluginData[] = - await services.$pluginsService.getAllInstalledPlugins( - services.$projectData, - ); + await $pluginsService.getAllInstalledPlugins($projectData); if (plugins && plugins.length > 0) { const pluginsWithHooks = getPluginsWithHooks(plugins); if (isList) { - listHooks(services, pluginsWithHooks); + listHooks(context.injector.get("logger"), pluginsWithHooks); } else { - await installHooks(services, pluginsWithHooks); + await installHooks(context, $projectData.projectDir, pluginsWithHooks); } } } -export function canExecuteHooksCommand( - context: CommandContext, - services: IHooksCommandServices, -): boolean { +function canExecuteHooksCommand(context: CommandContext): boolean { + // A hooks command only makes sense inside a project, and reporting a missing + // one takes precedence over the argument check. + inject("projectData").initializeProjectData(); + if (context.args.length > 0 && context.args[0] !== "list") { - services.$errors.failWithHelp( + inject("errors").failWithHelp( `Invalid argument ${context.args[0]}. Supported argument is "list".`, ); } @@ -90,10 +83,9 @@ export const hooksInstallCommandDefinition = defineCommand({ name: "hooks|install", description: "Runs the postinstall hook of every installed plugin.", arguments: "any", - setup: injectHooksCommandServices, canExecute: canExecuteHooksCommand, - run(context, services): Promise { - return runHooksCommand(services, context.args[0] === "list"); + run(context): Promise { + return runHooksCommand(context, context.args[0] === "list"); }, }); @@ -101,10 +93,9 @@ export const hooksListCommandDefinition = defineCommand({ name: "hooks|*list", description: "Lists the hooks every installed plugin contributes.", arguments: "any", - setup: injectHooksCommandServices, // The name accepts "list" as its only argument, and lists either way. canExecute: canExecuteHooksCommand, - run(context, services): Promise { - return runHooksCommand(services, true); + run(context): Promise { + return runHooksCommand(context, true); }, }); diff --git a/lib/commands/info.ts b/lib/commands/info.ts index 008ad002e4..f2dd0021a8 100644 --- a/lib/commands/info.ts +++ b/lib/commands/info.ts @@ -6,10 +6,8 @@ export const infoCommandDefinition = defineCommand({ name: "info", description: "Displays version information about the CLI and its components.", arguments: "none", - setup: () => ({ - $infoService: inject("infoService"), - }), - run(context, services): Promise { - return services.$infoService.printComponentsInfo(); + run(): Promise { + const $infoService = inject("infoService"); + return $infoService.printComponentsInfo(); }, }); diff --git a/lib/commands/install.ts b/lib/commands/install.ts index eacd029b4d..69ab52f2b2 100644 --- a/lib/commands/install.ts +++ b/lib/commands/install.ts @@ -18,71 +18,53 @@ import { IPlatformsDataService } from "../definitions/platform"; import { IPluginsService } from "../definitions/plugins"; import { IProjectData, IProjectDataService } from "../definitions/project"; -export const installCommandOptions = { +const installCommandOptions = { frameworkPath: stringOption(), disableNpmInstall: booleanOption(), ignoreScripts: booleanOption(), path: stringOption(), } satisfies CommandOptionsSchema; -export type InstallCommandContext = CommandContext< - typeof installCommandOptions ->; - -export function setupInstallCommand() { - const services = { - $options: inject("options"), - $mobileHelper: inject("mobileHelper"), - $platformsDataService: inject( - "platformsDataService", - ), - $platformCommandHelper: inject( - "platformCommandHelper", - ), - $projectData: inject("projectData"), - $projectDataService: inject("projectDataService"), - $pluginsService: inject("pluginsService"), - $logger: inject("logger"), - $fs: inject("fs"), - $packageManager: inject("packageManager"), - }; - services.$projectData.initializeProjectData(); - - return services; -} - -export type IInstallCommandServices = ReturnType; - async function installProjectDependencies( - context: InstallCommandContext, - services: IInstallCommandServices, + context: CommandContext, ): Promise { + const $options = context.injector.get("options"); + const $mobileHelper = + context.injector.get("mobileHelper"); + const $platformsDataService = context.injector.get( + "platformsDataService", + ); + const $platformCommandHelper = context.injector.get( + "platformCommandHelper", + ); + const $projectData = context.injector.get("projectData"); + const $projectDataService = + context.injector.get("projectDataService"); + const $pluginsService = + context.injector.get("pluginsService"); + const $logger = context.injector.get("logger"); + let error: string = ""; - await services.$pluginsService.ensureAllDependenciesAreInstalled( - services.$projectData, - ); + await $pluginsService.ensureAllDependenciesAreInstalled($projectData); - for (const platform of services.$mobileHelper.platformNames) { - const platformData = services.$platformsDataService.getPlatformData( + for (const platform of $mobileHelper.platformNames) { + const platformData = $platformsDataService.getPlatformData( platform, - services.$projectData, + $projectData, ); - const frameworkPackageData = services.$projectDataService.getRuntimePackage( - services.$projectData.projectDir, + const frameworkPackageData = $projectDataService.getRuntimePackage( + $projectData.projectDir, platformData.platformNameLowerCase, ); if (frameworkPackageData && frameworkPackageData.version) { try { const platformProjectService = platformData.platformProjectService; - await platformProjectService.validate( - services.$projectData, - services.$options, - ); + await platformProjectService.validate($projectData, $options); - await services.$platformCommandHelper.addPlatforms( + await $platformCommandHelper.addPlatforms( [`${platform}@${frameworkPackageData.version}`], - services.$projectData, + $projectData, context.options.frameworkPath, ); } catch (err) { @@ -92,23 +74,27 @@ async function installProjectDependencies( } if (error) { - services.$logger.error(error); + $logger.error(error); } } async function installModule( - context: InstallCommandContext, - services: IInstallCommandServices, + context: CommandContext, moduleName: string, ): Promise { - const projectDir = services.$projectData.projectDir; + const $projectData = context.injector.get("projectData"); + const $fs = context.injector.get("fs"); + const $packageManager = + context.injector.get("packageManager"); + + const projectDir = $projectData.projectDir; const devPrefix = "nativescript-dev-"; - if (!services.$fs.exists(moduleName) && moduleName.indexOf(devPrefix) !== 0) { + if (!$fs.exists(moduleName) && moduleName.indexOf(devPrefix) !== 0) { moduleName = devPrefix + moduleName; } - await services.$packageManager.install(moduleName, projectDir, { + await $packageManager.install(moduleName, projectDir, { "save-dev": true, disableNpmInstall: context.options.disableNpmInstall, frameworkPath: context.options.frameworkPath, @@ -117,15 +103,6 @@ async function installModule( }); } -export function runInstallCommand( - context: InstallCommandContext, - services: IInstallCommandServices, -): Promise { - return context.args[0] - ? installModule(context, services, context.args[0]) - : installProjectDependencies(context, services); -} - export const installCommandDefinition = defineCommand({ name: "install", description: @@ -133,6 +110,14 @@ export const installCommandDefinition = defineCommand({ options: installCommandOptions, arguments: [{ name: "moduleName" }], enableHooks: false, - setup: setupInstallCommand, - run: runInstallCommand, + // In setup, not run: it lands ahead of the arguments policy, so being + // outside a project is what a bad invocation reports first. + setup(): void { + inject("projectData").initializeProjectData(); + }, + run(context): Promise { + return context.args[0] + ? installModule(context, context.args[0]) + : installProjectDependencies(context); + }, }); diff --git a/lib/commands/list-platforms.ts b/lib/commands/list-platforms.ts index 789aa25b0b..9af91cf5d2 100644 --- a/lib/commands/list-platforms.ts +++ b/lib/commands/list-platforms.ts @@ -4,66 +4,47 @@ import { IPlatformCommandHelper } from "../declarations"; import { defineCommand } from "../common/define-command"; import { inject } from "../common/di"; -export function setupListPlatformsCommand() { - const services = { - $platformCommandHelper: inject( - "platformCommandHelper", - ), - $projectData: inject("projectData"), - $logger: inject("logger"), - }; - services.$projectData.initializeProjectData(); - - return services; -} - -export type IListPlatformsCommandServices = ReturnType< - typeof setupListPlatformsCommand ->; - export const listPlatformsCommandDefinition = defineCommand({ name: "platform|*list", description: "Lists all platforms that the project currently targets.", arguments: "none", - setup: setupListPlatformsCommand, - async run(context, services): Promise { + // In setup, not run: it lands ahead of the arguments policy, so being + // outside a project is what a bad invocation reports first. + setup(): void { + inject("projectData").initializeProjectData(); + }, + async run(): Promise { + const $platformCommandHelper = inject( + "platformCommandHelper", + ); + const $projectData = inject("projectData"); + const $logger = inject("logger"); const installedPlatforms = - services.$platformCommandHelper.getInstalledPlatforms( - services.$projectData, - ); + $platformCommandHelper.getInstalledPlatforms($projectData); if (installedPlatforms.length > 0) { const preparedPlatforms = - services.$platformCommandHelper.getPreparedPlatforms( - services.$projectData, - ); + $platformCommandHelper.getPreparedPlatforms($projectData); if (preparedPlatforms.length > 0) { - services.$logger.info( + $logger.info( "The project is prepared for: ", helpers.formatListOfNames(preparedPlatforms, "and"), ); } else { - services.$logger.info("The project is not prepared for any platform"); + $logger.info("The project is not prepared for any platform"); } - services.$logger.info( + $logger.info( "Installed platforms: ", helpers.formatListOfNames(installedPlatforms, "and"), ); } else { const formattedPlatformsList = helpers.formatListOfNames( - services.$platformCommandHelper.getAvailablePlatforms( - services.$projectData, - ), + $platformCommandHelper.getAvailablePlatforms($projectData), "and", ); - services.$logger.info( - "Available platforms for this OS: ", - formattedPlatformsList, - ); - services.$logger.info( - "No installed platforms found. Use $ ns platform add", - ); + $logger.info("Available platforms for this OS: ", formattedPlatformsList); + $logger.info("No installed platforms found. Use $ ns platform add"); } }, }); diff --git a/lib/commands/migrate.ts b/lib/commands/migrate.ts index e3345fcd46..9fb9c88213 100644 --- a/lib/commands/migrate.ts +++ b/lib/commands/migrate.ts @@ -3,48 +3,42 @@ import { IMigrateController, IMigrationData } from "../definitions/migrate"; import { defineCommand } from "../common/define-command"; import { inject } from "../common/di"; -export function setupMigrateCommand() { - const services = { - $devicePlatformsConstants: inject( - "devicePlatformsConstants", - ), - $migrateController: inject("migrateController"), - $staticConfig: inject("staticConfig"), - $projectData: inject("projectData"), - $logger: inject("logger"), - }; - services.$projectData.initializeProjectData(); - - return services; -} - -export type IMigrateCommandServices = ReturnType; - export const migrateCommandDefinition = defineCommand({ name: "migrate", description: "Migrates the project's dependencies to the ones the current CLI supports.", arguments: "none", - setup: setupMigrateCommand, - async run(context, services): Promise { + // In setup, not run: it lands ahead of the arguments policy, so being + // outside a project is what a bad invocation reports first. + setup(): void { + inject("projectData").initializeProjectData(); + }, + async run(): Promise { + const $devicePlatformsConstants = inject( + "devicePlatformsConstants", + ); + const $migrateController = inject("migrateController"); + const $staticConfig = inject("staticConfig"); + const $projectData = inject("projectData"); + const $logger = inject("logger"); const migrationData: IMigrationData = { - projectDir: services.$projectData.projectDir, + projectDir: $projectData.projectDir, platforms: [ - services.$devicePlatformsConstants.Android, - services.$devicePlatformsConstants.iOS, + $devicePlatformsConstants.Android, + $devicePlatformsConstants.iOS, ], }; const shouldMigrateResult = - await services.$migrateController.shouldMigrate(migrationData); + await $migrateController.shouldMigrate(migrationData); if (!shouldMigrateResult) { - const cliVersion = services.$staticConfig.version; - services.$logger.printMarkdown( + const cliVersion = $staticConfig.version; + $logger.printMarkdown( `__Project is compatible with NativeScript \`v${cliVersion}\`__`, ); return; } - await services.$migrateController.migrate(migrationData); + await $migrateController.migrate(migrationData); }, }); diff --git a/lib/commands/native-add.ts b/lib/commands/native-add.ts index 3567ed31b4..6b995934aa 100644 --- a/lib/commands/native-add.ts +++ b/lib/commands/native-add.ts @@ -2,7 +2,11 @@ import * as fs from "fs"; import { EOL } from "os"; import * as path from "path"; import { IErrors } from "../common/declarations"; -import { CommandName, defineCommand } from "../common/define-command"; +import { + CommandContext, + CommandName, + defineCommand, +} from "../common/define-command"; import { inject } from "../common/di"; import { capitalizeFirstLetter } from "../common/utils"; import { IProjectData } from "../definitions/project"; @@ -14,38 +18,19 @@ import { IProjectData } from "../definitions/project"; */ type NativeAddLanguage = "java" | "kotlin" | "swift" | "objective-c"; -interface INativeAddLanguageCommandServices extends INativeAddCommandServices { - language: NativeAddLanguage; -} - -export function setupNativeAddCommand() { - const services = { - $projectData: inject("projectData"), - $logger: inject("logger"), - $errors: inject("errors"), - }; - services.$projectData.initializeProjectData(); - - return services; -} - -export type INativeAddCommandServices = ReturnType< - typeof setupNativeAddCommand ->; - -function failWithUsage(services: INativeAddCommandServices): void { - services.$errors.failWithHelp( +function failWithUsage($errors: IErrors): void { + $errors.failWithHelp( "Usage: ns native add [swift|objective-c|java|kotlin] [class name]", ); } -function getIosSourcePathBase(services: INativeAddCommandServices): string { - const resources = services.$projectData.getAppResourcesDirectoryPath(); +function getIosSourcePathBase($projectData: IProjectData): string { + const resources = $projectData.getAppResourcesDirectoryPath(); return path.join(resources, "iOS", "src"); } -function getAndroidSourcePathBase(services: INativeAddCommandServices): string { - const resources = services.$projectData.getAppResourcesDirectoryPath(); +function getAndroidSourcePathBase($projectData: IProjectData): string { + const resources = $projectData.getAppResourcesDirectoryPath(); return path.join(resources, "Android", "src", "main", "java"); } @@ -102,10 +87,11 @@ class ${classSimpleName} { ); } -function checkAndUpdateGradleProperties( - services: INativeAddCommandServices, -): boolean { - const resources = services.$projectData.getAppResourcesDirectoryPath(); +function checkAndUpdateGradleProperties(ctx: CommandContext): boolean { + const $projectData = ctx.injector.get("projectData"); + const $logger = ctx.injector.get("logger"); + const $errors = ctx.injector.get("errors"); + const resources = $projectData.getAppResourcesDirectoryPath(); const filePath = path.join(resources, "Android", "gradle.properties"); @@ -118,7 +104,7 @@ function checkAndUpdateGradleProperties( const useKotlin = match[1]; if (useKotlin === "false") { - services.$errors.failWithHelp( + $errors.failWithHelp( "The useKotlin property is set to false. Stopping processing. Kotlin must be enabled in gradle.properties to use.", ); return false; @@ -129,41 +115,38 @@ function checkAndUpdateGradleProperties( } } else { fs.appendFileSync(filePath, `${EOL}useKotlin=true${EOL}`); - services.$logger.info( - 'Added "useKotlin=true" property to gradle.properties.', - ); + $logger.info('Added "useKotlin=true" property to gradle.properties.'); } } else { fs.writeFileSync(filePath, `useKotlin=true${EOL}`); - services.$logger.info( - 'Created gradle.properties with "useKotlin=true" property.', - ); + $logger.info('Created gradle.properties with "useKotlin=true" property.'); } return true; } -export function generateJavaKotlin( - services: INativeAddCommandServices, +function generateJavaKotlin( + ctx: CommandContext, className: string, extension: string, ): void { + const $projectData = ctx.injector.get("projectData"); + const $logger = ctx.injector.get("logger"); + const $errors = ctx.injector.get("errors"); const fileExt = extension == "java" ? extension : "kt"; const packageName = getPackageName(className); const classSimpleName = getClassSimpleName(className); const packagePath = path.join( - getAndroidSourcePathBase(services), + getAndroidSourcePathBase($projectData), ...packageName.split("."), ); const filePath = path.join(packagePath, `${classSimpleName}.${fileExt}`); if (fs.existsSync(filePath)) { - services.$errors.failWithHelp( - `${extension} file '${filePath}' already exists.`, - ); + $errors.failWithHelp(`${extension} file '${filePath}' already exists.`); return; } - if (extension == "kotlin" && !checkAndUpdateGradleProperties(services)) { + if (extension == "kotlin" && !checkAndUpdateGradleProperties(ctx)) { return; } @@ -174,7 +157,7 @@ export function generateJavaKotlin( fs.mkdirSync(packagePath, { recursive: true }); fs.writeFileSync(filePath, fileContent); - services.$logger.info( + $logger.info( `${capitalizeFirstLetter( extension, )} file '${filePath}' generated successfully.`, @@ -182,7 +165,7 @@ export function generateJavaKotlin( } function generateOrUpdateModuleMap( - services: INativeAddCommandServices, + $logger: ILogger, headerFileName: string, moduleMapPath: string, ): void { @@ -201,7 +184,7 @@ function generateOrUpdateModuleMap( // Module declaration already exists in the module map if (moduleMapContent.includes(headerDeclaration)) { // Header is already present in the module map - services.$logger.warn( + $logger.warn( `Header '${headerFileName}' is already added to the module map.`, ); return; @@ -221,28 +204,27 @@ function generateOrUpdateModuleMap( fs.writeFileSync(moduleMapPath, moduleMapContent); } - services.$logger.info( + $logger.info( `Module map '${moduleMapPath}' has been updated with the header '${headerFileName}'.`, ); } function generateObjectiveCFiles( - services: INativeAddCommandServices, + ctx: CommandContext, className: string, classFilePath: string, interfaceFilePath: string, ): boolean { + const $logger = ctx.injector.get("logger"); + const $errors = ctx.injector.get("errors"); + if (fs.existsSync(classFilePath)) { - services.$errors.failWithHelp( - `Error: File '${classFilePath}' already exists.`, - ); + $errors.failWithHelp(`Error: File '${classFilePath}' already exists.`); return false; } if (fs.existsSync(interfaceFilePath)) { - services.$errors.failWithHelp( - `Error: File '${interfaceFilePath}' already exists.`, - ); + $errors.failWithHelp(`Error: File '${interfaceFilePath}' already exists.`); return false; } @@ -267,32 +249,29 @@ function generateObjectiveCFiles( `; fs.writeFileSync(classFilePath, classContent); - services.$logger.trace( + $logger.trace( `Objective-C class file '${classFilePath}' generated successfully.`, ); fs.writeFileSync(interfaceFilePath, interfaceContent); - services.$logger.trace( + $logger.trace( `Objective-C interface file '${interfaceFilePath}' generated successfully.`, ); return true; } -export function generateObjectiveC( - services: INativeAddCommandServices, - className: string, -): void { - const iosSourceBase = getIosSourcePathBase(services); +function generateObjectiveC(ctx: CommandContext, className: string): void { + const $projectData = ctx.injector.get("projectData"); + const $logger = ctx.injector.get("logger"); + const iosSourceBase = getIosSourcePathBase($projectData); const classFilePath = path.join(iosSourceBase, `${className}.m`); const headerFilePath = path.join(iosSourceBase, `${className}.h`); - if ( - generateObjectiveCFiles(services, className, classFilePath, headerFilePath) - ) { + if (generateObjectiveCFiles(ctx, className, classFilePath, headerFilePath)) { // Modify/Generate moduleMap generateOrUpdateModuleMap( - services, + $logger, `${className}.h`, path.join(iosSourceBase, "module.modulemap"), ); @@ -300,19 +279,21 @@ export function generateObjectiveC( } function generateSwiftFile( - services: INativeAddCommandServices, + ctx: CommandContext, className: string, filePath: string, ): void { + const $logger = ctx.injector.get("logger"); + const $errors = ctx.injector.get("errors"); const directory = path.dirname(filePath); if (!fs.existsSync(directory)) { fs.mkdirSync(directory, { recursive: true }); - services.$logger.trace(`Created directory: '${directory}'.`); + $logger.trace(`Created directory: '${directory}'.`); } if (fs.existsSync(filePath)) { - services.$errors.failWithHelp(`Error: File '${filePath}' already exists.`); + $errors.failWithHelp(`Error: File '${filePath}' already exists.`); return; } @@ -326,26 +307,22 @@ import os; }`; fs.writeFileSync(filePath, content); - services.$logger.info(`Swift file '${filePath}' generated successfully.`); + $logger.info(`Swift file '${filePath}' generated successfully.`); } -export function generateSwift( - services: INativeAddCommandServices, - className: string, -): void { - const iosSourceBase = getIosSourcePathBase(services); +function generateSwift(ctx: CommandContext, className: string): void { + const $projectData = ctx.injector.get("projectData"); + const iosSourceBase = getIosSourcePathBase($projectData); const swiftFilePath = path.join(iosSourceBase, `${className}.swift`); - generateSwiftFile(services, className, swiftFilePath); + generateSwiftFile(ctx, className, swiftFilePath); } const generators: Record< NativeAddLanguage, - (services: INativeAddCommandServices, className: string) => void + (ctx: CommandContext, className: string) => void > = { - java: (services, className) => - generateJavaKotlin(services, className, "java"), - kotlin: (services, className) => - generateJavaKotlin(services, className, "kotlin"), + java: (ctx, className) => generateJavaKotlin(ctx, className, "java"), + kotlin: (ctx, className) => generateJavaKotlin(ctx, className, "kotlin"), swift: generateSwift, "objective-c": generateObjectiveC, }; @@ -355,13 +332,15 @@ export const nativeAddCommandDefinition = defineCommand({ description: "Commands to add native files to the application placing them in the correct directory.", arguments: "any", - setup: setupNativeAddCommand, - canExecute(context, services: INativeAddCommandServices): boolean { - failWithUsage(services); + setup() { + inject("projectData").initializeProjectData(); + }, + canExecute(): boolean { + failWithUsage(inject("errors")); return false; }, - run(context, services: INativeAddCommandServices): void { - failWithUsage(services); + run(): void { + failWithUsage(inject("errors")); }, }); @@ -375,21 +354,20 @@ const defineNativeAddLanguageCommand = ( // The one usage message answers both too few and too many arguments; a // declared argument spec would report them with two different ones. arguments: "any", - setup(): INativeAddLanguageCommandServices { - return { - ...setupNativeAddCommand(), - language, - }; + setup() { + inject("projectData").initializeProjectData(); }, - canExecute(context, services: INativeAddLanguageCommandServices): boolean { + canExecute(context): boolean { + const $errors = inject("errors"); + if (context.args.length !== 1) { - failWithUsage(services); + failWithUsage($errors); } return true; }, - run(context, services: INativeAddLanguageCommandServices): void { - generators[services.language](services, context.args[0]); + run(context): void { + generators[language](context, context.args[0]); }, }); diff --git a/lib/commands/open.ts b/lib/commands/open.ts index 1bf8696964..9d614ab2ba 100644 --- a/lib/commands/open.ts +++ b/lib/commands/open.ts @@ -4,6 +4,7 @@ import * as path from "path"; import { IChildProcess, IXcodeSelectService } from "../common/declarations"; import { booleanOption, + CommandContext, CommandOptionsSchema, defineCommand, } from "../common/define-command"; @@ -14,39 +15,7 @@ import { IOptions } from "../declarations"; import { IProjectData } from "../definitions/project"; import type { IOSProjectService } from "../services/ios-project-service"; -export function injectOpenXcodeProjectServices() { - return { - $iOSProjectService: inject("iOSProjectService"), - $logger: inject("logger"), - $childProcess: inject("childProcess"), - $projectData: inject("projectData"), - $xcodeSelectService: inject("xcodeSelectService"), - $xcodebuildArgsService: inject( - "xcodebuildArgsService", - ), - }; -} - -export type IOpenXcodeProjectServices = ReturnType< - typeof injectOpenXcodeProjectServices ->; - -export function injectOpenAndroidStudioServices() { - return { - $logger: inject("logger"), - $liveSyncCommandHelper: inject( - "liveSyncCommandHelper", - ), - $childProcess: inject("childProcess"), - $projectData: inject("projectData"), - }; -} - -export type IOpenAndroidStudioServices = ReturnType< - typeof injectOpenAndroidStudioServices ->; - -export function getAndroidStudioPath(): string | null { +function getAndroidStudioPath(): string | null { const os = currentPlatform(); if (os === "darwin") { @@ -79,14 +48,21 @@ export function getAndroidStudioPath(): string | null { * while `ns run` owns stdin and has to hand it back after `prepare` consumed * it, a one-shot CLI command exits instead. */ -export async function openAndroidStudioProject( - services: IOpenAndroidStudioServices, +async function openAndroidStudioProject( + context: CommandContext, platform: string, isInteractive: boolean, ): Promise { - services.$liveSyncCommandHelper.validatePlatform(platform); - services.$projectData.initializeProjectData(); - const androidDir = `${services.$projectData.platformsDir}/android`; + const $childProcess = context.injector.get("childProcess"); + const $liveSyncCommandHelper = context.injector.get( + "liveSyncCommandHelper", + ); + const $logger = context.injector.get("logger"); + const $projectData = context.injector.get("projectData"); + + $liveSyncCommandHelper.validatePlatform(platform); + $projectData.initializeProjectData(); + const androidDir = `${$projectData.platformsDir}/android`; if (!fs.existsSync(androidDir)) { const prepareCommand = injector.resolveCommand("prepare") as ICommand; @@ -104,7 +80,7 @@ export async function openAndroidStudioProject( studioPath = getAndroidStudioPath(); if (!studioPath) { - services.$logger.error( + $logger.error( "Android Studio is not installed, or is not in a standard location. Use NATIVESCRIPT_ANDROID_STUDIO_PATH.", ); return; @@ -113,34 +89,42 @@ export async function openAndroidStudioProject( const os = currentPlatform(); if (os === "darwin") { - services.$childProcess.exec(`open -a "${studioPath}" ${androidDir}`); + $childProcess.exec(`open -a "${studioPath}" ${androidDir}`); } else if (os === "win32") { - const child = services.$childProcess.spawn(studioPath, [androidDir], { + const child = $childProcess.spawn(studioPath, [androidDir], { detached: true, stdio: "ignore", }); child.unref(); } else if (os === "linux") { - services.$childProcess.exec(`${studioPath} ${androidDir}`); + $childProcess.exec(`${studioPath} ${androidDir}`); } } -export async function openXcodeProject( - services: IOpenXcodeProjectServices, +async function openXcodeProject( + context: CommandContext, platformDirName: string, isInteractive: boolean, ): Promise { + const $childProcess = context.injector.get("childProcess"); + const $iOSProjectService = + context.injector.get("iOSProjectService"); + const $logger = context.injector.get("logger"); + const $projectData = context.injector.get("projectData"); + const $xcodeSelectService = + context.injector.get("xcodeSelectService"); + const $xcodebuildArgsService = context.injector.get( + "xcodebuildArgsService", + ); + const os = currentPlatform(); if (os !== "darwin") { - services.$logger.error("Opening a project in XCode requires macOS."); + $logger.error("Opening a project in XCode requires macOS."); return; } - services.$projectData.initializeProjectData(); - const platformDir = path.resolve( - services.$projectData.platformsDir, - platformDirName, - ); + $projectData.initializeProjectData(); + const platformDir = path.resolve($projectData.platformsDir, platformDirName); if (!fs.existsSync(platformDir)) { const prepareCommand = injector.resolveCommand("prepare") as ICommand; @@ -150,33 +134,31 @@ export async function openXcodeProject( process.stdin.resume(); } } - const platformData = services.$iOSProjectService.getPlatformData( - services.$projectData, - ); - const xcprojectFile = services.$xcodebuildArgsService.getXcodeProjectArgs( + const platformData = $iOSProjectService.getPlatformData($projectData); + const xcprojectFile = $xcodebuildArgsService.getXcodeProjectArgs( platformData, - services.$projectData, + $projectData, )[1]; if (fs.existsSync(xcprojectFile)) { - services.$xcodeSelectService + $xcodeSelectService .getDeveloperDirectoryPath() - .then(() => services.$childProcess.exec(`open ${xcprojectFile}`, {})) + .then(() => $childProcess.exec(`open ${xcprojectFile}`, {})) .catch((e) => { - services.$logger.error(e.message); + $logger.error(e.message); }); } else { - services.$logger.error(`Unable to open project file: ${xcprojectFile}`); + $logger.error(`Unable to open project file: ${xcprojectFile}`); } } -export async function openVisionOSProject( - services: IOpenXcodeProjectServices, +async function openVisionOSProject( + context: CommandContext, $options: IOptions, isInteractive: boolean, ): Promise { $options.platformOverride = "visionOS"; - await openXcodeProject(services, "visionos", isInteractive); + await openXcodeProject(context, "visionos", isInteractive); $options.platformOverride = null; } @@ -208,16 +190,9 @@ export const iosOpenCommand = defineCommand({ description: "Opens the project in Xcode.", options: openCommandOptions, arguments: "none", - setup() { - return { - ...injectOpenXcodeProjectServices(), - $options: inject("options"), - }; - }, - async run(context, services): Promise { - await withoutWatch(services.$options, () => - openXcodeProject(services, "ios", false), - ); + async run(context): Promise { + const $options = inject("options"); + await withoutWatch($options, () => openXcodeProject(context, "ios", false)); }, }); @@ -226,15 +201,10 @@ export const visionOpenCommand = defineCommand({ description: "Opens the visionOS project in Xcode.", options: openCommandOptions, arguments: "none", - setup() { - return { - ...injectOpenXcodeProjectServices(), - $options: inject("options"), - }; - }, - async run(context, services): Promise { - await withoutWatch(services.$options, () => - openVisionOSProject(services, services.$options, false), + async run(context): Promise { + const $options = inject("options"); + await withoutWatch($options, () => + openVisionOSProject(context, $options, false), ); }, }); @@ -244,15 +214,10 @@ export const androidOpenCommand = defineCommand({ description: "Opens the project in Android Studio.", options: openCommandOptions, arguments: "none", - setup() { - return { - ...injectOpenAndroidStudioServices(), - $options: inject("options"), - }; - }, - async run(context, services): Promise { - await withoutWatch(services.$options, () => - openAndroidStudioProject(services, "Android", false), + async run(context): Promise { + const $options = inject("options"); + await withoutWatch($options, () => + openAndroidStudioProject(context, "Android", false), ); }, }); diff --git a/lib/commands/plugin/add-plugin.ts b/lib/commands/plugin/add-plugin.ts index 66287c59d7..96b3d9dab4 100644 --- a/lib/commands/plugin/add-plugin.ts +++ b/lib/commands/plugin/add-plugin.ts @@ -2,56 +2,42 @@ import * as _ from "lodash"; import { IProjectData } from "../../definitions/project"; import { IPluginsService, IPluginData } from "../../definitions/plugins"; import { IErrors } from "../../common/declarations"; -import { CommandContext, defineCommand } from "../../common/define-command"; +import { defineCommand } from "../../common/define-command"; import { inject } from "../../common/di"; -export function setupAddPluginCommand() { - const services = { - $pluginsService: inject("pluginsService"), - $projectData: inject("projectData"), - $errors: inject("errors"), - }; - services.$projectData.initializeProjectData(); - - return services; -} - -export type IAddPluginCommandServices = ReturnType< - typeof setupAddPluginCommand ->; - -export async function canExecuteAddPluginCommand( - context: CommandContext, - services: IAddPluginCommandServices, -): Promise { - if (!context.args[0]) { - services.$errors.failWithHelp("You must specify plugin name."); - } - - const installedPlugins = - await services.$pluginsService.getAllInstalledPlugins( - services.$projectData, - ); - const pluginName = context.args[0].toLowerCase(); - if ( - _.some( - installedPlugins, - (plugin: IPluginData) => plugin.name.toLowerCase() === pluginName, - ) - ) { - services.$errors.fail(`Plugin "${pluginName}" is already installed.`); - } - - return true; -} - export const addPluginCommandDefinition = defineCommand({ name: ["plugin|add", "plugin|install"], description: "Installs the specified plugin and its dependencies.", arguments: "any", - setup: setupAddPluginCommand, - canExecute: canExecuteAddPluginCommand, - run(context, services): Promise { - return services.$pluginsService.add(context.args[0], services.$projectData); + async canExecute(context): Promise { + const $pluginsService = inject("pluginsService"); + const $projectData = inject("projectData"); + const $errors = inject("errors"); + $projectData.initializeProjectData(); + + if (!context.args[0]) { + $errors.failWithHelp("You must specify plugin name."); + } + + const installedPlugins = + await $pluginsService.getAllInstalledPlugins($projectData); + const pluginName = context.args[0].toLowerCase(); + if ( + _.some( + installedPlugins, + (plugin: IPluginData) => plugin.name.toLowerCase() === pluginName, + ) + ) { + $errors.fail(`Plugin "${pluginName}" is already installed.`); + } + + return true; + }, + run(context): Promise { + const $pluginsService = inject("pluginsService"); + const $projectData = inject("projectData"); + $projectData.initializeProjectData(); + + return $pluginsService.add(context.args[0], $projectData); }, }); diff --git a/lib/commands/plugin/list-plugins.ts b/lib/commands/plugin/list-plugins.ts index cd589964ed..4341028f76 100644 --- a/lib/commands/plugin/list-plugins.ts +++ b/lib/commands/plugin/list-plugins.ts @@ -9,21 +9,6 @@ import { defineCommand } from "../../common/define-command"; import { inject } from "../../common/di"; import { color } from "../../color"; -export function setupListPluginsCommand() { - const services = { - $pluginsService: inject("pluginsService"), - $projectData: inject("projectData"), - $logger: inject("logger"), - }; - services.$projectData.initializeProjectData(); - - return services; -} - -export type IListPluginsCommandServices = ReturnType< - typeof setupListPluginsCommand ->; - function createTableCells(items: IBasePluginData[]): string[][] { return items.map((item) => [item.name, item.version]); } @@ -32,12 +17,17 @@ export const listPluginsCommandDefinition = defineCommand({ name: "plugin|*list", description: "Lists all installed plugins.", arguments: "none", - setup: setupListPluginsCommand, - async run(context, services): Promise { + // In setup, not run: it lands ahead of the arguments policy, so being + // outside a project is what a bad invocation reports first. + setup(): void { + inject("projectData").initializeProjectData(); + }, + async run(): Promise { + const $pluginsService = inject("pluginsService"); + const $projectData = inject("projectData"); + const $logger = inject("logger"); const installedPlugins: IPackageJsonDepedenciesResult = - services.$pluginsService.getDependenciesFromPackageJson( - services.$projectData.projectDir, - ); + $pluginsService.getDependenciesFromPackageJson($projectData.projectDir); const headers: string[] = ["Plugin", "Version"]; const dependenciesData: string[][] = createTableCells( @@ -45,8 +35,8 @@ export const listPluginsCommandDefinition = defineCommand({ ); const dependenciesTable: any = createTable(headers, dependenciesData); - services.$logger.info("Dependencies:"); - services.$logger.info(dependenciesTable.toString()); + $logger.info("Dependencies:"); + $logger.info(dependenciesTable.toString()); if ( installedPlugins.devDependencies && @@ -61,10 +51,10 @@ export const listPluginsCommandDefinition = defineCommand({ devDependenciesData, ); - services.$logger.info("Dev Dependencies:"); - services.$logger.info(devDependenciesTable.toString()); + $logger.info("Dev Dependencies:"); + $logger.info(devDependenciesTable.toString()); } else { - services.$logger.info("There are no dev dependencies."); + $logger.info("There are no dev dependencies."); } const viewDependenciesCommand: string = color.cyan( @@ -74,11 +64,11 @@ export const listPluginsCommandDefinition = defineCommand({ "npm view grep devDependencies", ); - services.$logger.warn("NOTE:"); - services.$logger.warn( + $logger.warn("NOTE:"); + $logger.warn( `If you want to check the dependencies of installed plugin use ${viewDependenciesCommand}`, ); - services.$logger.warn( + $logger.warn( `If you want to check the dev dependencies of installed plugin use ${viewDevDependenciesCommand}`, ); }, diff --git a/lib/commands/plugin/remove-plugin.ts b/lib/commands/plugin/remove-plugin.ts index 8cb0683c8e..a2cfe193df 100644 --- a/lib/commands/plugin/remove-plugin.ts +++ b/lib/commands/plugin/remove-plugin.ts @@ -2,64 +2,47 @@ import * as _ from "lodash"; import { IProjectData } from "../../definitions/project"; import { IPluginsService } from "../../definitions/plugins"; import { IErrors } from "../../common/declarations"; -import { CommandContext, defineCommand } from "../../common/define-command"; +import { defineCommand } from "../../common/define-command"; import { inject } from "../../common/di"; -export function setupRemovePluginCommand() { - const services = { - $pluginsService: inject("pluginsService"), - $errors: inject("errors"), - $logger: inject("logger"), - $projectData: inject("projectData"), - }; - services.$projectData.initializeProjectData(); - - return services; -} - -export type IRemovePluginCommandServices = ReturnType< - typeof setupRemovePluginCommand ->; - -export async function canExecuteRemovePluginCommand( - context: CommandContext, - services: IRemovePluginCommandServices, -): Promise { - if (!context.args[0]) { - services.$errors.failWithHelp("You must specify plugin name."); - } - - let pluginNames: string[] = []; - try { - // try installing the plugins, so we can get information from node_modules about their native code, libs, etc. - const installedPlugins = - await services.$pluginsService.getAllInstalledPlugins( - services.$projectData, - ); - pluginNames = installedPlugins.map((pl) => pl.name); - } catch (err) { - services.$logger.trace("Error while installing plugins. Error is:", err); - pluginNames = _.keys(services.$projectData.dependencies); - } - - const pluginName = context.args[0].toLowerCase(); - if (!_.some(pluginNames, (name) => name.toLowerCase() === pluginName)) { - services.$errors.fail(`Plugin "${pluginName}" is not installed.`); - } - - return true; -} - export const removePluginCommandDefinition = defineCommand({ name: "plugin|remove", description: "Uninstalls the specified plugin and its dependencies.", arguments: "any", - setup: setupRemovePluginCommand, - canExecute: canExecuteRemovePluginCommand, - run(context, services): Promise { - return services.$pluginsService.remove( - context.args[0], - services.$projectData, - ); + async canExecute(context): Promise { + const $pluginsService = inject("pluginsService"); + const $errors = inject("errors"); + const $logger = inject("logger"); + const $projectData = inject("projectData"); + $projectData.initializeProjectData(); + + if (!context.args[0]) { + $errors.failWithHelp("You must specify plugin name."); + } + + let pluginNames: string[] = []; + try { + // try installing the plugins, so we can get information from node_modules about their native code, libs, etc. + const installedPlugins = + await $pluginsService.getAllInstalledPlugins($projectData); + pluginNames = installedPlugins.map((pl) => pl.name); + } catch (err) { + $logger.trace("Error while installing plugins. Error is:", err); + pluginNames = _.keys($projectData.dependencies); + } + + const pluginName = context.args[0].toLowerCase(); + if (!_.some(pluginNames, (name) => name.toLowerCase() === pluginName)) { + $errors.fail(`Plugin "${pluginName}" is not installed.`); + } + + return true; + }, + run(context): Promise { + const $pluginsService = inject("pluginsService"); + const $projectData = inject("projectData"); + $projectData.initializeProjectData(); + + return $pluginsService.remove(context.args[0], $projectData); }, }); diff --git a/lib/commands/plugin/update-plugin.ts b/lib/commands/plugin/update-plugin.ts index c2672960bb..44de94e80d 100644 --- a/lib/commands/plugin/update-plugin.ts +++ b/lib/commands/plugin/update-plugin.ts @@ -2,74 +2,55 @@ import * as _ from "lodash"; import { IProjectData } from "../../definitions/project"; import { IPluginsService } from "../../definitions/plugins"; import { IErrors } from "../../common/declarations"; -import { CommandContext, defineCommand } from "../../common/define-command"; +import { defineCommand } from "../../common/define-command"; import { inject } from "../../common/di"; -export function setupUpdatePluginCommand() { - const services = { - $pluginsService: inject("pluginsService"), - $projectData: inject("projectData"), - $errors: inject("errors"), - }; - services.$projectData.initializeProjectData(); - - return services; -} - -export type IUpdatePluginCommandServices = ReturnType< - typeof setupUpdatePluginCommand ->; - -export async function canExecuteUpdatePluginCommand( - context: CommandContext, - services: IUpdatePluginCommandServices, -): Promise { - const args = context.args; - if (!args || args.length === 0) { - return true; - } - - const installedPlugins = - await services.$pluginsService.getAllInstalledPlugins( - services.$projectData, - ); - const installedPluginNames: string[] = installedPlugins.map((pl) => pl.name); - - const pluginName = args[0].toLowerCase(); - if ( - !_.some(installedPluginNames, (name) => name.toLowerCase() === pluginName) - ) { - services.$errors.fail(`Plugin "${pluginName}" is not installed.`); - } - - return true; -} +export const updatePluginCommandDefinition = defineCommand({ + name: "plugin|update", + description: "Uninstalls and installs the specified plugin(s).", + arguments: "any", + async canExecute(context): Promise { + const $pluginsService = inject("pluginsService"); + const $projectData = inject("projectData"); + const $errors = inject("errors"); + $projectData.initializeProjectData(); -export async function runUpdatePluginCommand( - context: CommandContext, - services: IUpdatePluginCommandServices, -): Promise { - let pluginNames = context.args; + const args = context.args; + if (!args || args.length === 0) { + return true; + } - if (!pluginNames || context.args.length === 0) { const installedPlugins = - await services.$pluginsService.getAllInstalledPlugins( - services.$projectData, - ); - pluginNames = installedPlugins.map((p) => p.name); - } + await $pluginsService.getAllInstalledPlugins($projectData); + const installedPluginNames: string[] = installedPlugins.map( + (pl) => pl.name, + ); - for (const pluginName of pluginNames) { - await services.$pluginsService.remove(pluginName, services.$projectData); - await services.$pluginsService.add(pluginName, services.$projectData); - } -} + const pluginName = args[0].toLowerCase(); + if ( + !_.some(installedPluginNames, (name) => name.toLowerCase() === pluginName) + ) { + $errors.fail(`Plugin "${pluginName}" is not installed.`); + } -export const updatePluginCommandDefinition = defineCommand({ - name: "plugin|update", - description: "Uninstalls and installs the specified plugin(s).", - arguments: "any", - setup: setupUpdatePluginCommand, - canExecute: canExecuteUpdatePluginCommand, - run: runUpdatePluginCommand, + return true; + }, + async run(context): Promise { + const $pluginsService = inject("pluginsService"); + const $projectData = inject("projectData"); + $projectData.initializeProjectData(); + + let pluginNames = context.args; + + if (!pluginNames || context.args.length === 0) { + const installedPlugins = + await $pluginsService.getAllInstalledPlugins($projectData); + pluginNames = installedPlugins.map((p) => p.name); + } + + for (const pluginName of pluginNames) { + await $pluginsService.remove(pluginName, $projectData); + await $pluginsService.add(pluginName, $projectData); + } + }, }); diff --git a/lib/commands/prepare.ts b/lib/commands/prepare.ts index a6e73d3577..29780ef800 100644 --- a/lib/commands/prepare.ts +++ b/lib/commands/prepare.ts @@ -1,6 +1,5 @@ import { canExecuteCommandBase, - injectPlatformCommandServices, platformArgument, validatePlatformArgument, validatePlatformOptions, @@ -15,6 +14,8 @@ import { defineCommand, } from "../common/define-command"; import { inject } from "../common/di"; +import { IOptions } from "../declarations"; +import { IProjectData } from "../definitions/project"; export const prepareCommandOptions = { watch: booleanOption({ default: false }), @@ -23,28 +24,15 @@ export const prepareCommandOptions = { force: booleanOption(), } satisfies CommandOptionsSchema; -export type PrepareCommandContext = CommandContext< - typeof prepareCommandOptions ->; +type PrepareCommandContext = CommandContext; -export function setupPrepareCommand() { - const services = { - ...injectPlatformCommandServices(), - $prepareController: inject("prepareController"), - $prepareDataService: inject("prepareDataService"), - $migrateController: inject("migrateController"), - }; - services.$projectData.initializeProjectData(); - - return services; -} - -export type IPrepareCommandServices = ReturnType; - -export async function canExecutePrepareCommand( +async function canExecutePrepareCommand( context: PrepareCommandContext, - services: IPrepareCommandServices, ): Promise { + const $migrateController = + context.injector.get("migrateController"); + const $projectData = context.injector.get("projectData"); + const platform = context.args[0]; if (!platform) { // The declared argument validates only a platform that was passed; an @@ -52,11 +40,11 @@ export async function canExecutePrepareCommand( validatePlatformArgument(context.injector, platform); } - const result = await validatePlatformOptions(services, platform); + const result = await validatePlatformOptions(context, platform); if (!context.options.force) { - await services.$migrateController.validate({ - projectDir: services.$projectData.projectDir, + await $migrateController.validate({ + projectDir: $projectData.projectDir, platforms: [platform], }); } @@ -65,19 +53,25 @@ export async function canExecutePrepareCommand( return false; } - return canExecuteCommandBase(services, platform); + return canExecuteCommandBase(context, platform); } export async function runPrepareCommand( context: PrepareCommandContext, - services: IPrepareCommandServices, ): Promise { - const prepareData = services.$prepareDataService.getPrepareData( - services.$projectData.projectDir, + const $options = context.injector.get("options"); + const $prepareController = + context.injector.get("prepareController"); + const $prepareDataService = + context.injector.get("prepareDataService"); + const $projectData = context.injector.get("projectData"); + + const prepareData = $prepareDataService.getPrepareData( + $projectData.projectDir, context.args[0], - services.$options, + $options, ); - await services.$prepareController.prepare(prepareData); + await $prepareController.prepare(prepareData); } export const prepareCommandDefinition = defineCommand({ @@ -85,7 +79,9 @@ export const prepareCommandDefinition = defineCommand({ description: "Copies common and platform-specific content to the platform.", options: prepareCommandOptions, arguments: [platformArgument], - setup: setupPrepareCommand, + setup() { + inject("projectData").initializeProjectData(); + }, canExecute: canExecutePrepareCommand, run: runPrepareCommand, }); diff --git a/lib/commands/remove-platform.ts b/lib/commands/remove-platform.ts index f690f0455b..77b36f829d 100644 --- a/lib/commands/remove-platform.ts +++ b/lib/commands/remove-platform.ts @@ -5,66 +5,42 @@ import { IPlatformValidationService, } from "../declarations"; import { IErrors } from "../common/declarations"; -import { CommandContext, defineCommand } from "../common/define-command"; +import { defineCommand } from "../common/define-command"; import { inject } from "../common/di"; -export function setupRemovePlatformCommand() { - const services = { - $errors: inject("errors"), - $platformCommandHelper: inject( - "platformCommandHelper", - ), - $platformValidationService: inject( - "platformValidationService", - ), - $projectData: inject("projectData"), - }; - services.$projectData.initializeProjectData(); - - return services; -} - -export type IRemovePlatformCommandServices = ReturnType< - typeof setupRemovePlatformCommand ->; - -export async function canExecuteRemovePlatformCommand( - context: CommandContext, - services: IRemovePlatformCommandServices, -): Promise { - const args = context.args; - if (!args || args.length === 0) { - services.$errors.failWithHelp( - "No platform specified. Please specify a platform to remove.", - ); - } - - _.each(args, (platform) => { - services.$platformValidationService.validatePlatform( - platform, - services.$projectData, - ); - }); - - return true; -} - -export function runRemovePlatformCommand( - context: CommandContext, - services: IRemovePlatformCommandServices, -): Promise { - return services.$platformCommandHelper.removePlatforms( - context.args, - services.$projectData, - ); -} - export const removePlatformCommandDefinition = defineCommand({ name: "platform|remove", description: "Removes the selected platform from the platforms that the project currently targets.", arguments: "any", - setup: setupRemovePlatformCommand, - canExecute: canExecuteRemovePlatformCommand, - run: runRemovePlatformCommand, + async canExecute(context): Promise { + const $errors = inject("errors"); + const $platformValidationService = inject( + "platformValidationService", + ); + const $projectData = inject("projectData"); + $projectData.initializeProjectData(); + + const args = context.args; + if (!args || args.length === 0) { + $errors.failWithHelp( + "No platform specified. Please specify a platform to remove.", + ); + } + + _.each(args, (platform) => { + $platformValidationService.validatePlatform(platform, $projectData); + }); + + return true; + }, + run(context): Promise { + const $platformCommandHelper = inject( + "platformCommandHelper", + ); + const $projectData = inject("projectData"); + $projectData.initializeProjectData(); + + return $platformCommandHelper.removePlatforms(context.args, $projectData); + }, }); diff --git a/lib/commands/resources/resources-update.ts b/lib/commands/resources/resources-update.ts index d958e9905f..4dfcf765cd 100644 --- a/lib/commands/resources/resources-update.ts +++ b/lib/commands/resources/resources-update.ts @@ -1,69 +1,60 @@ import { IProjectData } from "../../definitions/project"; import { IAndroidResourcesMigrationService } from "../../declarations"; import { IErrors } from "../../common/declarations"; -import { CommandContext, defineCommand } from "../../common/define-command"; +import { defineCommand } from "../../common/define-command"; import { inject } from "../../common/di"; -export function setupResourcesUpdateCommand() { - const services = { - $projectData: inject("projectData"), - $errors: inject("errors"), - $androidResourcesMigrationService: +export const resourcesUpdateCommandDefinition = defineCommand({ + name: "resources|update", + description: + "Updates the App_Resources directory to the structure the current Android runtime expects.", + arguments: "any", + async canExecute(context): Promise { + const $androidResourcesMigrationService = inject( "androidResourcesMigrationService", - ), - }; - services.$projectData.initializeProjectData(); - - return services; -} - -export type IResourcesUpdateCommandServices = ReturnType< - typeof setupResourcesUpdateCommand ->; - -export async function canExecuteResourcesUpdateCommand( - context: CommandContext, - services: IResourcesUpdateCommandServices, -): Promise { - let args = context.args; - if (!args || args.length === 0) { - // Command defaults to migrating the Android App_Resources, unless explicitly specified. - // The default reaches this check only; the migration itself ignores the arguments. - args = ["android"]; - } - - for (const platform of args) { - if (!services.$androidResourcesMigrationService.canMigrate(platform)) { - services.$errors.fail( - `The ${platform} does not need to have its resources updated.`, ); + const $errors = inject("errors"); + const $projectData = inject("projectData"); + $projectData.initializeProjectData(); + + let args = context.args; + if (!args || args.length === 0) { + // Command defaults to migrating the Android App_Resources, unless explicitly specified. + // The default reaches this check only; the migration itself ignores the arguments. + args = ["android"]; } - if ( - services.$androidResourcesMigrationService.hasMigrated( - services.$projectData.getAppResourcesDirectoryPath(), - ) - ) { - services.$errors.fail( - "The App_Resources have already been updated for the Android platform.", - ); + for (const platform of args) { + if (!$androidResourcesMigrationService.canMigrate(platform)) { + $errors.fail( + `The ${platform} does not need to have its resources updated.`, + ); + } + + if ( + $androidResourcesMigrationService.hasMigrated( + $projectData.getAppResourcesDirectoryPath(), + ) + ) { + $errors.fail( + "The App_Resources have already been updated for the Android platform.", + ); + } } - } - return true; -} + return true; + }, + async run(): Promise { + const $androidResourcesMigrationService = + inject( + "androidResourcesMigrationService", + ); + const $projectData = inject("projectData"); + $projectData.initializeProjectData(); -export const resourcesUpdateCommandDefinition = defineCommand({ - name: "resources|update", - description: - "Updates the App_Resources directory to the structure the current Android runtime expects.", - arguments: "any", - setup: setupResourcesUpdateCommand, - canExecute: canExecuteResourcesUpdateCommand, - async run(context, services): Promise { - await services.$androidResourcesMigrationService.migrate( - services.$projectData.getAppResourcesDirectoryPath(), + await $androidResourcesMigrationService.migrate( + $projectData.getAppResourcesDirectoryPath(), ); }, }); diff --git a/lib/commands/run.ts b/lib/commands/run.ts index 1e72107be6..4d21a898ac 100644 --- a/lib/commands/run.ts +++ b/lib/commands/run.ts @@ -36,103 +36,78 @@ const runCommandOptions = { keyStoreAliasPassword: stringOption(), } satisfies CommandOptionsSchema; -export type RunCommandContext = CommandContext; - -export interface IRunCommandServices { - /** - * Undefined for `run|*all`, which targets every platform. `canExecute` - * narrows it to Android off macOS, and `run` reads whatever it settled on. - */ - platform: string; - $devicePlatformsConstants: Mobile.IDevicePlatformsConstants; - $errors: IErrors; - $hostInfo: IHostInfo; - $keyShortcutService: IKeyShortcutService; - $liveSyncCommandHelper: ILiveSyncCommandHelper; - $migrateController: IMigrateController; - $options: IOptions; - $platformValidationService: IPlatformValidationService; - $projectData: IProjectData; - $projectDataService: IProjectDataService; -} - -export function setupRunCommand(): IRunCommandServices { - return { - platform: undefined, - $devicePlatformsConstants: inject( - "devicePlatformsConstants", - ), - $errors: inject("errors"), - $hostInfo: inject("hostInfo"), - $keyShortcutService: inject("keyShortcutService"), - $liveSyncCommandHelper: inject( - "liveSyncCommandHelper", - ), - $migrateController: inject("migrateController"), - $options: inject("options"), - $platformValidationService: inject( - "platformValidationService", - ), - $projectData: inject("projectData"), - $projectDataService: inject("projectDataService"), - }; -} +type RunCommandContext = CommandContext; +/** + * Which `$devicePlatformsConstants` entry a command runs for. The constants + * stay the source of truth for the platform spelling. + */ type RunPlatform = "iOS" | "Android" | "visionOS"; -const setupPlatformRunCommand = - (platform: RunPlatform) => (): IRunCommandServices => { - const services = setupRunCommand(); - services.platform = services.$devicePlatformsConstants[platform]; - - return services; - }; +const runPlatformName = ( + context: RunCommandContext, + platform: RunPlatform, +): string => + context.injector.get( + "devicePlatformsConstants", + )[platform]; -export async function canExecuteRunCommand( +async function canExecuteRunCommand( context: RunCommandContext, - services: IRunCommandServices, + platform: string, ): Promise { - if (context.args.length) { - services.$errors.failWithHelp(ERROR_NO_VALID_SUBCOMMAND_FORMAT, "run"); - } + const $devicePlatformsConstants = + context.injector.get( + "devicePlatformsConstants", + ); + const $errors = context.injector.get("errors"); + const $liveSyncCommandHelper = context.injector.get( + "liveSyncCommandHelper", + ); + const $migrateController = + context.injector.get("migrateController"); + const $projectData = context.injector.get("projectData"); - if (!services.platform && !services.$hostInfo.isDarwin) { - services.platform = services.$devicePlatformsConstants.Android; + if (context.args.length) { + $errors.failWithHelp(ERROR_NO_VALID_SUBCOMMAND_FORMAT, "run"); } - services.$projectData.initializeProjectData(); - const platforms = services.platform - ? [services.platform] - : [ - services.$devicePlatformsConstants.Android, - services.$devicePlatformsConstants.iOS, - ]; + $projectData.initializeProjectData(); + const platforms = platform + ? [platform] + : [$devicePlatformsConstants.Android, $devicePlatformsConstants.iOS]; if (!context.options.force) { - await services.$migrateController.validate({ - projectDir: services.$projectData.projectDir, + await $migrateController.validate({ + projectDir: $projectData.projectDir, platforms, }); } - await services.$liveSyncCommandHelper.validatePlatform(services.platform); + await $liveSyncCommandHelper.validatePlatform(platform); return true; } -export async function runRunCommand( +async function runRunCommand( context: RunCommandContext, - services: IRunCommandServices, + platform: string, ): Promise { - await services.$liveSyncCommandHelper.executeCommandLiveSync( - services.platform, + const $keyShortcutService = + context.injector.get("keyShortcutService"); + const $liveSyncCommandHelper = context.injector.get( + "liveSyncCommandHelper", + ); + + await $liveSyncCommandHelper.executeCommandLiveSync( + platform, {}, ); if (process.env.NS_IS_INTERACTIVE) { - services.$keyShortcutService.attach({ + $keyShortcutService.attach({ context: { - platform: services.platform, + platform: platform, processType: "run", }, shortcuts: keyShortcuts(), @@ -145,9 +120,9 @@ export async function runRunCommand( * outright; the launch and clean keys belong to the parent that respawns * things, which is why the `ns start` table is not reused here. */ -export function runCommandShortcuts( +function runCommandShortcuts( context: RunCommandContext, - services: IRunCommandServices, + platform: string, ): KeyShortcut[] { if (process.env.NS_IS_INTERACTIVE) { // A `ns start` child is driven over IPC through the table `run` attaches @@ -155,12 +130,13 @@ export function runCommandShortcuts( return []; } - const platform = services.platform; - return [ - restartShortcut({ platform }), - restartShortcut({ platform, full: true }), - restartShortcut({ platform, forceRebuildNativeApp: true }), + restartShortcut({ platform: platform }), + restartShortcut({ platform: platform, full: true }), + restartShortcut({ + platform: platform, + forceRebuildNativeApp: true, + }), watcherShortcut(), ]; } @@ -171,7 +147,16 @@ export const runCommandDefinition = defineCommand({ options: runCommandOptions, // The base rejects arguments itself, with the sub-command message. arguments: "any", - setup: setupRunCommand, + /** + * Undefined for `run|*all`, which targets every platform, except off macOS + * where only Android can be built. It is settled here, once per invocation, + * because `canExecute` and `run` have to agree on the platform. + */ + setup(context: RunCommandContext): string { + const $hostInfo = inject("hostInfo"); + + return $hostInfo.isDarwin ? undefined : runPlatformName(context, "Android"); + }, canExecute: canExecuteRunCommand, run: runRunCommand, shortcuts: runCommandShortcuts, @@ -179,28 +164,34 @@ export const runCommandDefinition = defineCommand({ async function canExecuteApplePlatformRunCommand( context: RunCommandContext, - services: IRunCommandServices, + platform: string, ): Promise { - const projectData = services.$projectDataService.getProjectData(); + const $errors = context.injector.get("errors"); + const $options = context.injector.get("options"); + const $platformValidationService = + context.injector.get( + "platformValidationService", + ); + const $projectDataService = + context.injector.get("projectDataService"); + + const projectData = $projectDataService.getProjectData(); if ( - !services.$platformValidationService.isPlatformSupportedForOS( - services.platform, - projectData, - ) + !$platformValidationService.isPlatformSupportedForOS(platform, projectData) ) { - services.$errors.fail( - `Applications for platform ${services.platform} can not be built on this OS`, + $errors.fail( + `Applications for platform ${platform} can not be built on this OS`, ); } const result = - (await canExecuteRunCommand(context, services)) && - (await services.$platformValidationService.validateOptions( - services.$options.provision, - services.$options.teamId, + (await canExecuteRunCommand(context, platform)) && + (await $platformValidationService.validateOptions( + $options.provision, + $options.teamId, projectData, - services.platform.toLowerCase(), + platform.toLowerCase(), )); return result; } @@ -214,10 +205,15 @@ const defineApplePlatformRunCommand = ( description: "Runs your project on a connected Apple device or simulator.", options: runCommandOptions, arguments: "any", - setup: setupPlatformRunCommand(platform), - canExecute: canExecuteApplePlatformRunCommand, - run: runRunCommand, - shortcuts: runCommandShortcuts, + canExecute: (context: RunCommandContext) => + canExecuteApplePlatformRunCommand( + context, + runPlatformName(context, platform), + ), + run: (context: RunCommandContext) => + runRunCommand(context, runPlatformName(context, platform)), + shortcuts: (context: RunCommandContext) => + runCommandShortcuts(context, runPlatformName(context, platform)), }); export const iosRunCommand = defineApplePlatformRunCommand("run|ios", "iOS"); @@ -232,24 +228,28 @@ export const androidRunCommand = defineCommand({ description: "Runs your project on a connected Android device or emulator.", options: runCommandOptions, arguments: "any", - setup: setupPlatformRunCommand("Android"), - async canExecute( - context: RunCommandContext, - services: IRunCommandServices, - ): Promise { + async canExecute(context: RunCommandContext): Promise { + const $errors = inject("errors"); + const $options = inject("options"); + const $platformValidationService = inject( + "platformValidationService", + ); + const $projectData = inject("projectData"); + const platform = runPlatformName(context, "Android"); + // The base verdict is dropped rather than combined with the checks below; // the base only ever returns true or throws, so the Android command has // always relied on it for its side effects alone. - await canExecuteRunCommand(context, services); + await canExecuteRunCommand(context, platform); if ( - !services.$platformValidationService.isPlatformSupportedForOS( - services.$devicePlatformsConstants.Android, - services.$projectData, + !$platformValidationService.isPlatformSupportedForOS( + platform, + $projectData, ) ) { - services.$errors.fail( - `Applications for platform ${services.$devicePlatformsConstants.Android} can not be built on this OS`, + $errors.fail( + `Applications for platform ${platform} can not be built on this OS`, ); } @@ -258,19 +258,21 @@ export const androidRunCommand = defineCommand({ !hasValidAndroidSigning(context.options) ) { if (context.options.release) { - services.$errors.failWithHelp(ANDROID_RELEASE_BUILD_ERROR_MESSAGE); + $errors.failWithHelp(ANDROID_RELEASE_BUILD_ERROR_MESSAGE); } else { - services.$errors.failWithHelp(ANDROID_APP_BUNDLE_SIGNING_ERROR_MESSAGE); + $errors.failWithHelp(ANDROID_APP_BUNDLE_SIGNING_ERROR_MESSAGE); } } - return services.$platformValidationService.validateOptions( - services.$options.provision, - services.$options.teamId, - services.$projectData, - services.$devicePlatformsConstants.Android.toLowerCase(), + return $platformValidationService.validateOptions( + $options.provision, + $options.teamId, + $projectData, + platform.toLowerCase(), ); }, - run: runRunCommand, - shortcuts: runCommandShortcuts, + run: (context: RunCommandContext) => + runRunCommand(context, runPlatformName(context, "Android")), + shortcuts: (context: RunCommandContext) => + runCommandShortcuts(context, runPlatformName(context, "Android")), }); diff --git a/lib/commands/setup.ts b/lib/commands/setup.ts index 69e21a161e..17879c24db 100644 --- a/lib/commands/setup.ts +++ b/lib/commands/setup.ts @@ -7,10 +7,7 @@ export const setupCommandDefinition = defineCommand({ description: "Run the setup script to try to automatically configure your environment.", arguments: "none", - setup: () => ({ - $doctorService: inject("doctorService"), - }), - run(context, services): Promise { - return services.$doctorService.runSetupScript(); + run(): Promise { + return inject("doctorService").runSetupScript(); }, }); diff --git a/lib/commands/start.ts b/lib/commands/start.ts index d1ac27f997..9070b4770f 100644 --- a/lib/commands/start.ts +++ b/lib/commands/start.ts @@ -7,13 +7,11 @@ export const startCommandDefinition = defineCommand({ name: "start", description: "Starts the NativeScript interactive command line.", arguments: "any", - setup: () => ({ - $startService: inject("startService"), - }), - async run(context, services): Promise { + async run(): Promise { + const $startService = inject("startService"); printHeader(); // Left unawaited: the command returns while the service keeps running. - services.$startService.start(); + $startService.start(); return; }, }); diff --git a/lib/commands/test.ts b/lib/commands/test.ts index 2715f37d98..0829bb97ec 100644 --- a/lib/commands/test.ts +++ b/lib/commands/test.ts @@ -49,79 +49,66 @@ const testCommandOptions = { keyStoreAliasPassword: stringOption(), } satisfies CommandOptionsSchema; -export type TestCommandContext = CommandContext; +type TestCommandContext = CommandContext; -export function setupTestCommand(testPlatform: TestPlatform) { - return { - platform: testPlatform, - $analyticsService: inject("analyticsService"), - $cleanupService: inject("cleanupService"), - $devicesService: inject("devicesService"), - $errors: inject("errors"), - $liveSyncCommandHelper: inject( - "liveSyncCommandHelper", - ), - $logger: inject("logger"), - $migrateController: inject("migrateController"), - $options: inject("options"), - $platformEnvironmentRequirements: inject( - "platformEnvironmentRequirements", - ), - $projectData: inject("projectData"), - $testExecutionService: inject( - "testExecutionService", - ), - $vitestExecutionService: inject( - "vitestExecutionService", - ), - }; -} - -export type ITestCommandServices = ReturnType; - -export async function canExecuteTestCommand( +async function canExecuteTestCommand( context: TestCommandContext, - services: ITestCommandServices, + platform: TestPlatform, ): Promise { + const $analyticsService = + context.injector.get("analyticsService"); + const $cleanupService = + context.injector.get("cleanupService"); + const $errors = context.injector.get("errors"); + const $migrateController = + context.injector.get("migrateController"); + const $options = context.injector.get("options"); + const $platformEnvironmentRequirements = + context.injector.get( + "platformEnvironmentRequirements", + ); + const $projectData = context.injector.get("projectData"); + const $testExecutionService = context.injector.get( + "testExecutionService", + ); + const $vitestExecutionService = context.injector.get( + "vitestExecutionService", + ); + if (!context.options.force) { if (context.options.hmr) { // With HMR we are not restarting after LiveSync which is causing a 30 seconds app start on Android // because the Runtime does not watch for the `/data/local/tmp-livesync-in-progress` file deletion. // The App is closing itself after each test execution and the bug will be reproducible on each LiveSync. - services.$errors.fail( - "The `--hmr` option is not supported for this command.", - ); + $errors.fail("The `--hmr` option is not supported for this command."); } - await services.$migrateController.validate({ - projectDir: services.$projectData.projectDir, - platforms: [services.platform], + await $migrateController.validate({ + projectDir: $projectData.projectDir, + platforms: [platform], }); } - services.$projectData.initializeProjectData(); - services.$analyticsService.setShouldDispose( + $projectData.initializeProjectData(); + $analyticsService.setShouldDispose( context.options.justlaunch || !context.options.watch, ); - services.$cleanupService.setShouldDispose( + $cleanupService.setShouldDispose( context.options.justlaunch || !context.options.watch, ); const output = - await services.$platformEnvironmentRequirements.checkEnvironmentRequirements( - { - platform: services.platform, - projectDir: services.$projectData.projectDir, - options: services.$options, - }, - ); + await $platformEnvironmentRequirements.checkEnvironmentRequirements({ + platform, + projectDir: $projectData.projectDir, + options: $options, + }); - if (services.$vitestExecutionService.isVitestProject(services.$projectData)) { - const canStartTestRun = services.$vitestExecutionService.canStartTestRun( - services.$projectData, - ); + if ($vitestExecutionService.isVitestProject($projectData)) { + const canStartTestRun = + $vitestExecutionService.canStartTestRun($projectData); if (!canStartTestRun) { - services.$errors.fail({ + $errors.fail({ formatStr: "Error: In order to run unit tests, your project must already be configured by running $ ns test init.", errorCode: ErrorCodes.TESTS_INIT_REQUIRED, @@ -131,11 +118,9 @@ export async function canExecuteTestCommand( } const canStartKarmaServer = - await services.$testExecutionService.canStartKarmaServer( - services.$projectData, - ); + await $testExecutionService.canStartKarmaServer($projectData); if (!canStartKarmaServer) { - services.$errors.fail({ + $errors.fail({ formatStr: "Error: In order to run unit tests, your project must already be configured by running $ ns test init.", errorCode: ErrorCodes.TESTS_INIT_REQUIRED, @@ -145,57 +130,66 @@ export async function canExecuteTestCommand( return output.canExecute && canStartKarmaServer; } -export async function runTestCommand( +async function runTestCommand( context: TestCommandContext, - services: ITestCommandServices, + platform: TestPlatform, ): Promise { - if (services.$vitestExecutionService.isVitestProject(services.$projectData)) { - await services.$vitestExecutionService.startTestRun( - services.platform, - services.$projectData, - ); + const $devicesService = + context.injector.get("devicesService"); + const $liveSyncCommandHelper = context.injector.get( + "liveSyncCommandHelper", + ); + const $logger = context.injector.get("logger"); + const $options = context.injector.get("options"); + const $projectData = context.injector.get("projectData"); + const $testExecutionService = context.injector.get( + "testExecutionService", + ); + const $vitestExecutionService = context.injector.get( + "vitestExecutionService", + ); + + if ($vitestExecutionService.isVitestProject($projectData)) { + await $vitestExecutionService.startTestRun(platform, $projectData); process.exit(0); } - services.$logger.warn( + $logger.warn( "Karma-based unit testing is deprecated and will be removed in a future release. " + "Re-initialize your tests with '$ ns test init --framework vitest' to migrate.", ); let devices = []; if (context.options.debugBrk) { - await services.$devicesService.initialize({ - platform: services.platform, + await $devicesService.initialize({ + platform, deviceId: context.options.device, emulator: context.options.emulator, - skipInferPlatform: !services.platform, + skipInferPlatform: !platform, sdk: context.options.sdk, }); - const selectedDeviceForDebug = - await services.$devicesService.pickSingleDevice({ - onlyEmulators: context.options.emulator, - onlyDevices: context.options.forDevice, - deviceId: context.options.device, - }); + const selectedDeviceForDebug = await $devicesService.pickSingleDevice({ + onlyEmulators: context.options.emulator, + onlyDevices: context.options.forDevice, + deviceId: context.options.device, + }); devices = [selectedDeviceForDebug]; // const debugData = this.getDebugData(platform, projectData, deployOptions, { device: selectedDeviceForDebug.deviceInfo.identifier }); // await this.$debugService.debug(debugData, this.$options); } else { - devices = await services.$liveSyncCommandHelper.getDeviceInstances( - services.platform, - ); + devices = await $liveSyncCommandHelper.getDeviceInstances(platform); } // The bundler reads unitTesting off the shared options service, so the flag // is set there rather than on the command's own snapshot. - if (!services.$options.env) { - services.$options.env = {}; + if (!$options.env) { + $options.env = {}; } - services.$options.env.unitTesting = true; + $options.env.unitTesting = true; - const liveSyncInfo = services.$liveSyncCommandHelper.getLiveSyncData( - services.$projectData.projectDir, + const liveSyncInfo = $liveSyncCommandHelper.getLiveSyncData( + $projectData.projectDir, ); const deviceDebugMap: IDictionary = {}; @@ -205,14 +199,12 @@ export async function runTestCommand( ); const deviceDescriptors = - await services.$liveSyncCommandHelper.createDeviceDescriptors( - devices, - services.platform, - { deviceDebugMap }, - ); + await $liveSyncCommandHelper.createDeviceDescriptors(devices, platform, < + any + >{ deviceDebugMap }); - await services.$testExecutionService.startKarmaServer( - services.platform, + await $testExecutionService.startKarmaServer( + platform, liveSyncInfo, deviceDescriptors, ); @@ -226,9 +218,9 @@ export const testCommandDefinition = defineCommand({ options: testCommandOptions, // Arguments have never been rejected here, only ignored. arguments: "any", - setup: () => setupTestCommand("iOS"), - canExecute: canExecuteTestCommand, - run: runTestCommand, + canExecute: (context: TestCommandContext) => + canExecuteTestCommand(context, "iOS"), + run: (context: TestCommandContext) => runTestCommand(context, "iOS"), }); export const testAndroidCommandDefinition = defineCommand({ @@ -237,30 +229,26 @@ export const testAndroidCommandDefinition = defineCommand({ "Runs the tests in your project on connected Android devices or Android emulators.", options: testCommandOptions, arguments: "any", - setup: () => setupTestCommand("android"), - async canExecute( - context: TestCommandContext, - services: ITestCommandServices, - ): Promise { - const canExecuteBase = await canExecuteTestCommand(context, services); + async canExecute(context: TestCommandContext): Promise { + const $errors = inject("errors"); + + const canExecuteBase = await canExecuteTestCommand(context, "android"); if (canExecuteBase) { if ( (context.options.release || context.options.aab) && !hasValidAndroidSigning(context.options) ) { if (context.options.release) { - services.$errors.failWithHelp(ANDROID_RELEASE_BUILD_ERROR_MESSAGE); + $errors.failWithHelp(ANDROID_RELEASE_BUILD_ERROR_MESSAGE); } else { - services.$errors.failWithHelp( - ANDROID_APP_BUNDLE_SIGNING_ERROR_MESSAGE, - ); + $errors.failWithHelp(ANDROID_APP_BUNDLE_SIGNING_ERROR_MESSAGE); } } } return canExecuteBase; }, - run: runTestCommand, + run: (context: TestCommandContext) => runTestCommand(context, "android"), }); export const testVisionOSCommandDefinition = defineCommand({ @@ -269,23 +257,23 @@ export const testVisionOSCommandDefinition = defineCommand({ "Runs the tests in your project in the visionOS Simulator or on connected Apple Vision Pro devices.", options: testCommandOptions, arguments: "any", - setup: () => setupTestCommand("visionOS"), - async canExecute( - context: TestCommandContext, - services: ITestCommandServices, - ): Promise { - services.$projectData.initializeProjectData(); + async canExecute(context: TestCommandContext): Promise { + const $errors = inject("errors"); + const $projectData = inject("projectData"); + const $vitestExecutionService = inject( + "vitestExecutionService", + ); + + $projectData.initializeProjectData(); // The Karma runner (v4 line) never supported visionOS — only the Vitest // path can drive it. - if ( - !services.$vitestExecutionService.isVitestProject(services.$projectData) - ) { - services.$errors.fail( + if (!$vitestExecutionService.isVitestProject($projectData)) { + $errors.fail( "visionOS unit testing requires the Vitest runner. Run '$ ns test init --framework vitest' to configure your project.", ); } - return canExecuteTestCommand(context, services); + return canExecuteTestCommand(context, "visionOS"); }, - run: runTestCommand, + run: (context: TestCommandContext) => runTestCommand(context, "visionOS"), }); diff --git a/lib/commands/widget.ts b/lib/commands/widget.ts index 033d6942ff..1347e3ba4e 100644 --- a/lib/commands/widget.ts +++ b/lib/commands/widget.ts @@ -894,35 +894,29 @@ declare class AppleWidgetUtils extends NSObject { } } -interface IWidgetCommandServices { - generator: IOSWidgetGenerator; -} - // No flat "widget": the subcommand registration below synthesizes the parent // dispatcher. export const widgetIOSCommandDefinition = defineCommand({ name: "widget|ios", description: "Generates an iOS widget extension for the project.", arguments: "any", - setup(): IWidgetCommandServices { - const $projectData = inject("projectData"); - $projectData.initializeProjectData(); - - return { - generator: new IOSWidgetGenerator( - $projectData, - inject("projectConfigService"), - inject("logger"), - inject("errors"), - ), - }; - }, - canExecute(): boolean { - return true; + // In setup, not run: it lands ahead of the arguments policy, so being + // outside a project is what a bad invocation reports first. + setup(): void { + inject("projectData").initializeProjectData(); }, - run(context, services: IWidgetCommandServices): void { + run(ctx): void { + const $projectData = inject("projectData"); + + const generator = new IOSWidgetGenerator( + $projectData, + inject("projectConfigService"), + inject("logger"), + inject("errors"), + ); + // Not awaited: the command has always reported completion before the // prompts it opens are answered. - services.generator.startPrompt(context.args); + generator.startPrompt(ctx.args); }, }); diff --git a/lib/common/commands/analytics.ts b/lib/common/commands/analytics.ts index 3fee2c4041..ee1a4c50f7 100644 --- a/lib/common/commands/analytics.ts +++ b/lib/common/commands/analytics.ts @@ -18,30 +18,13 @@ interface IAnalyticsSetting { humanReadableSettingName: string; } -export const analyticsCommandOptions = { +const analyticsCommandOptions = { json: booleanOption(), } satisfies CommandOptionsSchema; -export type AnalyticsCommandContext = CommandContext< - typeof analyticsCommandOptions ->; +type AnalyticsCommandContext = CommandContext; -export function setupAnalyticsCommand(setting: IAnalyticsSetting) { - const $staticConfig = inject("staticConfig"); - - return { - settingName: $staticConfig[setting.staticConfigKey], - humanReadableSettingName: setting.humanReadableSettingName, - $analyticsService: inject("analyticsService"), - $logger: inject("logger"), - }; -} - -export type IAnalyticsCommandServices = ReturnType< - typeof setupAnalyticsCommand ->; - -export function validateAnalyticsState(value: string): boolean | string { +function validateAnalyticsState(value: string): boolean | string { switch ((value || "").toLowerCase()) { case "enable": case "disable": @@ -53,33 +36,35 @@ export function validateAnalyticsState(value: string): boolean | string { } } -export async function runAnalyticsCommand( +async function runAnalyticsCommand( context: AnalyticsCommandContext, - services: IAnalyticsCommandServices, + setting: IAnalyticsSetting, ): Promise { + const $analyticsService = inject("analyticsService"); + const $logger = inject("logger"); + const $staticConfig = inject("staticConfig"); + const settingName = $staticConfig[setting.staticConfigKey]; + const { humanReadableSettingName } = setting; + const arg = context.args[0] || ""; switch (arg.toLowerCase()) { case "enable": - await services.$analyticsService.setStatus(services.settingName, true); + await $analyticsService.setStatus(settingName, true); // TODO(Analytics): await this.$analyticsService.track(this.settingName, "enabled"); - services.$logger.info( - `${services.humanReadableSettingName} is now enabled.`, - ); + $logger.info(`${humanReadableSettingName} is now enabled.`); break; case "disable": // TODO(Analytics): await this.$analyticsService.track(this.settingName, "disabled"); - await services.$analyticsService.setStatus(services.settingName, false); - services.$logger.info( - `${services.humanReadableSettingName} is now disabled.`, - ); + await $analyticsService.setStatus(settingName, false); + $logger.info(`${humanReadableSettingName} is now disabled.`); break; case "status": case "": - services.$logger.info( - await services.$analyticsService.getStatusMessage( - services.settingName, + $logger.info( + await $analyticsService.getStatusMessage( + settingName, context.options.json, - services.humanReadableSettingName, + humanReadableSettingName, ), ); break; @@ -96,8 +81,7 @@ const defineAnalyticsCommand = ( options: analyticsCommandOptions, arguments: [{ name: "state", validate: validateAnalyticsState }], disableAnalytics: true, - setup: () => setupAnalyticsCommand(setting), - run: runAnalyticsCommand, + run: (context) => runAnalyticsCommand(context, setting), }); export const usageReportingCommand = defineAnalyticsCommand("usage-reporting", { diff --git a/lib/common/commands/autocompletion.ts b/lib/common/commands/autocompletion.ts index 774b75be67..1e47462689 100644 --- a/lib/common/commands/autocompletion.ts +++ b/lib/common/commands/autocompletion.ts @@ -3,52 +3,41 @@ import { IAutoCompletionService } from "../declarations"; import { defineCommand } from "../define-command"; import { inject } from "../di"; -export function injectAutoCompleteCommandServices() { - return { - $autoCompletionService: inject( - "autoCompletionService", - ), - $logger: inject("logger"), - }; -} - -export type IAutoCompleteCommandServices = ReturnType< - typeof injectAutoCompleteCommandServices ->; - export const autoCompleteCommandDefinition = defineCommand({ name: "autocomplete|*default", description: "Prompts to enable command-line completion for the CLI.", arguments: "none", disableAnalytics: true, - setup: () => ({ - ...injectAutoCompleteCommandServices(), - $prompter: inject("prompter"), - }), - async run(context, services): Promise { + async run(): Promise { + const $autoCompletionService = inject( + "autoCompletionService", + ); + const $logger = inject("logger"); + const $prompter = inject("prompter"); + if (helpers.isInteractive()) { - if (services.$autoCompletionService.isAutoCompletionEnabled()) { - if (services.$autoCompletionService.isObsoleteAutoCompletionEnabled()) { + if ($autoCompletionService.isAutoCompletionEnabled()) { + if ($autoCompletionService.isObsoleteAutoCompletionEnabled()) { // obsolete autocompletion is enabled, update it to the new one: - await services.$autoCompletionService.enableAutoCompletion(); + await $autoCompletionService.enableAutoCompletion(); } else { - services.$logger.info("Autocompletion is already enabled"); + $logger.info("Autocompletion is already enabled"); } } else { - services.$logger.info( + $logger.info( "If you are using bash or zsh, you can enable command-line completion.", ); const message = "Do you want to enable it now?"; - const autoCompetionStatus = await services.$prompter.confirm( + const autoCompetionStatus = await $prompter.confirm( message, () => true, ); if (autoCompetionStatus) { - await services.$autoCompletionService.enableAutoCompletion(); + await $autoCompletionService.enableAutoCompletion(); } else { // make sure we've removed all autocompletion code from all shell profiles - services.$autoCompletionService.disableAutoCompletion(); + $autoCompletionService.disableAutoCompletion(); } } } @@ -60,12 +49,16 @@ export const disableAutoCompleteCommandDefinition = defineCommand({ description: "Disables command-line completion for the CLI.", arguments: "none", disableAnalytics: true, - setup: injectAutoCompleteCommandServices, - async run(context, services): Promise { - if (services.$autoCompletionService.isAutoCompletionEnabled()) { - services.$autoCompletionService.disableAutoCompletion(); + async run(): Promise { + const $autoCompletionService = inject( + "autoCompletionService", + ); + const $logger = inject("logger"); + + if ($autoCompletionService.isAutoCompletionEnabled()) { + $autoCompletionService.disableAutoCompletion(); } else { - services.$logger.info("Autocompletion is already disabled."); + $logger.info("Autocompletion is already disabled."); } }, }); @@ -75,12 +68,16 @@ export const enableAutoCompleteCommandDefinition = defineCommand({ description: "Enables command-line completion for the CLI.", arguments: "none", disableAnalytics: true, - setup: injectAutoCompleteCommandServices, - async run(context, services): Promise { - if (services.$autoCompletionService.isAutoCompletionEnabled()) { - services.$logger.info("Autocompletion is already enabled."); + async run(): Promise { + const $autoCompletionService = inject( + "autoCompletionService", + ); + const $logger = inject("logger"); + + if ($autoCompletionService.isAutoCompletionEnabled()) { + $logger.info("Autocompletion is already enabled."); } else { - await services.$autoCompletionService.enableAutoCompletion(); + await $autoCompletionService.enableAutoCompletion(); } }, }); @@ -90,12 +87,16 @@ export const autoCompleteStatusCommandDefinition = defineCommand({ description: "Prints whether command-line completion is enabled.", arguments: "none", disableAnalytics: true, - setup: injectAutoCompleteCommandServices, - async run(context, services): Promise { - if (services.$autoCompletionService.isAutoCompletionEnabled()) { - services.$logger.info("Autocompletion is enabled."); + async run(): Promise { + const $autoCompletionService = inject( + "autoCompletionService", + ); + const $logger = inject("logger"); + + if ($autoCompletionService.isAutoCompletionEnabled()) { + $logger.info("Autocompletion is enabled."); } else { - services.$logger.info("Autocompletion is disabled."); + $logger.info("Autocompletion is disabled."); } }, }); diff --git a/lib/common/commands/device/device-log-stream.ts b/lib/common/commands/device/device-log-stream.ts index ccc7f7ff12..730458e4c6 100644 --- a/lib/common/commands/device/device-log-stream.ts +++ b/lib/common/commands/device/device-log-stream.ts @@ -1,7 +1,6 @@ import { ICleanupService } from "../../../definitions/cleanup-service"; import { IErrors } from "../../declarations"; import { - CommandContext, CommandOptionsSchema, defineCommand, stringOption, @@ -15,58 +14,41 @@ const openDeviceLogStreamCommandOptions = { device: stringOption(), } satisfies CommandOptionsSchema; -export type OpenDeviceLogStreamCommandContext = CommandContext< - typeof openDeviceLogStreamCommandOptions ->; - -export function setupOpenDeviceLogStreamCommand() { - // The log stream is the command's whole output, so neither the simulator log - // provider nor the cleanup process may be torn down while it is open. The - // legacy command did this from its constructor, which ran before anything - // looked at the command line. - inject( - "iOSSimulatorLogProvider", - ).setShouldDispose(false); - inject("cleanupService").setShouldDispose(false); - - return { - $commandsService: inject("commandsService"), - $deviceLogProvider: inject("deviceLogProvider"), - $devicesService: inject("devicesService"), - $errors: inject("errors"), - $loggingLevels: inject("loggingLevels"), - }; -} - -export type IOpenDeviceLogStreamCommandServices = ReturnType< - typeof setupOpenDeviceLogStreamCommand ->; - -export async function runOpenDeviceLogStreamCommand( - context: OpenDeviceLogStreamCommandContext, - services: IOpenDeviceLogStreamCommandServices, -): Promise { - services.$deviceLogProvider.setLogLevel(services.$loggingLevels.full); - - await services.$devicesService.initialize({ - deviceId: context.options.device, - skipInferPlatform: true, - }); - - if (services.$devicesService.deviceCount > 1) { - await services.$commandsService.tryExecuteCommand("device", []); - services.$errors.failWithHelp(NOT_SPECIFIED_DEVICE_ERROR_MESSAGE); - } - - const action = (device: Mobile.IiOSDevice) => device.openDeviceLogStream(); - await services.$devicesService.execute(action); -} - export const openDeviceLogStreamCommandDefinition = defineCommand({ name: ["device|log", "devices|log"], description: "Opens the device log stream for a connected device.", options: openDeviceLogStreamCommandOptions, arguments: "none", - setup: setupOpenDeviceLogStreamCommand, - run: runOpenDeviceLogStreamCommand, + // The log stream is the command's whole output, so neither the simulator log + // provider nor the cleanup process may be torn down while it is open. In + // setup, so the flags are set at the point in the invocation they always were. + setup(): void { + inject( + "iOSSimulatorLogProvider", + ).setShouldDispose(false); + inject("cleanupService").setShouldDispose(false); + }, + async run(context): Promise { + const $commandsService = inject("commandsService"); + const $deviceLogProvider = + inject("deviceLogProvider"); + const $devicesService = inject("devicesService"); + const $errors = inject("errors"); + const $loggingLevels = inject("loggingLevels"); + + $deviceLogProvider.setLogLevel($loggingLevels.full); + + await $devicesService.initialize({ + deviceId: context.options.device, + skipInferPlatform: true, + }); + + if ($devicesService.deviceCount > 1) { + await $commandsService.tryExecuteCommand("device", []); + $errors.failWithHelp(NOT_SPECIFIED_DEVICE_ERROR_MESSAGE); + } + + const action = (device: Mobile.IiOSDevice) => device.openDeviceLogStream(); + await $devicesService.execute(action); + }, }); diff --git a/lib/common/commands/device/get-file.ts b/lib/common/commands/device/get-file.ts index 7265fe5f71..c6c2a2d760 100644 --- a/lib/common/commands/device/get-file.ts +++ b/lib/common/commands/device/get-file.ts @@ -1,7 +1,6 @@ import { IProjectData } from "../../../definitions/project"; import { IErrors } from "../../declarations"; import { - CommandContext, CommandOptionsSchema, defineCommand, stringOption, @@ -13,63 +12,47 @@ const getFileCommandOptions = { file: stringOption(), } satisfies CommandOptionsSchema; -export type GetFileCommandContext = CommandContext< - typeof getFileCommandOptions ->; - -export function setupGetFileCommand() { - return { - $devicesService: inject("devicesService"), - $errors: inject("errors"), - $projectData: inject("projectData"), - }; -} - -export type IGetFileCommandServices = ReturnType; - -export async function runGetFileCommand( - context: GetFileCommandContext, - services: IGetFileCommandServices, -): Promise { - await services.$devicesService.initialize({ - deviceId: context.options.device, - skipInferPlatform: true, - }); - let appIdentifier = context.args[1]; - - if (!appIdentifier) { - try { - services.$projectData.initializeProjectData(); - } catch (err) { - // ignore the error - } - if (!services.$projectData.projectIdentifiers) { - services.$errors.fail( - "Please enter application identifier or execute this command in project.", - ); - } - } - - const action = async (device: Mobile.IDevice) => { - appIdentifier = - appIdentifier || - services.$projectData.projectIdentifiers[ - device.deviceInfo.platform.toLowerCase() - ]; - await device.fileSystem.getFile( - context.args[0], - appIdentifier, - context.options.file, - ); - }; - await services.$devicesService.execute(action); -} - export const getFileCommandDefinition = defineCommand({ name: ["device|get-file", "devices|get-file"], description: "Downloads a file from a connected device.", options: getFileCommandOptions, arguments: [{ name: "path" }, { name: "appId" }], - setup: setupGetFileCommand, - run: runGetFileCommand, + async run(context): Promise { + const $devicesService = inject("devicesService"); + const $errors = inject("errors"); + const $projectData = inject("projectData"); + + await $devicesService.initialize({ + deviceId: context.options.device, + skipInferPlatform: true, + }); + let appIdentifier = context.args[1]; + + if (!appIdentifier) { + try { + $projectData.initializeProjectData(); + } catch (err) { + // ignore the error + } + if (!$projectData.projectIdentifiers) { + $errors.fail( + "Please enter application identifier or execute this command in project.", + ); + } + } + + const action = async (device: Mobile.IDevice) => { + appIdentifier = + appIdentifier || + $projectData.projectIdentifiers[ + device.deviceInfo.platform.toLowerCase() + ]; + await device.fileSystem.getFile( + context.args[0], + appIdentifier, + context.options.file, + ); + }; + await $devicesService.execute(action); + }, }); diff --git a/lib/common/commands/device/list-applications.ts b/lib/common/commands/device/list-applications.ts index d67a9fc058..2b2a375353 100644 --- a/lib/common/commands/device/list-applications.ts +++ b/lib/common/commands/device/list-applications.ts @@ -2,7 +2,6 @@ import * as _ from "lodash"; import { EOL } from "os"; import * as util from "util"; import { - CommandContext, CommandOptionsSchema, defineCommand, stringOption, @@ -13,53 +12,37 @@ const listApplicationsCommandOptions = { device: stringOption(), } satisfies CommandOptionsSchema; -export type ListApplicationsCommandContext = CommandContext< - typeof listApplicationsCommandOptions ->; - -export function setupListApplicationsCommand() { - return { - $devicesService: inject("devicesService"), - $logger: inject("logger"), - }; -} - -export type IListApplicationsCommandServices = ReturnType< - typeof setupListApplicationsCommand ->; - -export async function runListApplicationsCommand( - context: ListApplicationsCommandContext, - services: IListApplicationsCommandServices, -): Promise { - await services.$devicesService.initialize({ - deviceId: context.options.device, - skipInferPlatform: true, - }); - const output: string[] = []; - - const action = async (device: Mobile.IDevice) => { - const applications = - await device.applicationManager.getInstalledApplications(); - output.push( - util.format( - "%s=====Installed applications on device with UDID '%s' are:", - EOL, - device.deviceInfo.identifier, - ), - ); - _.each(applications, (applicationId: string) => output.push(applicationId)); - }; - await services.$devicesService.execute(action); - - services.$logger.info(output.join(EOL)); -} - export const listApplicationsCommandDefinition = defineCommand({ name: ["device|list-applications", "devices|list-applications"], description: "Lists the installed applications on all connected devices.", options: listApplicationsCommandOptions, arguments: "none", - setup: setupListApplicationsCommand, - run: runListApplicationsCommand, + async run(context): Promise { + const $devicesService = inject("devicesService"); + const $logger = inject("logger"); + + await $devicesService.initialize({ + deviceId: context.options.device, + skipInferPlatform: true, + }); + const output: string[] = []; + + const action = async (device: Mobile.IDevice) => { + const applications = + await device.applicationManager.getInstalledApplications(); + output.push( + util.format( + "%s=====Installed applications on device with UDID '%s' are:", + EOL, + device.deviceInfo.identifier, + ), + ); + _.each(applications, (applicationId: string) => + output.push(applicationId), + ); + }; + await $devicesService.execute(action); + + $logger.info(output.join(EOL)); + }, }); diff --git a/lib/common/commands/device/list-files.ts b/lib/common/commands/device/list-files.ts index 37831a6cca..c20e591cd9 100644 --- a/lib/common/commands/device/list-files.ts +++ b/lib/common/commands/device/list-files.ts @@ -1,7 +1,6 @@ import { IProjectData } from "../../../definitions/project"; import { IErrors } from "../../declarations"; import { - CommandContext, CommandOptionsSchema, defineCommand, stringOption, @@ -12,62 +11,44 @@ const listFilesCommandOptions = { device: stringOption(), } satisfies CommandOptionsSchema; -export type ListFilesCommandContext = CommandContext< - typeof listFilesCommandOptions ->; - -export function setupListFilesCommand() { - return { - $devicesService: inject("devicesService"), - $errors: inject("errors"), - $projectData: inject("projectData"), - }; -} - -export type IListFilesCommandServices = ReturnType< - typeof setupListFilesCommand ->; - -export async function runListFilesCommand( - context: ListFilesCommandContext, - services: IListFilesCommandServices, -): Promise { - await services.$devicesService.initialize({ - deviceId: context.options.device, - skipInferPlatform: true, - }); - const pathToList = context.args[0]; - let appIdentifier = context.args[1]; - - if (!appIdentifier) { - try { - services.$projectData.initializeProjectData(); - } catch (err) { - // ignore the error - } - if (!services.$projectData.projectIdentifiers) { - services.$errors.fail( - "Please enter application identifier or execute this command in project.", - ); - } - } - - const action = async (device: Mobile.IDevice) => { - appIdentifier = - appIdentifier || - services.$projectData.projectIdentifiers[ - device.deviceInfo.platform.toLowerCase() - ]; - await device.fileSystem.listFiles(pathToList, appIdentifier); - }; - await services.$devicesService.execute(action); -} - export const listFilesCommandDefinition = defineCommand({ name: ["device|list-files", "devices|list-files"], description: "Lists the files in a directory on a connected device.", options: listFilesCommandOptions, arguments: [{ name: "path" }, { name: "appId" }], - setup: setupListFilesCommand, - run: runListFilesCommand, + async run(context): Promise { + const $devicesService = inject("devicesService"); + const $errors = inject("errors"); + const $projectData = inject("projectData"); + + await $devicesService.initialize({ + deviceId: context.options.device, + skipInferPlatform: true, + }); + const pathToList = context.args[0]; + let appIdentifier = context.args[1]; + + if (!appIdentifier) { + try { + $projectData.initializeProjectData(); + } catch (err) { + // ignore the error + } + if (!$projectData.projectIdentifiers) { + $errors.fail( + "Please enter application identifier or execute this command in project.", + ); + } + } + + const action = async (device: Mobile.IDevice) => { + appIdentifier = + appIdentifier || + $projectData.projectIdentifiers[ + device.deviceInfo.platform.toLowerCase() + ]; + await device.fileSystem.listFiles(pathToList, appIdentifier); + }; + await $devicesService.execute(action); + }, }); diff --git a/lib/common/commands/device/put-file.ts b/lib/common/commands/device/put-file.ts index 043f4fc86d..bd658db719 100644 --- a/lib/common/commands/device/put-file.ts +++ b/lib/common/commands/device/put-file.ts @@ -1,7 +1,6 @@ import { IProjectData } from "../../../definitions/project"; import { IErrors } from "../../declarations"; import { - CommandContext, CommandOptionsSchema, defineCommand, stringOption, @@ -12,63 +11,47 @@ const putFileCommandOptions = { device: stringOption(), } satisfies CommandOptionsSchema; -export type PutFileCommandContext = CommandContext< - typeof putFileCommandOptions ->; - -export function setupPutFileCommand() { - return { - $devicesService: inject("devicesService"), - $errors: inject("errors"), - $projectData: inject("projectData"), - }; -} - -export type IPutFileCommandServices = ReturnType; - -export async function runPutFileCommand( - context: PutFileCommandContext, - services: IPutFileCommandServices, -): Promise { - await services.$devicesService.initialize({ - deviceId: context.options.device, - skipInferPlatform: true, - }); - let appIdentifier = context.args[2]; - - if (!appIdentifier) { - try { - services.$projectData.initializeProjectData(); - } catch (err) { - // ignore the error - } - if (!services.$projectData.projectIdentifiers) { - services.$errors.fail( - "Please enter application identifier or execute this command in project.", - ); - } - } - - const action = async (device: Mobile.IDevice) => { - appIdentifier = - appIdentifier || - services.$projectData.projectIdentifiers[ - device.deviceInfo.platform.toLowerCase() - ]; - await device.fileSystem.putFile( - context.args[0], - context.args[1], - appIdentifier, - ); - }; - await services.$devicesService.execute(action); -} - export const putFileCommandDefinition = defineCommand({ name: ["device|put-file", "devices|put-file"], description: "Uploads a file to a connected device.", options: putFileCommandOptions, arguments: [{ name: "localPath" }, { name: "devicePath" }, { name: "appId" }], - setup: setupPutFileCommand, - run: runPutFileCommand, + async run(context): Promise { + const $devicesService = inject("devicesService"); + const $errors = inject("errors"); + const $projectData = inject("projectData"); + + await $devicesService.initialize({ + deviceId: context.options.device, + skipInferPlatform: true, + }); + let appIdentifier = context.args[2]; + + if (!appIdentifier) { + try { + $projectData.initializeProjectData(); + } catch (err) { + // ignore the error + } + if (!$projectData.projectIdentifiers) { + $errors.fail( + "Please enter application identifier or execute this command in project.", + ); + } + } + + const action = async (device: Mobile.IDevice) => { + appIdentifier = + appIdentifier || + $projectData.projectIdentifiers[ + device.deviceInfo.platform.toLowerCase() + ]; + await device.fileSystem.putFile( + context.args[0], + context.args[1], + appIdentifier, + ); + }; + await $devicesService.execute(action); + }, }); diff --git a/lib/common/commands/device/run-application.ts b/lib/common/commands/device/run-application.ts index b56f10c32a..78b97ed2ad 100644 --- a/lib/common/commands/device/run-application.ts +++ b/lib/common/commands/device/run-application.ts @@ -1,6 +1,5 @@ import { IErrors } from "../../declarations"; import { - CommandContext, CommandOptionsSchema, defineCommand, stringOption, @@ -11,53 +10,35 @@ const runApplicationOnDeviceCommandOptions = { device: stringOption(), } satisfies CommandOptionsSchema; -export type RunApplicationOnDeviceCommandContext = CommandContext< - typeof runApplicationOnDeviceCommandOptions ->; - -export function setupRunApplicationOnDeviceCommand() { - return { - $devicesService: inject("devicesService"), - $errors: inject("errors"), - $staticConfig: inject("staticConfig"), - }; -} - -export type IRunApplicationOnDeviceCommandServices = ReturnType< - typeof setupRunApplicationOnDeviceCommand ->; - -export async function runRunApplicationOnDeviceCommand( - context: RunApplicationOnDeviceCommandContext, - services: IRunApplicationOnDeviceCommandServices, -): Promise { - await services.$devicesService.initialize({ - deviceId: context.options.device, - skipInferPlatform: true, - }); - - if (services.$devicesService.deviceCount > 1) { - services.$errors.failWithHelp( - "More than one device found. Specify device explicitly with --device option. To discover device ID, use $%s device command.", - services.$staticConfig.CLIENT_NAME.toLowerCase(), - ); - } - - await services.$devicesService.execute( - async (device: Mobile.IDevice) => - await device.applicationManager.startApplication({ - appId: context.args[0], - projectName: context.args[1], - projectDir: null, - }), - ); -} - export const runApplicationOnDeviceCommandDefinition = defineCommand({ name: ["device|run", "devices|run"], description: "Runs the selected application on a connected device.", options: runApplicationOnDeviceCommandOptions, arguments: [{ name: "appId" }, { name: "projectName" }], - setup: setupRunApplicationOnDeviceCommand, - run: runRunApplicationOnDeviceCommand, + async run(context): Promise { + const $devicesService = inject("devicesService"); + const $errors = inject("errors"); + const $staticConfig = inject("staticConfig"); + + await $devicesService.initialize({ + deviceId: context.options.device, + skipInferPlatform: true, + }); + + if ($devicesService.deviceCount > 1) { + $errors.failWithHelp( + "More than one device found. Specify device explicitly with --device option. To discover device ID, use $%s device command.", + $staticConfig.CLIENT_NAME.toLowerCase(), + ); + } + + await $devicesService.execute( + async (device: Mobile.IDevice) => + await device.applicationManager.startApplication({ + appId: context.args[0], + projectName: context.args[1], + projectDir: null, + }), + ); + }, }); diff --git a/lib/common/commands/device/stop-application.ts b/lib/common/commands/device/stop-application.ts index d151c82538..e0463ca2df 100644 --- a/lib/common/commands/device/stop-application.ts +++ b/lib/common/commands/device/stop-application.ts @@ -1,5 +1,4 @@ import { - CommandContext, CommandOptionsSchema, defineCommand, stringOption, @@ -10,44 +9,26 @@ const stopApplicationOnDeviceCommandOptions = { device: stringOption(), } satisfies CommandOptionsSchema; -export type StopApplicationOnDeviceCommandContext = CommandContext< - typeof stopApplicationOnDeviceCommandOptions ->; - -export function setupStopApplicationOnDeviceCommand() { - return { - $devicesService: inject("devicesService"), - }; -} - -export type IStopApplicationOnDeviceCommandServices = ReturnType< - typeof setupStopApplicationOnDeviceCommand ->; - -export async function runStopApplicationOnDeviceCommand( - context: StopApplicationOnDeviceCommandContext, - services: IStopApplicationOnDeviceCommandServices, -): Promise { - await services.$devicesService.initialize({ - deviceId: context.options.device, - skipInferPlatform: true, - platform: context.args[1], - }); - - const action = (device: Mobile.IDevice) => - device.applicationManager.stopApplication({ - appId: context.args[0], - projectName: context.args[2], - projectDir: null, - }); - await services.$devicesService.execute(action); -} - export const stopApplicationOnDeviceCommandDefinition = defineCommand({ name: ["device|stop", "devices|stop"], description: "Stops the selected application on a connected device.", options: stopApplicationOnDeviceCommandOptions, arguments: [{ name: "appId" }, { name: "platform" }, { name: "projectName" }], - setup: setupStopApplicationOnDeviceCommand, - run: runStopApplicationOnDeviceCommand, + async run(context): Promise { + const $devicesService = inject("devicesService"); + + await $devicesService.initialize({ + deviceId: context.options.device, + skipInferPlatform: true, + platform: context.args[1], + }); + + const action = (device: Mobile.IDevice) => + device.applicationManager.stopApplication({ + appId: context.args[0], + projectName: context.args[2], + projectDir: null, + }); + await $devicesService.execute(action); + }, }); diff --git a/lib/common/commands/device/uninstall-application.ts b/lib/common/commands/device/uninstall-application.ts index ca14a530e4..9d90e88f53 100644 --- a/lib/common/commands/device/uninstall-application.ts +++ b/lib/common/commands/device/uninstall-application.ts @@ -1,5 +1,4 @@ import { - CommandContext, CommandOptionsSchema, defineCommand, stringOption, @@ -10,39 +9,21 @@ const uninstallApplicationCommandOptions = { device: stringOption(), } satisfies CommandOptionsSchema; -export type UninstallApplicationCommandContext = CommandContext< - typeof uninstallApplicationCommandOptions ->; - -export function setupUninstallApplicationCommand() { - return { - $devicesService: inject("devicesService"), - }; -} - -export type IUninstallApplicationCommandServices = ReturnType< - typeof setupUninstallApplicationCommand ->; - -export async function runUninstallApplicationCommand( - context: UninstallApplicationCommandContext, - services: IUninstallApplicationCommandServices, -): Promise { - await services.$devicesService.initialize({ - deviceId: context.options.device, - skipInferPlatform: true, - }); - - const action = (device: Mobile.IDevice) => - device.applicationManager.uninstallApplication(context.args[0]); - await services.$devicesService.execute(action); -} - export const uninstallApplicationCommandDefinition = defineCommand({ name: ["device|uninstall", "devices|uninstall"], description: "Uninstalls an application from all connected devices.", options: uninstallApplicationCommandOptions, arguments: [{ name: "appId" }], - setup: setupUninstallApplicationCommand, - run: runUninstallApplicationCommand, + async run(context): Promise { + const $devicesService = inject("devicesService"); + + await $devicesService.initialize({ + deviceId: context.options.device, + skipInferPlatform: true, + }); + + const action = (device: Mobile.IDevice) => + device.applicationManager.uninstallApplication(context.args[0]); + await $devicesService.execute(action); + }, }); diff --git a/lib/common/commands/doctor.ts b/lib/common/commands/doctor.ts index 1657545889..5d0b04fe3d 100644 --- a/lib/common/commands/doctor.ts +++ b/lib/common/commands/doctor.ts @@ -3,16 +3,6 @@ import { CommandName, defineCommand } from "../define-command"; import { inject } from "../di"; import { PlatformTypes } from "../../constants"; -export function setupDoctorCommand(platform?: PlatformTypes) { - return { - platform, - $doctorService: inject("doctorService"), - $projectHelper: inject("projectHelper"), - }; -} - -export type IDoctorCommandServices = ReturnType; - const defineDoctorCommand = ( name: TName, platform?: PlatformTypes, @@ -22,13 +12,15 @@ const defineDoctorCommand = ( description: "Checks the local environment for configuration issues, and prints what it finds.", arguments: "none", - setup: () => setupDoctorCommand(platform), - run(context, services): Promise { - return services.$doctorService.printWarnings({ + run(): Promise { + const $doctorService = inject("doctorService"); + const $projectHelper = inject("projectHelper"); + + return $doctorService.printWarnings({ trackResult: false, - projectDir: services.$projectHelper.projectDir, + projectDir: $projectHelper.projectDir, forceCheck: true, - ...(services.platform ? { platform: services.platform } : {}), + ...(platform ? { platform } : {}), }); }, }); diff --git a/lib/common/commands/generate-messages.ts b/lib/common/commands/generate-messages.ts index 80cc52dfb5..e508491a86 100644 --- a/lib/common/commands/generate-messages.ts +++ b/lib/common/commands/generate-messages.ts @@ -19,14 +19,13 @@ export const generateMessagesCommandDefinition = defineCommand({ description: "Regenerates the CLI's message contracts.", options: generateMessagesCommandOptions, arguments: "none", - setup: () => ({ - $fs: inject("fs"), - $messageContractGenerator: inject( + async run(context): Promise { + const $fs = inject("fs"); + const $messageContractGenerator = inject( "messageContractGenerator", - ), - }), - async run(context, services): Promise { - const result = await services.$messageContractGenerator.generate(); + ); + + const result = await $messageContractGenerator.generate(); const innerMessagesDirectory = path.join(__dirname, "../messages"); const outerMessagesDirectory = path.join(__dirname, "../.."); let interfaceFilePath: string; @@ -52,7 +51,7 @@ export const generateMessagesCommandDefinition = defineCommand({ ); } - services.$fs.writeFile(interfaceFilePath, result.interfaceFile); - services.$fs.writeFile(implementationFilePath, result.implementationFile); + $fs.writeFile(interfaceFilePath, result.interfaceFile); + $fs.writeFile(implementationFilePath, result.implementationFile); }, }); diff --git a/lib/common/commands/help.ts b/lib/common/commands/help.ts index 25b430dced..f04f04dccb 100644 --- a/lib/common/commands/help.ts +++ b/lib/common/commands/help.ts @@ -1,66 +1,44 @@ import * as _ from "lodash"; import { CommandRegistry } from "../contracts/command-registry"; import { IHelpService } from "../declarations"; -import { - booleanOption, - CommandContext, - CommandOptionsSchema, - defineCommand, -} from "../define-command"; +import { booleanOption, defineCommand } from "../define-command"; import { inject } from "../di"; -export const helpCommandOptions = { - help: booleanOption(), -} satisfies CommandOptionsSchema; - -export type HelpCommandContext = CommandContext; - -export function setupHelpCommand() { - return { - $commandRegistry: inject(CommandRegistry), - $helpService: inject("helpService"), - }; -} - -export type IHelpCommandServices = ReturnType; - -export async function runHelpCommand( - context: HelpCommandContext, - services: IHelpCommandServices, -): Promise { - const args = context.args; - let commandName = (args[0] || "").toLowerCase(); - let commandArguments = _.tail(args); - const hierarchicalCommand = - services.$commandRegistry.buildHierarchicalCommand( - args[0], - commandArguments, - ); - if (hierarchicalCommand) { - commandName = hierarchicalCommand.commandName; - commandArguments = hierarchicalCommand.remainingArguments; - } - - const commandData: ICommandData = { - commandName, - commandArguments, - }; - - if (context.options.help) { - await services.$helpService.showCommandLineHelp(commandData); - } else { - await services.$helpService.openHelpForCommandInBrowser(commandData); - } -} - export const helpCommandDefinition = defineCommand({ name: ["help", "/?"], description: "Shows the help for a command.", - options: helpCommandOptions, + options: { + help: booleanOption(), + }, // The command names whatever command it explains, so every argument after // the first is that command's own. arguments: "any", enableHooks: false, - setup: setupHelpCommand, - run: runHelpCommand, + async run(context): Promise { + const $commandRegistry = inject(CommandRegistry); + const $helpService = inject("helpService"); + + const args = context.args; + let commandName = (args[0] || "").toLowerCase(); + let commandArguments = _.tail(args); + const hierarchicalCommand = $commandRegistry.buildHierarchicalCommand( + args[0], + commandArguments, + ); + if (hierarchicalCommand) { + commandName = hierarchicalCommand.commandName; + commandArguments = hierarchicalCommand.remainingArguments; + } + + const commandData: ICommandData = { + commandName, + commandArguments, + }; + + if (context.options.help) { + await $helpService.showCommandLineHelp(commandData); + } else { + await $helpService.openHelpForCommandInBrowser(commandData); + } + }, }); diff --git a/lib/common/commands/package-manager-get.ts b/lib/common/commands/package-manager-get.ts index 95c8b20732..47c95460b1 100644 --- a/lib/common/commands/package-manager-get.ts +++ b/lib/common/commands/package-manager-get.ts @@ -2,28 +2,17 @@ import { IUserSettingsService } from "../declarations"; import { defineCommand } from "../define-command"; import { inject } from "../di"; -export function setupPackageManagerGetCommand() { - return { - $logger: inject("logger"), - $userSettingsService: inject("userSettingsService"), - }; -} - -export type IPackageManagerGetCommandServices = ReturnType< - typeof setupPackageManagerGetCommand ->; - export const packageManagerGetCommandDefinition = defineCommand({ name: "package-manager|*get", description: "Prints the value of the current package manager.", - setup: setupPackageManagerGetCommand, - async run( - context, - services: IPackageManagerGetCommandServices, - ): Promise { - const result = - await services.$userSettingsService.getSettingValue("packageManager"); - services.$logger.printMarkdown( + async run(): Promise { + const $logger = inject("logger"); + const $userSettingsService = inject( + "userSettingsService", + ); + + const result = await $userSettingsService.getSettingValue("packageManager"); + $logger.printMarkdown( `Your current package manager is \`${result || "npm"}\`.`, ); }, diff --git a/lib/common/commands/package-manager-set.ts b/lib/common/commands/package-manager-set.ts index 9b831a6658..64d09c5a8a 100644 --- a/lib/common/commands/package-manager-set.ts +++ b/lib/common/commands/package-manager-set.ts @@ -3,46 +3,36 @@ import { IErrors, IUserSettingsService } from "../declarations"; import { defineCommand } from "../define-command"; import { inject } from "../di"; -export function setupPackageManagerSetCommand() { - return { - $userSettingsService: inject("userSettingsService"), - $errors: inject("errors"), - $logger: inject("logger"), - }; -} - -export type IPackageManagerSetCommandServices = ReturnType< - typeof setupPackageManagerSetCommand ->; - export const packageManagerSetCommandDefinition = defineCommand({ name: "package-manager|set", description: "Sets the package manager the CLI installs dependencies with.", arguments: [{ name: "packageManager" }], - setup: setupPackageManagerSetCommand, - async run( - context, - services: IPackageManagerSetCommandServices, - ): Promise { + async run(context): Promise { + const $userSettingsService = inject( + "userSettingsService", + ); + const $errors = inject("errors"); + const $logger = inject("logger"); + const packageManagerName = context.args[0]; const supportedPackageManagers = Object.keys(PackageManagers); if (supportedPackageManagers.indexOf(packageManagerName) === -1) { - services.$errors.fail( + $errors.fail( `${packageManagerName} is not a valid package manager. Supported values are: ${supportedPackageManagers.join( ", ", )}.`, ); } - await services.$userSettingsService.saveSetting( + await $userSettingsService.saveSetting( "packageManager", packageManagerName, ); - services.$logger.printMarkdown( + $logger.printMarkdown( `Please ensure you have the directory containing \`${packageManagerName}\` executable available in your PATH.`, ); - services.$logger.printMarkdown( + $logger.printMarkdown( `You've successfully set \`${packageManagerName}\` as your package manager.`, ); }, diff --git a/lib/common/commands/post-install.ts b/lib/common/commands/post-install.ts index dfbdfe571d..fe027b6dd7 100644 --- a/lib/common/commands/post-install.ts +++ b/lib/common/commands/post-install.ts @@ -7,11 +7,10 @@ export const postInstallCommandDefinition = defineCommand({ description: "Deprecated; use `ns dev-post-install-cli`.", arguments: "none", disableAnalytics: true, - setup: () => ({ - $errors: inject("errors"), - }), - async run(context, services): Promise { - services.$errors.fail( + async run(): Promise { + const $errors = inject("errors"); + + $errors.fail( "This command is deprecated. Use `ns dev-post-install-cli` instead", ); }, diff --git a/lib/common/commands/preuninstall.ts b/lib/common/commands/preuninstall.ts index f26e01636e..7a1b823fb5 100644 --- a/lib/common/commands/preuninstall.ts +++ b/lib/common/commands/preuninstall.ts @@ -17,24 +17,6 @@ import { IExtensibilityService } from "../definitions/extensibility"; // disabled for now (6/24/2020) // const FEEDBACK_FORM_URL = "https://www.nativescript.org/uninstall-feedback"; -export function setupPreUninstallCommand() { - return { - $analyticsService: inject("analyticsService"), - $extensibilityService: inject( - "extensibilityService", - ), - $fs: inject("fs"), - $packageInstallationManager: inject( - "packageInstallationManager", - ), - $settingsService: inject("settingsService"), - }; -} - -export type IPreUninstallCommandServices = ReturnType< - typeof setupPreUninstallCommand ->; - async function handleFeedbackForm(): Promise { // disabled for now (6/24/2020) // if (isInteractive()) { @@ -44,10 +26,11 @@ async function handleFeedbackForm(): Promise { } async function handleIntentionalUninstall( - services: IPreUninstallCommandServices, + $extensibilityService: IExtensibilityService, + $packageInstallationManager: IPackageInstallationManager, ): Promise { - services.$extensibilityService.removeAllExtensions(); - services.$packageInstallationManager.clearInspectorCache(); + $extensibilityService.removeAllExtensions(); + $packageInstallationManager.clearInspectorCache(); await handleFeedbackForm(); } @@ -55,8 +38,17 @@ export const preUninstallCommandDefinition = defineCommand({ name: "dev-preuninstall", description: "Runs the CLI's own uninstall bookkeeping.", arguments: "none", - setup: setupPreUninstallCommand, - async run(context, services): Promise { + async run(): Promise { + const $analyticsService = inject("analyticsService"); + const $extensibilityService = inject( + "extensibilityService", + ); + const $fs = inject("fs"); + const $packageInstallationManager = inject( + "packageInstallationManager", + ); + const $settingsService = inject("settingsService"); + const isIntentionalUninstall = doesCurrentNpmCommandMatch([ /^uninstall$/, /^remove$/, @@ -66,22 +58,21 @@ export const preUninstallCommandDefinition = defineCommand({ /^unlink$/, ]); - await services.$analyticsService.trackEventActionInGoogleAnalytics({ + await $analyticsService.trackEventActionInGoogleAnalytics({ action: TrackActionNames.UninstallCLI, additionalData: `isIntentionalUninstall${AnalyticsEventLabelDelimiter}${isIntentionalUninstall}${AnalyticsEventLabelDelimiter}isInteractive${AnalyticsEventLabelDelimiter}${!!isInteractive()}`, }); if (isIntentionalUninstall) { - await handleIntentionalUninstall(services); + await handleIntentionalUninstall( + $extensibilityService, + $packageInstallationManager, + ); } - services.$fs.deleteFile( - path.join( - services.$settingsService.getProfileDir(), - "KillSwitches", - "cli", - ), + $fs.deleteFile( + path.join($settingsService.getProfileDir(), "KillSwitches", "cli"), ); - await services.$analyticsService.finishTracking(); + await $analyticsService.finishTracking(); }, }); diff --git a/lib/common/commands/proxy/proxy-base.ts b/lib/common/commands/proxy/proxy-base.ts index 222609d044..f7908b51a1 100644 --- a/lib/common/commands/proxy/proxy-base.ts +++ b/lib/common/commands/proxy/proxy-base.ts @@ -1,29 +1,14 @@ -import { IAnalyticsService, IProxyService } from "../../declarations"; -import { inject } from "../../di"; - -export function injectProxyCommandServices() { - return { - $analyticsService: inject("analyticsService"), - $logger: inject("logger"), - $proxyService: inject("proxyService"), - }; -} - -export type IProxyCommandServices = ReturnType< - typeof injectProxyCommandServices ->; - export async function tryTrackProxyCommandUsage( - services: IProxyCommandServices, + $logger: ILogger, commandName: string, ): Promise { try { // TODO(Analytics): Check why we have set the `disableAnalytics` to true and we track the command as separate one // instead of tracking it through the commandsService. - services.$logger.trace(commandName); - // await services.$analyticsService.trackFeature(commandName); + $logger.trace(commandName); + // await $analyticsService.trackFeature(commandName); } catch (ex) { - services.$logger.trace("Error in trying to track proxy command usage:"); - services.$logger.trace(ex); + $logger.trace("Error in trying to track proxy command usage:"); + $logger.trace(ex); } } diff --git a/lib/common/commands/proxy/proxy-clear.ts b/lib/common/commands/proxy/proxy-clear.ts index a48655f22f..80624762ab 100644 --- a/lib/common/commands/proxy/proxy-clear.ts +++ b/lib/common/commands/proxy/proxy-clear.ts @@ -1,9 +1,7 @@ +import { IProxyService } from "../../declarations"; import { defineCommand } from "../../define-command"; -import { - injectProxyCommandServices, - IProxyCommandServices, - tryTrackProxyCommandUsage, -} from "./proxy-base"; +import { inject } from "../../di"; +import { tryTrackProxyCommandUsage } from "./proxy-base"; const proxyClearCommandName = "proxy|clear"; @@ -12,10 +10,12 @@ export const proxyClearCommandDefinition = defineCommand({ description: "Clears the currently configured proxy settings.", arguments: "none", disableAnalytics: true, - setup: injectProxyCommandServices, - async run(context, services: IProxyCommandServices): Promise { - await services.$proxyService.clearCache(); - services.$logger.info("Successfully cleared proxy."); - await tryTrackProxyCommandUsage(services, proxyClearCommandName); + async run(): Promise { + const $logger = inject("logger"); + const $proxyService = inject("proxyService"); + + await $proxyService.clearCache(); + $logger.info("Successfully cleared proxy."); + await tryTrackProxyCommandUsage($logger, proxyClearCommandName); }, }); diff --git a/lib/common/commands/proxy/proxy-get.ts b/lib/common/commands/proxy/proxy-get.ts index 7325648ed2..263a1e70c2 100644 --- a/lib/common/commands/proxy/proxy-get.ts +++ b/lib/common/commands/proxy/proxy-get.ts @@ -1,9 +1,7 @@ +import { IProxyService } from "../../declarations"; import { defineCommand } from "../../define-command"; -import { - injectProxyCommandServices, - IProxyCommandServices, - tryTrackProxyCommandUsage, -} from "./proxy-base"; +import { inject } from "../../di"; +import { tryTrackProxyCommandUsage } from "./proxy-base"; const proxyGetCommandName = "proxy|*get"; @@ -12,9 +10,11 @@ export const proxyGetCommandDefinition = defineCommand({ description: "Prints the current proxy settings.", arguments: "none", disableAnalytics: true, - setup: injectProxyCommandServices, - async run(context, services: IProxyCommandServices): Promise { - services.$logger.info(await services.$proxyService.getInfo()); - await tryTrackProxyCommandUsage(services, proxyGetCommandName); + async run(): Promise { + const $logger = inject("logger"); + const $proxyService = inject("proxyService"); + + $logger.info(await $proxyService.getInfo()); + await tryTrackProxyCommandUsage($logger, proxyGetCommandName); }, }); From 0158722190ed92aa5743a4d7dce773d39451b750 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 14 Sep 2026 19:37:28 -0300 Subject: [PATCH 09/17] docs(commands): describe where a handler gets its services Says that a handler resolves its own dependencies at its top, that services are never bundled or shared between commands, and that setup is optional sugar for one command. Documents canExecuteCommand as the way to reuse another command's precondition, and sharpens which authoring form fits which command. --- defining-commands.md | 192 +++++++++++++++++++++++++++---------------- 1 file changed, 120 insertions(+), 72 deletions(-) diff --git a/defining-commands.md b/defining-commands.md index 57951dcf5d..e7983816b2 100644 --- a/defining-commands.md +++ b/defining-commands.md @@ -419,73 +419,69 @@ resolves providers a child scope supplied — see [Registering a definition](#registering-a-definition). The same guidance, and the reasoning behind it, is in `dependency-injection.md`. -`setup` — hoisting work out of `run` ------------------------------------- +Where a handler gets its services +--------------------------------- -`setup(ctx)` runs once per invocation, before `canExecute`, and its return -value is handed to `canExecute`, `run` and `postRun` as their second argument: +A handler resolves what it needs itself, at the top of its own body: ```ts export default defineCommand({ name: "widget|add", arguments: "any", - setup() { + async run(ctx) { + const widgets = inject(WidgetService); const projectData = inject(ProjectData); + projectData.initializeProjectData(); - return { projectData, widgets: inject(WidgetService) }; - }, - canExecute(ctx, { projectData }) { - return !!projectData.projectDir; - }, - async run(ctx, { widgets }) { await widgets.add(ctx.args); }, }); ``` -It exists for two reasons. It is the place to inject services before the first -`await` when several handlers need them, and it is where the work a command -class used to do in its constructor goes — most often -`$projectData.initializeProjectData()`. - -`setup` is sugar. A command may ignore it entirely and call `inject()` at the -top of `run`; nothing else changes. "Once per invocation" means once across -`canExecute`, `run` and `postRun` together — whichever of them the CLI reaches -first triggers it, and the rest reuse the value. - -When several commands share a setup, or a helper outside the definition takes -the services as a parameter, lift it into a named function and derive the type -from it instead of writing the shape out by hand: - -```ts -export function setupWidgetAddCommand() { - const projectData = inject(ProjectData); - projectData.initializeProjectData(); - return { projectData, widgets: inject(WidgetService) }; -} -export type IWidgetAddCommandServices = ReturnType< - typeof setupWidgetAddCommand ->; - -export function canAddWidget(services: IWidgetAddCommandServices): boolean { - return !!services.projectData.projectDir; -} +The injection context is synchronous, so the `inject()` calls belong **above +the first `await`** — see [Injection, and the first +`await`](#injection-and-the-first-await). Resolve everything the handler needs +there and the rule never bites; for anything that genuinely has to wait — +resolved after an `await`, or inside a helper called later — use +`ctx.injector.get(token)`, which works at any point. + +**Services are never bundled.** There is no `setupXCommand()` returning an +object of injected services for another command to spread, and no +`IXCommandServices` type travelling between commands. A dependency is named +where it is used, so reading a handler tells you exactly what it touches. +Sharing is either of two things, and neither of them is a bag: + +- **Shared logic** — a plain function taking the typed `ctx` and plain values, + resolving its own services through `ctx.injector.get(...)`: + + ```ts + export async function canBuildFor( + ctx: CommandContext, + platform: string, + ): Promise { + const validation = ctx.injector.get(PlatformValidationService); + return validation.canBuild(platform); + } + ``` + +- **A whole command's precondition** — `canExecuteCommand(name, args)`, which + asks that command itself; see [Asking another + command](#asking-another-command). + +### `setup`, when a command has one -export default defineCommand({ - name: "widget|add", - arguments: "any", - setup: setupWidgetAddCommand, - canExecute: (ctx, services) => canAddWidget(services), - async run(ctx, { widgets }) { - await widgets.add(ctx.args); - }, -}); -``` - -Leave the setup function's return type off: the alias reads what the body -infers, so annotating the function with the alias makes the pair circular. Read -a setup curried over a parameter — `setupX(platform)` returning the setup -itself — through its inner function, `ReturnType>`. +`setup(ctx)` runs once per invocation, before `canExecute`, and its return +value is handed to `canExecute`, `run` and `postRun` as their second argument. +"Once per invocation" means once across the three together — whichever the CLI +reaches first triggers it, and the rest reuse the value. + +It is optional sugar for **one** command's own handlers, for the case where +`canExecute` and `run` would otherwise repeat the same per-invocation +derivation. It is never a place to assemble services for anything but the +command it belongs to, and a command with a single handler does not need it at +all. When a command has enough structure to want one, the +[class form](#class-form) usually says the same thing better: the instance *is* +the setup, and each dependency is a field. `run`'s return value, and `postRun` ----------------------------------- @@ -569,12 +565,19 @@ class does not declare is left out of the definition entirely, so a class without `postRun` gets no `postCommandAction`, exactly as an object without one does. -**Which form to use.** The class form is for a single named command. When a -function generates variants of one command — the `run|ios` / `run|vision` -family, one definition per platform — the object form is what fits, because -the thing being parameterized is a value and definitions are values. -Registering the same class twice under two names is not the equivalent: the -class is one definition. +**Which form to use.** The class form is for a single named command with +internal structure: state shared between `canExecute` and `run`, values derived +once per invocation, several private steps, or enough collaborators that +`this.$service` reads better than a local in every handler. Everything simpler +— a handful of services and a short handler — is an object definition with its +handlers written inline, where `ctx` is typed by inference and there is nothing +to name. + +When a function generates variants of one command — the `run|ios` / +`run|vision` family, one definition per platform — the object form is what +fits, because the thing being parameterized is a value and definitions are +values. Registering the same class twice under two names is not the +equivalent: the class is one definition. **The class is the setup.** One instance is constructed per invocation, as that invocation's `setup`, before `canExecute` runs. So field initializers and the @@ -597,26 +600,37 @@ the base class reads it. A provider registered for one command — through the `providers` argument of `registerCommand` or `registerLazyCommand` — can inject it too, and resolves nothing outside a running invocation. -**Share through functions, not base classes.** Two commands that need the same -services share an `inject()`-based helper, not a common ancestor: +**One field per dependency.** Each service the class uses is its own field, +read as `this.$x`: ```ts -export function injectPlatformCommandServices() { - const projectData = inject(ProjectData); - projectData.initializeProjectData(); - return { projectData, platformHelper: inject(PlatformCommandHelper) }; -} - export class PlatformAddCommand extends Command({ name: "platform|add" }) { - private services = injectPlatformCommandServices(); + private $projectData = inject("projectData"); + private $platformHelper = inject( + "platformCommandHelper", + ); + + constructor() { + super(); + this.$projectData.initializeProjectData(); + } // ... } ``` -A helper composes — a command can call two of them — and it stays readable -without the reader walking a chain of files. A base class between `Command()` -and the command does not: it is the pattern the legacy `ICommand` hierarchy -used, and untangling it is most of why this API exists. +Never a `private services = injectSomething()` holding a bag — the fields are +the point, and a bag puts the dependency list back behind one more hop. Two +commands needing the same four services restate those four lines; that +duplication is cheaper than a shared shape neither of them owns. + +**Share logic, not base classes and not services.** What two commands genuinely +have in common is a check or a step, so share a function that takes +`this.context` and plain values and resolves its own services — see [Where a +handler gets its services](#where-a-handler-gets-its-services). To reuse +another command's precondition whole, ask that command: [Asking another +command](#asking-another-command). A base class between `Command()` and the +command is the pattern the legacy `ICommand` hierarchy used, and untangling it +is most of why this API exists. Registration takes the class itself; see [Registering a definition](#registering-a-definition): @@ -848,6 +862,40 @@ the injector of the current injection context, and the CLI's own outside one. `runCommand` is a thin call onto `CommandsService.executeCommandInProcess`, where the pipeline itself lives. +### Asking another command + +`canExecuteCommand(name, args)` asks a registered command whether it *could* +run, without running it: + +```ts +import { canExecuteCommand } from "../common/services/command-definition-adapter"; + +async canExecute(): Promise { + if (!(await canExecuteCommand("prepare", [this.args[0]]))) { + return false; + } + + return !!this.hostProjectPath; +} +``` + +This is how one command builds on another's precondition. `embed` prepares the +project, so "could `embed` run" starts with "could `prepare` run" — and the way +to ask that is to ask `prepare`, not to import its `canExecute` and hand it +services. The named command is resolved and its options primed exactly as +`runCommand` does, then its own `canExecute` returns the verdict. It builds its +own setup from its own services; nothing crosses between the two commands but +the name and the arguments. + +Pass only the arguments the child's own `arguments` policy accepts. The child +enforces that policy before its `canExecute`, so forwarding a caller's whole +argument list to a child that declares fewer is a rejection, not a wider check. + +`canExecuteCommand` is a thin call onto +`CommandsService.canExecuteCommandInProcess`, and follows `runCommand` in +everything else: the same injector rule, the same option priming and +restoration. + ### Key shortcuts The interactive keys `ns start` and `ns run` offer are the CLI's own caller. A From 25f15ec381c996635a64bf4fc14bf28eb33c7e95 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 14 Sep 2026 20:16:52 -0300 Subject: [PATCH 10/17] feat(commands): make the in-process dispatcher a contract CommandsService is the API a command or plugin runs or consults another command through: runCommand and canExecuteCommand take the registered name or the definition or class it was registered from. The free helpers stay as convenience over it; the *InProcess methods are deprecated. --- defining-commands.md | 28 ++++++-- lib/commands/embedding/embed.ts | 13 +++- lib/commands/post-install.ts | 5 +- .../commands/device/device-log-stream.ts | 5 +- lib/common/contracts/commands-service.ts | 45 +++++++++++++ lib/common/contracts/index.ts | 1 + lib/common/define-command.ts | 22 ++++++ lib/common/definitions/commands-service.d.ts | 14 +++- .../services/command-definition-adapter.ts | 43 +++++------- lib/common/services/commands-service.ts | 60 ++++++++++++----- lib/contracts/index.ts | 3 + test/commands/post-install.ts | 6 +- test/define-command.ts | 67 +++++++++++++++++++ test/services/key-shortcuts.ts | 17 ++--- test/stubs.ts | 18 ++++- 15 files changed, 277 insertions(+), 70 deletions(-) create mode 100644 lib/common/contracts/commands-service.ts diff --git a/defining-commands.md b/defining-commands.md index e7983816b2..f015b19f2d 100644 --- a/defining-commands.md +++ b/defining-commands.md @@ -824,12 +824,20 @@ per-platform command subclasses a shared base to override one field. Running a command in process ---------------------------- -`runCommand` dispatches a registered command from inside the process that is -already running: +The `CommandsService` contract dispatches a registered command from inside the +process that is already running. A class command injects it like any other +service; an inline handler or a key shortcut may use the `runCommand` +convenience, which only resolves the contract from the current context: ```ts +import { CommandsService } from "../common/contracts/commands-service"; import { runCommand } from "../common/services/command-definition-adapter"; +// in a class command +private $commandsService = inject(CommandsService); +await this.$commandsService.runCommand("autocomplete"); + +// in an inline handler or a shortcut action await runCommand("open|ios"); await runCommand("install", ["lodash"]); ``` @@ -857,14 +865,20 @@ declarations into it rewrites the values the host process is still running on — `open|ios` declares `watch: false`, which would otherwise leave an `ns start` out of watch mode for the rest of its life. -Which injector it dispatches through follows the rule `registerCommand` does: -the injector of the current injection context, and the CLI's own outside one. -`runCommand` is a thin call onto `CommandsService.executeCommandInProcess`, -where the pipeline itself lives. +Which injector `runCommand` dispatches through follows the rule +`registerCommand` does: the injector of the current injection context, and the +CLI's own outside one. The pipeline itself lives on the contract, so a plugin +that holds an injector can call `CommandsService.runCommand` directly. ### Asking another command -`canExecuteCommand(name, args)` asks a registered command whether it *could* +Both methods take the command's registered name, or — the typed way — the +definition or `Command()` class it was registered from, whose first name is +used: `runCommand(prepareCommandDefinition)` cannot go stale the way a string +can. + +`CommandsService.canExecuteCommand(command, args)` — or the +`canExecuteCommand` convenience — asks a registered command whether it *could* run, without running it: ```ts diff --git a/lib/commands/embedding/embed.ts b/lib/commands/embedding/embed.ts index 111522c98f..a39b361c19 100644 --- a/lib/commands/embedding/embed.ts +++ b/lib/commands/embedding/embed.ts @@ -7,7 +7,11 @@ import { IFileSystem } from "../../common/declarations"; import { inject } from "../../common/di"; import { canExecuteCommand } from "../../common/services/command-definition-adapter"; import { platformArgument } from "../command-base"; -import { prepareCommandOptions, runPrepareCommand } from "../prepare"; +import { + prepareCommandDefinition, + prepareCommandOptions, + runPrepareCommand, +} from "../prepare"; function resolveHostProjectPath( projectDir: string, @@ -52,7 +56,12 @@ export class EmbedCommand extends Command({ public async canExecute(): Promise { // `prepare` takes the platform alone; the host project arguments are this // command's own and it would reject them. - if (!(await canExecuteCommand("prepare", this.args.slice(0, 1)))) { + if ( + !(await canExecuteCommand( + prepareCommandDefinition, + this.args.slice(0, 1), + )) + ) { return false; } diff --git a/lib/commands/post-install.ts b/lib/commands/post-install.ts index 2731c152dd..8206cdec7d 100644 --- a/lib/commands/post-install.ts +++ b/lib/commands/post-install.ts @@ -6,6 +6,7 @@ import { IHostInfo, ISettingsService, } from "../common/declarations"; +import { CommandsService } from "../common/contracts/commands-service"; import { Command } from "../common/define-command"; import { inject } from "../common/di"; import { doesCurrentNpmCommandMatch } from "../common/helpers"; @@ -16,7 +17,7 @@ export class PostInstallCliCommand extends Command({ disableAnalytics: true, }) { private $fs = inject("fs"); - private $commandsService = inject("commandsService"); + private $commandsService = inject(CommandsService); private $helpService = inject("helpService"); private $settingsService = inject("settingsService"); private $analyticsService = inject("analyticsService"); @@ -47,7 +48,7 @@ export class PostInstallCliCommand extends Command({ // Explicitly ask for confirmation of usage-reporting: await this.$analyticsService.checkConsent(); - await this.$commandsService.tryExecuteCommand("autocomplete", []); + await this.$commandsService.runCommand("autocomplete"); } } diff --git a/lib/common/commands/device/device-log-stream.ts b/lib/common/commands/device/device-log-stream.ts index 730458e4c6..ad165f6bc7 100644 --- a/lib/common/commands/device/device-log-stream.ts +++ b/lib/common/commands/device/device-log-stream.ts @@ -1,4 +1,5 @@ import { ICleanupService } from "../../../definitions/cleanup-service"; +import { CommandsService } from "../../contracts/commands-service"; import { IErrors } from "../../declarations"; import { CommandOptionsSchema, @@ -29,7 +30,7 @@ export const openDeviceLogStreamCommandDefinition = defineCommand({ inject("cleanupService").setShouldDispose(false); }, async run(context): Promise { - const $commandsService = inject("commandsService"); + const $commandsService = inject(CommandsService); const $deviceLogProvider = inject("deviceLogProvider"); const $devicesService = inject("devicesService"); @@ -44,7 +45,7 @@ export const openDeviceLogStreamCommandDefinition = defineCommand({ }); if ($devicesService.deviceCount > 1) { - await $commandsService.tryExecuteCommand("device", []); + await $commandsService.runCommand("device"); $errors.failWithHelp(NOT_SPECIFIED_DEVICE_ERROR_MESSAGE); } diff --git a/lib/common/contracts/commands-service.ts b/lib/common/contracts/commands-service.ts new file mode 100644 index 0000000000..d19dfb379a --- /dev/null +++ b/lib/common/contracts/commands-service.ts @@ -0,0 +1,45 @@ +import { Contract } from "../di/contract"; +import type { CommandReference } from "../define-command"; + +/** + * Dispatches commands inside the running process: the surface a command, a + * key shortcut or a plugin uses to run or consult another command. The command + * line's own entry points into the dispatcher are not part of it. + */ +@Contract({ name: "commandsService" }) +export abstract class CommandsService { + /** + * Whether the command running now was dispatched in process rather than by + * the command line — what tells a command it is borrowing a host process + * instead of owning one. + */ + abstract readonly isExecutingInProcess: boolean; + + /** + * Runs a registered command in the current process. The command gets what a + * typed command line gives it — its declared options primed with their + * defaults, the arguments policy, `canExecute`, hooks and `postRun` — and a + * failure throws instead of exiting, so a process that has to keep running + * can catch it. Analytics do not fire: this is not a new CLI invocation. + * + * `command` is the registered name, or the definition or `Command()` class + * it was registered from — the typed way to refer to a command. + */ + abstract runCommand( + command: CommandReference, + args?: string[], + ): Promise; + + /** + * Asks a registered command whether it could run on `args`, without running + * it. The command is resolved and its options primed exactly as for + * `runCommand`, and its own `canExecute` returns the verdict. The child + * builds its own setup from its own services, so nothing crosses between + * the two but the name and the arguments; pass only the arguments the + * child's own `arguments` policy accepts. + */ + abstract canExecuteCommand( + command: CommandReference, + args?: string[], + ): Promise; +} diff --git a/lib/common/contracts/index.ts b/lib/common/contracts/index.ts index 6c5d9ec26f..3267f1d173 100644 --- a/lib/common/contracts/index.ts +++ b/lib/common/contracts/index.ts @@ -15,5 +15,6 @@ export type { DeferredCommandResult, } from "./command-registry"; export { COMMAND_CONTEXT } from "./command-context"; +export { CommandsService } from "./commands-service"; export { ModuleRegistry } from "./module-registry"; export { PublicApiBuilder } from "./public-api-builder"; diff --git a/lib/common/define-command.ts b/lib/common/define-command.ts index 43031f5d70..9e600064b5 100644 --- a/lib/common/define-command.ts +++ b/lib/common/define-command.ts @@ -762,6 +762,28 @@ export function toCommandDefinition( return isCommandDefinition(value) ? value : null; } +/** + * What a dispatcher accepts in place of a command name: the name itself, or + * the definition or class it was registered from, whose first name is used. + */ +export type CommandReference = string | RegisterableCommand; + +export function commandNameOf(command: CommandReference): string { + if (typeof command === "string") { + return command; + } + + const definition = toCommandDefinition(command); + if (!definition) { + throw new Error( + `${describeDefinition(command)} is neither a command name, a ` + + `defineCommand() definition nor a Command() class.`, + ); + } + + return Array.isArray(definition.name) ? definition.name[0] : definition.name; +} + /** * The class authoring form: sugar over defineCommand, not a second execution * path. The returned base carries a `definition` that reads the class it is diff --git a/lib/common/definitions/commands-service.d.ts b/lib/common/definitions/commands-service.d.ts index 39a1cbcd98..f6ed426658 100644 --- a/lib/common/definitions/commands-service.d.ts +++ b/lib/common/definitions/commands-service.d.ts @@ -19,14 +19,24 @@ interface ICommandsService { * Runs a command inside the running process, throwing on failure rather * than exiting, so a long-lived host survives it. */ - executeCommandInProcess( - commandName: string, + runCommand( + command: import("../define-command").CommandReference, commandArguments?: string[], ): Promise; /** * Asks a command whether it could run, without running it. The command * builds its own setup from its own services. */ + canExecuteCommand( + command: import("../define-command").CommandReference, + commandArguments?: string[], + ): Promise; + /** @deprecated Use `runCommand`. */ + executeCommandInProcess( + commandName: string, + commandArguments?: string[], + ): Promise; + /** @deprecated Use `canExecuteCommand`. */ canExecuteCommandInProcess( commandName: string, commandArguments?: string[], diff --git a/lib/common/services/command-definition-adapter.ts b/lib/common/services/command-definition-adapter.ts index 10047daf25..b967131d1a 100644 --- a/lib/common/services/command-definition-adapter.ts +++ b/lib/common/services/command-definition-adapter.ts @@ -6,6 +6,7 @@ import { Injector } from "../di/injector"; import { IDictionary, IDashedOption, IErrors } from "../declarations"; import { ICommand } from "../definitions/commands"; import { COMMAND_CONTEXT } from "../contracts/command-context"; +import { CommandsService } from "../contracts/commands-service"; import { COMMAND_OWNER, CommandRegistry, @@ -29,6 +30,7 @@ import { CommandOptionSpec, CommandOptionType, CommandOptionsSchema, + CommandReference, DefinedCommand, RegisterableCommand, defineCommand, @@ -403,10 +405,9 @@ export function createCommandFromDefinition< return; } - const commandsService = targetInjector.get( - "commandsService", - { optional: true }, - ); + const commandsService = targetInjector.get(CommandsService, { + optional: true, + }); if (commandsService && commandsService.isExecutingInProcess) { return; } @@ -543,40 +544,28 @@ const contextInjector = (): Injector => getCurrentInjector() || (getRootInjector()); /** - * Runs a registered command in the current process. The command gets what a - * typed command line gives it — its declared options primed with their - * defaults, the arguments policy, `canExecute`, hooks and `postRun` — and a - * failure throws instead of exiting, so a process that has to keep running - * (`ns start`, dispatching a key shortcut) can catch it. + * Convenience over `CommandsService.runCommand` for code that has no injected + * service at hand, such as a key shortcut action or an inline handler; the + * contract is the API, this only resolves it from the current context. */ export async function runCommand( - name: string, + command: CommandReference, args: string[] = [], ): Promise { - const commandsService = - contextInjector().get("commandsService"); - - await commandsService.executeCommandInProcess(name, args); + await contextInjector().get(CommandsService).runCommand(command, args); } /** - * Asks a registered command whether it could run on `args`, without running it. - * The named command is resolved and its options primed exactly as `runCommand` - * does, and its own `canExecute` returns the verdict. - * - * This is how one command reuses another's precondition — `embed` asking - * whether `prepare` would run. The child resolves its own services, so nothing - * crosses between the two but the name and the arguments; pass only the - * arguments the child's own `arguments` policy accepts. + * Convenience over `CommandsService.canExecuteCommand`, resolved from the + * current context the way `runCommand` is. */ export async function canExecuteCommand( - name: string, + command: CommandReference, args: string[] = [], ): Promise { - const commandsService = - contextInjector().get("commandsService"); - - return commandsService.canExecuteCommandInProcess(name, args); + return contextInjector() + .get(CommandsService) + .canExecuteCommand(command, args); } /** diff --git a/lib/common/services/commands-service.ts b/lib/common/services/commands-service.ts index eaa797860b..432215e585 100644 --- a/lib/common/services/commands-service.ts +++ b/lib/common/services/commands-service.ts @@ -10,6 +10,8 @@ import { IInjector } from "../definitions/yok"; import { injector } from "../yok"; import { IExtensibilityService } from "../definitions/extensibility"; import { IGoogleAnalyticsPageviewData } from "../definitions/google-analytics"; +import { CommandsService as CommandsServiceContract } from "../contracts/commands-service"; +import { CommandReference, commandNameOf } from "../define-command"; import { ICommandParameter, ICommand, @@ -27,7 +29,10 @@ class CommandArgumentsValidationHelper { public remainingArguments: string[]; } -export class CommandsService implements ICommandsService { +export class CommandsService + extends CommandsServiceContract + implements ICommandsService +{ public get currentCommandData(): ICommandData { return _.last(this.commands); } @@ -48,7 +53,9 @@ export class CommandsService implements ICommandsService { private $staticConfig: Config.IStaticConfig, private $extensibilityService: IExtensibilityService, private $optionsTracker: IOptionsTracker, - ) {} + ) { + super(); + } public allCommands(opts: { includeDevCommands: boolean }): string[] { const commands = this.$injector.getRegisteredCommandsNames( @@ -191,7 +198,7 @@ export class CommandsService implements ICommandsService { ); } - return this.canExecuteCommand(commandName, commandArguments); + return this.canExecuteResolvedCommand(commandName, commandArguments); } public async tryExecuteCommand( @@ -239,10 +246,11 @@ export class CommandsService implements ICommandsService { * Analytics stay out of it: this is not a new CLI invocation, and * `checkConsent` may prompt on a terminal the caller has put in raw mode. */ - public async executeCommandInProcess( - commandName: string, + public async runCommand( + command: CommandReference, commandArguments: string[] = [], ): Promise { + const commandName = commandNameOf(command); this.inProcessDepth++; try { const command = this.$injector.resolveCommand(commandName); @@ -255,7 +263,9 @@ export class CommandsService implements ICommandsService { this.commands.push({ commandName, commandArguments }); const restoreOptions = this.primeOptions(command); try { - if (!(await this.canExecuteCommand(commandName, commandArguments))) { + if ( + !(await this.canExecuteResolvedCommand(commandName, commandArguments)) + ) { let commandWithArgs = commandName; if (commandArguments && commandArguments.length) { commandWithArgs += ` ${commandArguments.join(" ")}`; @@ -284,16 +294,17 @@ export class CommandsService implements ICommandsService { } /** - * The `canExecute` half of {@link executeCommandInProcess}: the named command - * is resolved and its options are primed the same way, and its own - * `canExecute` returns the verdict. The child builds its own setup from its - * own services — nothing is threaded in from the caller — which is what lets - * one command reuse another's precondition without importing its handlers. + * The `canExecute` half of {@link runCommand}: the named command is resolved + * and its options are primed the same way, and its own `canExecute` returns + * the verdict. The child builds its own setup from its own services — + * nothing is threaded in from the caller — which is what lets one command + * reuse another's precondition without importing its handlers. */ - public async canExecuteCommandInProcess( - commandName: string, + public async canExecuteCommand( + command: CommandReference, commandArguments: string[] = [], ): Promise { + const commandName = commandNameOf(command); this.inProcessDepth++; try { const command = this.$injector.resolveCommand(commandName); @@ -306,7 +317,10 @@ export class CommandsService implements ICommandsService { this.commands.push({ commandName, commandArguments }); const restoreOptions = this.primeOptions(command); try { - return await this.canExecuteCommand(commandName, commandArguments); + return await this.canExecuteResolvedCommand( + commandName, + commandArguments, + ); } finally { restoreOptions(); this.commands.pop(); @@ -316,6 +330,22 @@ export class CommandsService implements ICommandsService { } } + /** @deprecated Use {@link runCommand}. */ + public executeCommandInProcess( + commandName: string, + commandArguments: string[] = [], + ): Promise { + return this.runCommand(commandName, commandArguments); + } + + /** @deprecated Use {@link canExecuteCommand}. */ + public canExecuteCommandInProcess( + commandName: string, + commandArguments: string[] = [], + ): Promise { + return this.canExecuteCommand(commandName, commandArguments); + } + /** * Merging a command's options into the parser rewrites the values the host * process is still running on: a declared default replaces the CLI-wide one @@ -341,7 +371,7 @@ export class CommandsService implements ICommandsService { }; } - private async canExecuteCommand( + private async canExecuteResolvedCommand( commandName: string, commandArguments: string[], isDynamicCommand?: boolean, diff --git a/lib/contracts/index.ts b/lib/contracts/index.ts index db83a537a9..54aac0ffa3 100644 --- a/lib/contracts/index.ts +++ b/lib/contracts/index.ts @@ -91,6 +91,9 @@ export type { // Promoted from the internal contracts index: the class form reads it in a // field initializer, and a per-command provider is written against it. export { COMMAND_CONTEXT } from "../common/contracts/command-context"; +// The in-process dispatcher a command or plugin runs or consults other +// commands through. +export { CommandsService } from "../common/contracts/commands-service"; export { defineHook, isHookDefinition } from "../common/define-hook"; export type { HookContext, diff --git a/test/commands/post-install.ts b/test/commands/post-install.ts index 4a7b31f216..b0dd9ca476 100644 --- a/test/commands/post-install.ts +++ b/test/commands/post-install.ts @@ -17,7 +17,7 @@ const createTestInjector = (): IInjector => { testInjector.register("staticConfig", {}); testInjector.register("commandsService", { - tryExecuteCommand: async ( + runCommand: async ( commandName: string, commandArguments: string[], ): Promise => undefined, @@ -85,7 +85,7 @@ describe("post-install command", () => { const commandsService = testInjector.resolve("commandsService"); let isTryExecuteCommandCalled = false; - commandsService.tryExecuteCommand = async (): Promise => { + commandsService.runCommand = async (): Promise => { isTryExecuteCommandCalled = true; }; @@ -110,7 +110,7 @@ describe("post-install command", () => { assert.equal( isTryExecuteCommandCalled, opts.shouldCallMethod, - `post-install-cli command must ${hasNotInMsg} call commandsService.tryExecuteCommand`, + `post-install-cli command must ${hasNotInMsg} call commandsService.runCommand`, ); }; diff --git a/test/define-command.ts b/test/define-command.ts index 67241c2d2b..6eb18c2030 100644 --- a/test/define-command.ts +++ b/test/define-command.ts @@ -14,6 +14,7 @@ import { CommandRegistry, DeferredCommandResult, } from "../lib/common/contracts/command-registry"; +import { CommandsService as CommandsServiceContract } from "../lib/common/contracts/commands-service"; import { CommandsService } from "../lib/common/services/commands-service"; import { Options } from "../lib/options"; import { Errors } from "../lib/common/errors"; @@ -1405,6 +1406,72 @@ describe("defineCommand", () => { assert.isFalse(ran); }); + it("is the CommandsService contract's method, resolved by the registered name", async () => { + const testInjector = createInProcessInjector(); + let ran = false; + + runInInjectionContext(testInjector, () => { + registerCommand( + defineCommand({ + name: "dctest-can-contract", + arguments: "any", + canExecute: (context) => context.args[0] === "ok", + run: () => { + ran = true; + }, + }), + ); + }); + + const service = testInjector.get(CommandsServiceContract); + assert.instanceOf(service, CommandsServiceContract); + assert.isTrue( + await service.canExecuteCommand("dctest-can-contract", ["ok"]), + ); + assert.isFalse(ran); + + await service.runCommand("dctest-can-contract", ["ok"]); + assert.isTrue(ran); + }); + + it("takes the definition or class in place of the name", async () => { + const testInjector = createInProcessInjector(); + const runs: string[] = []; + const definition = defineCommand({ + name: ["dctest-ref-primary", "dctest-ref-alias"], + arguments: "any", + canExecute: (context) => context.args[0] === "ok", + run: () => { + runs.push("definition"); + }, + }); + class RefCommand extends Command({ + name: "dctest-ref-class", + arguments: "any", + }) { + run(): void { + runs.push("class"); + } + } + + runInInjectionContext(testInjector, () => { + registerCommand(definition); + registerCommand(RefCommand); + }); + const service = testInjector.get(CommandsServiceContract); + + assert.isTrue(await service.canExecuteCommand(definition, ["ok"])); + assert.isFalse(await service.canExecuteCommand(definition, ["no"])); + await service.runCommand(definition, ["ok"]); + await service.runCommand(RefCommand); + assert.deepEqual(runs, ["definition", "class"]); + + await assert.isRejected( + service.runCommand({ name: "not-a-definition" }), + /neither a command name/, + ); + }); + it("enforces the child's arguments policy before its canExecute", async () => { const testInjector = createInProcessInjector(); let consulted = false; diff --git a/test/services/key-shortcuts.ts b/test/services/key-shortcuts.ts index fe7a05ee1a..4be80230ae 100644 --- a/test/services/key-shortcuts.ts +++ b/test/services/key-shortcuts.ts @@ -1,6 +1,7 @@ import { assert } from "chai"; import { EventEmitter } from "events"; import { RunOnDeviceEvents } from "../../lib/constants"; +import { getContractName } from "../../lib/common/di/contract"; import { runInInjectionContext } from "../../lib/common/di/inject"; import { Injector } from "../../lib/common/di/injector"; import { runCommand } from "../../lib/common/services/command-definition-adapter"; @@ -41,8 +42,10 @@ class FakeStdin extends EventEmitter { const fakeInjector = ( registrations: Map = new Map(), -): Injector => - ({ get: (token: any) => registrations.get(token) }); +): Injector => ({ + get: (token: any) => + registrations.get(token) ?? registrations.get(getContractName(token)), + }); const baseContext = (): KeyContextBase => ({ injector: fakeInjector() }); @@ -217,7 +220,7 @@ describe("key shortcuts", () => { [ "commandsService", { - executeCommandInProcess: async (name: string): Promise => + runCommand: async (name: string): Promise => void invoked.push(name), }, ], @@ -958,10 +961,8 @@ describe("key shortcuts", () => { [ "commandsService", { - executeCommandInProcess: async ( - name: string, - args: string[], - ): Promise => void dispatched.push({ name, args }), + runCommand: async (name: string, args: string[]): Promise => + void dispatched.push({ name, args }), }, ], ]), @@ -984,7 +985,7 @@ describe("key shortcuts", () => { [ "commandsService", { - executeCommandInProcess: async (): Promise => { + runCommand: async (): Promise => { throw new Error("Unable to execute command 'open ios'."); }, }, diff --git a/test/stubs.ts b/test/stubs.ts index 96d60ca966..8011d1c0be 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -1328,20 +1328,34 @@ export class CommandsService implements ICommandsService { return Promise.resolve(true); } - public executeCommandInProcess( + public runCommand( commandName: string, commandArguments?: string[], ): Promise { return Promise.resolve(); } - public canExecuteCommandInProcess( + public canExecuteCommand( commandName: string, commandArguments?: string[], ): Promise { return Promise.resolve(true); } + public executeCommandInProcess( + commandName: string, + commandArguments?: string[], + ): Promise { + return this.runCommand(commandName, commandArguments); + } + + public canExecuteCommandInProcess( + commandName: string, + commandArguments?: string[], + ): Promise { + return this.canExecuteCommand(commandName, commandArguments); + } + public completeCommand(): Promise { return Promise.resolve(true); } From 71c4324c90feedc53adb611de777b371fb2963d8 Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Mon, 14 Sep 2026 20:46:55 -0300 Subject: [PATCH 11/17] fix(commands): run a definition passed to the dispatcher as given A name is looked up in the registry; a definition or Command() class is built and run as the caller holds it, registered or not, its first name serving only hooks and reporting. --- defining-commands.md | 9 +-- lib/common/contracts/commands-service.ts | 5 +- lib/common/define-command.ts | 20 +------ lib/common/services/commands-service.ts | 76 ++++++++++++++++++------ test/define-command.ts | 21 +++++-- 5 files changed, 82 insertions(+), 49 deletions(-) diff --git a/defining-commands.md b/defining-commands.md index f015b19f2d..6821842d08 100644 --- a/defining-commands.md +++ b/defining-commands.md @@ -872,10 +872,11 @@ that holds an injector can call `CommandsService.runCommand` directly. ### Asking another command -Both methods take the command's registered name, or — the typed way — the -definition or `Command()` class it was registered from, whose first name is -used: `runCommand(prepareCommandDefinition)` cannot go stale the way a string -can. +Both methods take a registered name, or — the typed way — a definition or +`Command()` class. A name is looked up in the registry; a definition runs as +given, whether or not it is registered, so `runCommand(prepareCommandDefinition)` +runs exactly what you hold and cannot go stale the way a string can. Its first +name still identifies it for hooks and reporting. `CommandsService.canExecuteCommand(command, args)` — or the `canExecuteCommand` convenience — asks a registered command whether it *could* diff --git a/lib/common/contracts/commands-service.ts b/lib/common/contracts/commands-service.ts index d19dfb379a..0cb64462db 100644 --- a/lib/common/contracts/commands-service.ts +++ b/lib/common/contracts/commands-service.ts @@ -22,8 +22,9 @@ export abstract class CommandsService { * failure throws instead of exiting, so a process that has to keep running * can catch it. Analytics do not fire: this is not a new CLI invocation. * - * `command` is the registered name, or the definition or `Command()` class - * it was registered from — the typed way to refer to a command. + * `command` is a registered name, looked up in the registry, or a + * definition or `Command()` class, which runs as given whether or not it is + * registered — the typed way to refer to a command. */ abstract runCommand( command: CommandReference, diff --git a/lib/common/define-command.ts b/lib/common/define-command.ts index 9e600064b5..0f985e449a 100644 --- a/lib/common/define-command.ts +++ b/lib/common/define-command.ts @@ -763,27 +763,11 @@ export function toCommandDefinition( } /** - * What a dispatcher accepts in place of a command name: the name itself, or - * the definition or class it was registered from, whose first name is used. + * What a dispatcher accepts: a registered command's name, or a definition or + * class to run as given. */ export type CommandReference = string | RegisterableCommand; -export function commandNameOf(command: CommandReference): string { - if (typeof command === "string") { - return command; - } - - const definition = toCommandDefinition(command); - if (!definition) { - throw new Error( - `${describeDefinition(command)} is neither a command name, a ` + - `defineCommand() definition nor a Command() class.`, - ); - } - - return Array.isArray(definition.name) ? definition.name[0] : definition.name; -} - /** * The class authoring form: sugar over defineCommand, not a second execution * path. The returned base carries a `definition` that reads the class it is diff --git a/lib/common/services/commands-service.ts b/lib/common/services/commands-service.ts index 432215e585..f66ac774b4 100644 --- a/lib/common/services/commands-service.ts +++ b/lib/common/services/commands-service.ts @@ -11,7 +11,8 @@ import { injector } from "../yok"; import { IExtensibilityService } from "../definitions/extensibility"; import { IGoogleAnalyticsPageviewData } from "../definitions/google-analytics"; import { CommandsService as CommandsServiceContract } from "../contracts/commands-service"; -import { CommandReference, commandNameOf } from "../define-command"; +import { CommandReference, toCommandDefinition } from "../define-command"; +import { createCommandFromDefinition } from "./command-definition-adapter"; import { ICommandParameter, ICommand, @@ -247,24 +248,28 @@ export class CommandsService * `checkConsent` may prompt on a terminal the caller has put in raw mode. */ public async runCommand( - command: CommandReference, + reference: CommandReference, commandArguments: string[] = [], ): Promise { - const commandName = commandNameOf(command); + // Known before the lookup, so a failure to resolve reports under the name + // the caller used. + let commandName = typeof reference === "string" ? reference : undefined; this.inProcessDepth++; try { - const command = this.$injector.resolveCommand(commandName); - if (!command) { - this.$errors.failWithHelp( - `Unknown command '${helpers.stringReplaceAll(commandName, "|", " ")}'.`, - ); - } + const resolved = this.resolveReference(reference); + const command = resolved.command; + commandName = resolved.commandName; this.commands.push({ commandName, commandArguments }); const restoreOptions = this.primeOptions(command); try { if ( - !(await this.canExecuteResolvedCommand(commandName, commandArguments)) + !(await this.canExecuteResolvedCommand( + commandName, + commandArguments, + undefined, + command, + )) ) { let commandWithArgs = commandName; if (commandArguments && commandArguments.length) { @@ -301,18 +306,12 @@ export class CommandsService * reuse another's precondition without importing its handlers. */ public async canExecuteCommand( - command: CommandReference, + reference: CommandReference, commandArguments: string[] = [], ): Promise { - const commandName = commandNameOf(command); this.inProcessDepth++; try { - const command = this.$injector.resolveCommand(commandName); - if (!command) { - this.$errors.failWithHelp( - `Unknown command '${helpers.stringReplaceAll(commandName, "|", " ")}'.`, - ); - } + const { commandName, command } = this.resolveReference(reference); this.commands.push({ commandName, commandArguments }); const restoreOptions = this.primeOptions(command); @@ -320,6 +319,8 @@ export class CommandsService return await this.canExecuteResolvedCommand( commandName, commandArguments, + undefined, + command, ); } finally { restoreOptions(); @@ -352,6 +353,42 @@ export class CommandsService * and the host keeps reading the replacement long after the command is * done. An in-process dispatch has to put the parser back where it found it. */ + /** + * A name is looked up in the registry; a definition or class is run as the + * caller holds it, registered or not, so what runs is what was referenced. + * Its first name still identifies it for hooks and reporting. + */ + private resolveReference(reference: CommandReference): { + commandName: string; + command: ICommand; + } { + if (typeof reference === "string") { + const command = this.$injector.resolveCommand(reference); + if (!command) { + this.$errors.failWithHelp( + `Unknown command '${helpers.stringReplaceAll(reference, "|", " ")}'.`, + ); + } + + return { commandName: reference, command }; + } + + const definition = toCommandDefinition(reference); + if (!definition) { + throw new Error( + "Expected a command name, a defineCommand() definition or a " + + "Command() class to run.", + ); + } + + return { + commandName: Array.isArray(definition.name) + ? definition.name[0] + : definition.name, + command: createCommandFromDefinition(definition, this.$injector), + }; + } + private primeOptions(command: ICommand): () => void { if (command.isHierarchicalCommand) { return () => undefined; @@ -375,8 +412,9 @@ export class CommandsService commandName: string, commandArguments: string[], isDynamicCommand?: boolean, + resolved?: ICommand, ): Promise { - const command = this.$injector.resolveCommand(commandName); + const command = resolved || this.$injector.resolveCommand(commandName); const beautifiedName = helpers.stringReplaceAll(commandName, "|", " "); if (command) { // Verify command is enabled diff --git a/test/define-command.ts b/test/define-command.ts index 6eb18c2030..1f28916dd7 100644 --- a/test/define-command.ts +++ b/test/define-command.ts @@ -1434,7 +1434,7 @@ describe("defineCommand", () => { assert.isTrue(ran); }); - it("takes the definition or class in place of the name", async () => { + it("runs a definition or class as given, registered or not", async () => { const testInjector = createInProcessInjector(); const runs: string[] = []; const definition = defineCommand({ @@ -1453,10 +1453,18 @@ describe("defineCommand", () => { runs.push("class"); } } - + // Registered under the same name as the definition, to show the + // definition wins over the lookup. runInInjectionContext(testInjector, () => { - registerCommand(definition); - registerCommand(RefCommand); + registerCommand( + defineCommand({ + name: "dctest-ref-primary", + arguments: "any", + run: () => { + runs.push("registered"); + }, + }), + ); }); const service = testInjector.get(CommandsServiceContract); @@ -1464,11 +1472,12 @@ describe("defineCommand", () => { assert.isFalse(await service.canExecuteCommand(definition, ["no"])); await service.runCommand(definition, ["ok"]); await service.runCommand(RefCommand); - assert.deepEqual(runs, ["definition", "class"]); + await service.runCommand("dctest-ref-primary"); + assert.deepEqual(runs, ["definition", "class", "registered"]); await assert.isRejected( service.runCommand({ name: "not-a-definition" }), - /neither a command name/, + /Expected a command name/, ); }); From f40ff395b0c3b76174400e212178dcb20a244453 Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Wed, 16 Sep 2026 11:11:32 +0200 Subject: [PATCH 12/17] refactor: move package managers into lib/package-managers Group the package manager dispatcher, the per-manager implementations (npm, yarn, yarn2, pnpm, bun), their shared base class and the installation manager under a single directory, and shorten the implementation class names to match their file names. --- lib/bootstrap.ts | 14 +++++----- .../base-package-manager.ts | 6 ++--- .../bun.ts} | 14 +++++----- .../index.ts} | 14 +++++----- .../npm.ts} | 14 +++++----- .../package-installation-manager.ts | 10 +++---- .../pnpm.ts} | 14 +++++----- .../yarn.ts} | 12 ++++----- .../yarn2.ts} | 12 ++++----- test/bun-package-manager.ts | 8 +++--- test/controllers/add-platform-controller.ts | 22 ++++++++-------- test/ios-project-service.ts | 10 +++---- test/node-package-manager.ts | 8 +++--- test/package-installation-manager.ts | 24 ++++++++--------- test/plugins-service.ts | 24 ++++++++--------- test/pnpm-package-manager.ts | 26 +++++++++---------- test/services/extensibility-service.ts | 22 ++++++++-------- 17 files changed, 127 insertions(+), 127 deletions(-) rename lib/{ => package-managers}/base-package-manager.ts (98%) rename lib/{bun-package-manager.ts => package-managers/bun.ts} (93%) rename lib/{package-manager.ts => package-managers/index.ts} (93%) rename lib/{node-package-manager.ts => package-managers/npm.ts} (94%) rename lib/{ => package-managers}/package-installation-manager.ts (97%) rename lib/{pnpm-package-manager.ts => package-managers/pnpm.ts} (95%) rename lib/{yarn-package-manager.ts => package-managers/yarn.ts} (93%) rename lib/{yarn2-package-manager.ts => package-managers/yarn2.ts} (94%) 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/base-package-manager.ts b/lib/package-managers/base-package-manager.ts similarity index 98% rename from lib/base-package-manager.ts rename to lib/package-managers/base-package-manager.ts index 5ee9a96abd..ab177a6a12 100644 --- a/lib/base-package-manager.ts +++ b/lib/package-managers/base-package-manager.ts @@ -1,17 +1,17 @@ -import { isInteractive } from "./common/helpers"; +import { isInteractive } from "../common/helpers"; import { INodePackageManager, INodePackageManagerInstallOptions, INpmInstallResultInfo, INpmsResult, INpmPackageNameParts, -} from "./declarations"; +} from "../declarations"; import { IDictionary, IChildProcess, IFileSystem, IHostInfo, -} from "./common/declarations"; +} from "../common/declarations"; export abstract class BasePackageManager implements INodePackageManager { public abstract install( diff --git a/lib/bun-package-manager.ts b/lib/package-managers/bun.ts similarity index 93% rename from lib/bun-package-manager.ts rename to lib/package-managers/bun.ts index cfd5ffc057..0bf36bb0d7 100644 --- a/lib/bun-package-manager.ts +++ b/lib/package-managers/bun.ts @@ -1,23 +1,23 @@ 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, 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 { +export class Bun extends BasePackageManager { constructor( $childProcess: IChildProcess, private $errors: IErrors, @@ -152,4 +152,4 @@ export class BunPackageManager extends BasePackageManager { } } -injector.register("bun", BunPackageManager); +injector.register("bun", Bun); diff --git a/lib/package-manager.ts b/lib/package-managers/index.ts similarity index 93% rename from lib/package-manager.ts rename to lib/package-managers/index.ts index df6d18aa92..3230e14bd8 100644 --- a/lib/package-manager.ts +++ b/lib/package-managers/index.ts @@ -1,6 +1,6 @@ -import { cache, exported, invokeInit } from "./common/decorators"; -import { performanceLog } from "./common/decorators"; -import { PackageManagers } from "./constants"; +import { cache, exported, invokeInit } from "../common/decorators"; +import { performanceLog } from "../common/decorators"; +import { PackageManagers } from "../constants"; import { IPackageManager, INodePackageManager, @@ -9,14 +9,14 @@ import { INpmInstallResultInfo, INpmsResult, INpmPackageNameParts, -} from "./declarations"; +} from "../declarations"; import { IErrors, IUserSettingsService, IDictionary, -} from "./common/declarations"; -import { injector } from "./common/yok"; -import { IProjectConfigService } from "./definitions/project"; +} from "../common/declarations"; +import { injector } from "../common/yok"; +import { IProjectConfigService } from "../definitions/project"; export class PackageManager implements IPackageManager { private packageManager: INodePackageManager; private _packageManagerName: string; diff --git a/lib/node-package-manager.ts b/lib/package-managers/npm.ts similarity index 94% rename from lib/node-package-manager.ts rename to lib/package-managers/npm.ts index bf63cae523..1e508f7c27 100644 --- a/lib/node-package-manager.ts +++ b/lib/package-managers/npm.ts @@ -1,23 +1,23 @@ 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, 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 NodePackageManager extends BasePackageManager { +export class NPM extends BasePackageManager { constructor( $childProcess: IChildProcess, private $errors: IErrors, @@ -172,4 +172,4 @@ export class NodePackageManager extends BasePackageManager { } } -injector.register("npm", NodePackageManager); +injector.register("npm", NPM); diff --git a/lib/package-installation-manager.ts b/lib/package-managers/package-installation-manager.ts similarity index 97% rename from lib/package-installation-manager.ts rename to lib/package-managers/package-installation-manager.ts index 535ff2d942..d34fe6290f 100644 --- a/lib/package-installation-manager.ts +++ b/lib/package-managers/package-installation-manager.ts @@ -1,20 +1,20 @@ import * as path from "path"; -import * as constants from "./constants"; +import * as constants from "../constants"; import { INpmInstallOptions, INpmInstallResultInfo, 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"; diff --git a/lib/pnpm-package-manager.ts b/lib/package-managers/pnpm.ts similarity index 95% rename from lib/pnpm-package-manager.ts rename to lib/package-managers/pnpm.ts index 5970d50c2e..c14814b1d2 100644 --- a/lib/pnpm-package-manager.ts +++ b/lib/package-managers/pnpm.ts @@ -1,13 +1,13 @@ 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, INpmInstallResultInfo, INpmsResult, -} from "./declarations"; +} from "../declarations"; import { IChildProcess, IErrors, @@ -15,10 +15,10 @@ import { IHostInfo, Server, IDictionary, -} from "./common/declarations"; -import { injector } from "./common/yok"; +} from "../common/declarations"; +import { injector } from "../common/yok"; -export class PnpmPackageManager extends BasePackageManager { +export class PNPM extends BasePackageManager { constructor( $childProcess: IChildProcess, private $errors: IErrors, @@ -199,4 +199,4 @@ export class PnpmPackageManager extends BasePackageManager { } } -injector.register("pnpm", PnpmPackageManager); +injector.register("pnpm", PNPM); diff --git a/lib/yarn-package-manager.ts b/lib/package-managers/yarn.ts similarity index 93% rename from lib/yarn-package-manager.ts rename to lib/package-managers/yarn.ts index d4d08ad7f0..253f5efbd5 100644 --- a/lib/yarn-package-manager.ts +++ b/lib/package-managers/yarn.ts @@ -1,12 +1,12 @@ 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, INpmInstallResultInfo, INpmsResult, -} from "./declarations"; +} from "../declarations"; import { IChildProcess, IErrors, @@ -14,10 +14,10 @@ import { IHostInfo, Server, IDictionary, -} from "./common/declarations"; -import { injector } from "./common/yok"; +} from "../common/declarations"; +import { injector } from "../common/yok"; -export class YarnPackageManager extends BasePackageManager { +export class Yarn extends BasePackageManager { constructor( $childProcess: IChildProcess, private $errors: IErrors, @@ -147,4 +147,4 @@ export class YarnPackageManager extends BasePackageManager { } } -injector.register("yarn", YarnPackageManager); +injector.register("yarn", Yarn); diff --git a/lib/yarn2-package-manager.ts b/lib/package-managers/yarn2.ts similarity index 94% rename from lib/yarn2-package-manager.ts rename to lib/package-managers/yarn2.ts index a8312abff3..d717cdf793 100644 --- a/lib/yarn2-package-manager.ts +++ b/lib/package-managers/yarn2.ts @@ -1,12 +1,12 @@ 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, INpmInstallResultInfo, INpmsResult, -} from "./declarations"; +} from "../declarations"; import { IChildProcess, IErrors, @@ -14,10 +14,10 @@ import { IHostInfo, Server, IDictionary, -} from "./common/declarations"; -import { injector } from "./common/yok"; +} from "../common/declarations"; +import { injector } from "../common/yok"; -export class Yarn2PackageManager extends BasePackageManager { +export class Yarn2 extends BasePackageManager { private $hostInfo_: IHostInfo; constructor( $childProcess: IChildProcess, @@ -165,4 +165,4 @@ export class Yarn2PackageManager extends BasePackageManager { } } -injector.register("yarn2", Yarn2PackageManager); +injector.register("yarn2", Yarn2); diff --git a/test/bun-package-manager.ts b/test/bun-package-manager.ts index 758b620f54..8569831ab2 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 { Bun } from "../lib/package-managers/bun"; 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("bun", BunPackageManager); + injector.register("bun", Bun); injector.register("pacoteService", { manifest: () => Promise.resolve(), }); @@ -50,7 +50,7 @@ describe("node-package-manager", () => { ].forEach((testCase) => { it(testCase.name, async () => { const testInjector = createTestInjector(); - const npm = testInjector.resolve("bun"); + const npm = testInjector.resolve("bun"); const templateNameParts = await npm.getPackageNameParts( testCase.templateFullName ); @@ -85,7 +85,7 @@ describe("node-package-manager", () => { ].forEach((testCase) => { it(testCase.name, async () => { const testInjector = createTestInjector(); - const npm = testInjector.resolve("bun"); + const npm = testInjector.resolve("bun"); const templateFullName = await npm.getPackageFullName({ name: testCase.templateName, version: testCase.templateVersion, diff --git a/test/controllers/add-platform-controller.ts b/test/controllers/add-platform-controller.ts index f7680f789f..fa869f692e 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 { NPM } from "../../lib/package-managers/npm"; +import { Yarn } from "../../lib/package-managers/yarn"; +import { Yarn2 } from "../../lib/package-managers/yarn2"; +import { PNPM } from "../../lib/package-managers/pnpm"; +import { Bun } from "../../lib/package-managers/bun"; import { MobileHelper } from "../../lib/common/mobile/mobile-helper"; let actualMessage: string = null; @@ -29,11 +29,11 @@ function createInjector(data?: { latestFrameworkVersion: string }) { trackEventActionInGoogleAnalytics: () => ({}), }); injector.register("packageManager", PackageManager); - injector.register("npm", NodePackageManager); - injector.register("yarn", YarnPackageManager); - injector.register("yarn2", Yarn2PackageManager); - injector.register("pnpm", PnpmPackageManager); - injector.register("bun", BunPackageManager); + injector.register("npm", NPM); + injector.register("yarn", Yarn); + injector.register("yarn2", Yarn2); + injector.register("pnpm", PNPM); + injector.register("bun", Bun); injector.register("userSettingsService", { getSettingValue: async (settingName: string): Promise => undefined, diff --git a/test/ios-project-service.ts b/test/ios-project-service.ts index e55c28f3f2..399557963e 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 { NPM } from "../lib/package-managers/npm"; +import { Yarn } from "../lib/package-managers/yarn"; import { assert } from "chai"; import { SettingsService } from "../lib/common/test/unit-tests/stubs"; @@ -183,8 +183,8 @@ function createTestInjector( }); testInjector.register("packageManager", PackageManager); testInjector.register("projectConfigService", ProjectConfigServiceStub); - testInjector.register("npm", NodePackageManager); - testInjector.register("yarn", YarnPackageManager); + testInjector.register("npm", NPM); + testInjector.register("yarn", Yarn); testInjector.register("xcconfigService", XcconfigService); testInjector.register("settingsService", SettingsService); testInjector.register("httpClient", {}); diff --git a/test/node-package-manager.ts b/test/node-package-manager.ts index 27efb270f3..79af0c71d0 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 { NPM } 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", NPM); 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..c1a5a462a6 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"; @@ -46,11 +46,11 @@ function createTestInjector(): IInjector { testInjector.register("userSettingsService", { getSettingValue: async (settingName: string): Promise => undefined, }); - testInjector.register("npm", NpmLib.NodePackageManager); - testInjector.register("yarn", YarnLib.YarnPackageManager); - testInjector.register("yarn2", Yarn2Lib.Yarn2PackageManager); - testInjector.register("pnpm", PnpmLib.PnpmPackageManager); - testInjector.register("bun", BunLib.BunPackageManager); + testInjector.register("npm", NpmLib.NPM); + testInjector.register("yarn", YarnLib.Yarn); + testInjector.register("yarn2", Yarn2Lib.Yarn2); + testInjector.register("pnpm", PnpmLib.PNPM); + testInjector.register("bun", BunLib.Bun); testInjector.register("packageManager", PackageManagerLib.PackageManager); testInjector.register("projectConfigService", ProjectConfigServiceStub); testInjector.register( diff --git a/test/plugins-service.ts b/test/plugins-service.ts index edf62e97b0..3372674ed3 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 { NPM } from "../lib/package-managers/npm"; +import { Yarn } from "../lib/package-managers/yarn"; +import { Yarn2 } from "../lib/package-managers/yarn2"; +import { PNPM } from "../lib/package-managers/pnpm"; +import { Bun } from "../lib/package-managers/bun"; import { ProjectData } from "../lib/project-data"; import { ChildProcess } from "../lib/common/child-process"; import { Options } from "../lib/options"; @@ -75,11 +75,11 @@ function createTestInjector() { "projectConfigService", stubs.PackageInstallationManagerStub, ); - testInjector.register("npm", NodePackageManager); - testInjector.register("yarn", YarnPackageManager); - testInjector.register("yarn2", Yarn2PackageManager); - testInjector.register("pnpm", PnpmPackageManager); - testInjector.register("bun", BunPackageManager); + testInjector.register("npm", NPM); + testInjector.register("yarn", Yarn); + testInjector.register("yarn2", Yarn2); + testInjector.register("pnpm", PNPM); + testInjector.register("bun", Bun); testInjector.register("fs", FileSystem); // const fileSystemStub = new stubs.FileSystemStub(); // fileSystemStub.exists = (fileName: string) => { diff --git a/test/pnpm-package-manager.ts b/test/pnpm-package-manager.ts index 3c825f877a..6c6a4645bb 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 { PNPM } from "../lib/package-managers/pnpm"; import { IInjector } from "../lib/common/definitions/yok"; class RecordingChildProcessStub extends stubs.ChildProcessStub { @@ -66,7 +66,7 @@ function createTestInjector(): IInjector { injector.register("childProcess", RecordingChildProcessStub); injector.register("httpClient", {}); injector.register("fs", SelectiveFileSystemStub); - injector.register("pnpm", PnpmPackageManager); + injector.register("pnpm", PNPM); injector.register("pacoteService", { manifest: () => Promise.resolve({ name: "left-pad", version: "1.3.0" }), }); @@ -80,7 +80,7 @@ describe("pnpm-package-manager", () => { describe("install", () => { it("passes --shamefully-hoist when the project has no pnpm layout config", async () => { const testInjector = createTestInjector(); - const pnpm = testInjector.resolve("pnpm"); + const pnpm = testInjector.resolve("pnpm"); const childProcess = testInjector.resolve("childProcess"); @@ -94,7 +94,7 @@ describe("pnpm-package-manager", () => { it("omits --shamefully-hoist when a pnpm-workspace.yaml governs the project", async () => { const testInjector = createTestInjector(); - const pnpm = testInjector.resolve("pnpm"); + const pnpm = testInjector.resolve("pnpm"); const childProcess = testInjector.resolve("childProcess"); const fs = testInjector.resolve("fs"); @@ -107,7 +107,7 @@ describe("pnpm-package-manager", () => { it("omits --shamefully-hoist when an ancestor pnpm-workspace.yaml governs the project", async () => { const testInjector = createTestInjector(); - const pnpm = testInjector.resolve("pnpm"); + const pnpm = testInjector.resolve("pnpm"); const childProcess = testInjector.resolve("childProcess"); const fs = testInjector.resolve("fs"); @@ -120,7 +120,7 @@ describe("pnpm-package-manager", () => { it("omits --shamefully-hoist when an .npmrc sets a layout key", async () => { const testInjector = createTestInjector(); - const pnpm = testInjector.resolve("pnpm"); + const pnpm = testInjector.resolve("pnpm"); const childProcess = testInjector.resolve("childProcess"); const fs = testInjector.resolve("fs"); @@ -137,7 +137,7 @@ describe("pnpm-package-manager", () => { ["hoist-pattern[]", "public-hoist-pattern[]"].forEach((layoutKey) => { it(`omits --shamefully-hoist when an .npmrc sets array-valued ${layoutKey}`, async () => { const testInjector = createTestInjector(); - const pnpm = testInjector.resolve("pnpm"); + const pnpm = testInjector.resolve("pnpm"); const childProcess = testInjector.resolve("childProcess"); const fs = testInjector.resolve("fs"); @@ -154,7 +154,7 @@ describe("pnpm-package-manager", () => { it("keeps --shamefully-hoist when an .npmrc has no layout key", async () => { const testInjector = createTestInjector(); - const pnpm = testInjector.resolve("pnpm"); + const pnpm = testInjector.resolve("pnpm"); const childProcess = testInjector.resolve("childProcess"); const fs = testInjector.resolve("fs"); @@ -172,7 +172,7 @@ describe("pnpm-package-manager", () => { it("maps ignoreScripts to --ignore-scripts and drops internal options pnpm rejects", async () => { const testInjector = createTestInjector(); - const pnpm = testInjector.resolve("pnpm"); + const pnpm = testInjector.resolve("pnpm"); const childProcess = testInjector.resolve("childProcess"); @@ -191,7 +191,7 @@ describe("pnpm-package-manager", () => { it("spawns non-interactive installs with stdin closed", async () => { const testInjector = createTestInjector(); - const pnpm = testInjector.resolve("pnpm"); + const pnpm = testInjector.resolve("pnpm"); const childProcess = testInjector.resolve("childProcess"); @@ -213,7 +213,7 @@ describe("pnpm-package-manager", () => { it("appends the package name when installing a single package", async () => { const testInjector = createTestInjector(); - const pnpm = testInjector.resolve("pnpm"); + const pnpm = testInjector.resolve("pnpm"); const childProcess = testInjector.resolve("childProcess"); @@ -231,7 +231,7 @@ describe("pnpm-package-manager", () => { describe("getCachePath", () => { it("uses the configured cache directory when pnpm reports one", async () => { const testInjector = createTestInjector(); - const pnpm = testInjector.resolve("pnpm"); + const pnpm = testInjector.resolve("pnpm"); const childProcess = testInjector.resolve("childProcess"); childProcess.execResponses["pnpm config get cache"] = "/custom/cache\n"; @@ -243,7 +243,7 @@ describe("pnpm-package-manager", () => { it("falls back to the store's parent directory when the cache key is unset", async () => { const testInjector = createTestInjector(); - const pnpm = testInjector.resolve("pnpm"); + const pnpm = testInjector.resolve("pnpm"); const childProcess = testInjector.resolve("childProcess"); childProcess.execResponses["pnpm config get cache"] = "undefined\n"; diff --git a/test/services/extensibility-service.ts b/test/services/extensibility-service.ts index a6b1970ee5..e5acaf36d8 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 { NPM } from "../../lib/package-managers/npm"; +import { PackageManager } from "../../lib/package-managers"; +import { Yarn } from "../../lib/package-managers/yarn"; +import { Yarn2 } from "../../lib/package-managers/yarn2"; +import { PNPM } from "../../lib/package-managers/pnpm"; +import { Bun } from "../../lib/package-managers/bun"; import * as constants from "../../lib/constants"; import { ChildProcess } from "../../lib/common/child-process"; import { CommandsDelimiters } from "../../lib/common/constants"; @@ -75,11 +75,11 @@ describe("extensibilityService", () => { testInjector.register("userSettingsService", { getSettingValue: async (settingName: string): Promise => undefined, }); - testInjector.register("npm", NodePackageManager); - testInjector.register("yarn", YarnPackageManager); - testInjector.register("yarn2", Yarn2PackageManager); - testInjector.register("pnpm", PnpmPackageManager); - testInjector.register("bun", BunPackageManager); + testInjector.register("npm", NPM); + testInjector.register("yarn", Yarn); + testInjector.register("yarn2", Yarn2); + testInjector.register("pnpm", PNPM); + testInjector.register("bun", Bun); testInjector.register("settingsService", SettingsService); testInjector.register("requireService", { require: (pathToRequire: string): any => undefined, From 1157cae5d74da3670d0dcca57dd94839c20745ff Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Wed, 16 Sep 2026 11:28:12 +0200 Subject: [PATCH 13/17] refactor: type package manager install and uninstall options Replace the untyped npm flag bag passed to install/uninstall with IPackageInstallOptions and IPackageUninstallOptions (save, dev, optional, exact, silent, ignoreScripts plus the CLI-internal options). Each package manager declares how it spells each option, and options a manager has no flag for are dropped instead of leaking npm syntax onto its command line. This fixes yarn berry receiving --save-dev / --save-exact (silently dropped, so platforms landed in dependencies) and --ignore-scripts (an unknown option that aborted the install); it now gets --dev, --exact and --mode=skip-build. bun receives its own --dev / --exact instead of npm's. Also settle the implementation class names on NpmPackageManager, YarnPackageManager, Yarn2PackageManager, PnpmPackageManager and BunPackageManager. --- PublicAPI.md | 6 +- lib/commands/install.ts | 2 +- lib/commands/plugin/create-plugin.ts | 3 +- lib/commands/preview.ts | 6 +- lib/commands/test-init.ts | 9 +- lib/constants.ts | 7 - lib/contracts/package-manager.ts | 11 +- lib/declarations.d.ts | 51 +++- lib/package-managers/base-package-manager.ts | 56 +++- lib/package-managers/bun.ts | 34 ++- lib/package-managers/index.ts | 11 +- lib/package-managers/npm.ts | 40 ++- .../package-installation-manager.ts | 21 +- lib/package-managers/pnpm.ts | 37 ++- lib/package-managers/yarn.ts | 29 ++- lib/package-managers/yarn2.ts | 35 +-- lib/services/extensibility-service.ts | 10 +- lib/services/platform/add-platform-service.ts | 5 +- lib/services/plugins-service.ts | 4 +- test/bun-package-manager.ts | 8 +- test/controllers/add-platform-controller.ts | 20 +- test/ios-project-service.ts | 8 +- test/node-package-manager.ts | 8 +- test/package-installation-manager.ts | 10 +- test/package-manager-flags.ts | 241 ++++++++++++++++++ test/plugins-service.ts | 20 +- test/pnpm-package-manager.ts | 35 ++- test/project-templates-service.ts | 4 +- test/services/extensibility-service.ts | 29 ++- test/stubs.ts | 4 +- 30 files changed, 556 insertions(+), 208 deletions(-) create mode 100644 test/package-manager-flags.ts diff --git a/PublicAPI.md b/PublicAPI.md index b59cba2fe3..0d59215163 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: 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..ce1444dd0b 100644 --- a/lib/commands/preview.ts +++ b/lib/commands/preview.ts @@ -53,9 +53,9 @@ export class PreviewCommand extends Command({ `${PREVIEW_CLI_PACKAGE}@latest`, this.$projectData.projectDir, { - "save-dev": true, - "save-exact": true, - } as any, + dev: true, + exact: true, + }, ); } diff --git a/lib/commands/test-init.ts b/lib/commands/test-init.ts index dc070f4e2d..9dd5879e3b 100644 --- a/lib/commands/test-init.ts +++ b/lib/commands/test-init.ts @@ -129,9 +129,8 @@ 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, @@ -186,8 +185,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/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..ef58e3e10f 100644 --- a/lib/contracts/package-manager.ts +++ b/lib/contracts/package-manager.ts @@ -1,7 +1,8 @@ import { Contract } from "../common/di/contract"; import type { IDictionary } from "../common/declarations"; import type { - INodePackageManagerInstallOptions, + IPackageInstallOptions, + IPackageUninstallOptions, INpmInstallResultInfo, INpmPackageNameParts, INpmsResult, @@ -17,25 +18,25 @@ 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; diff --git a/lib/declarations.d.ts b/lib/declarations.d.ts index 466f3830fe..c51b2a8ce9 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; @@ -167,18 +167,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 +420,8 @@ interface INpmInstallResultInfo { interface INpmInstallOptions { pathToSave?: string; version?: string; - dependencyType?: string; + /** Record the package under devDependencies. */ + dev?: boolean; } /** diff --git a/lib/package-managers/base-package-manager.ts b/lib/package-managers/base-package-manager.ts index ab177a6a12..789b9c7ab2 100644 --- a/lib/package-managers/base-package-manager.ts +++ b/lib/package-managers/base-package-manager.ts @@ -1,7 +1,8 @@ import { isInteractive } from "../common/helpers"; import { INodePackageManager, - INodePackageManagerInstallOptions, + IPackageInstallOptions, + IPackageUninstallOptions, INpmInstallResultInfo, INpmsResult, INpmPackageNameParts, @@ -13,15 +14,33 @@ import { 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; @@ -133,6 +152,37 @@ export abstract class BasePackageManager implements INodePackageManager { }; } + 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; + } + protected getFlagsString(config: any, asArray: boolean): any { const array: Array = []; for (const flag in config) { diff --git a/lib/package-managers/bun.ts b/lib/package-managers/bun.ts index 0bf36bb0d7..ddb46464b4 100644 --- a/lib/package-managers/bun.ts +++ b/lib/package-managers/bun.ts @@ -4,7 +4,8 @@ import { exported, cache } from "../common/decorators"; import { CACACHE_DIRECTORY_NAME } from "../constants"; import * as _ from "lodash"; import { - INodePackageManagerInstallOptions, + IPackageInstallOptions, + IPackageUninstallOptions, INpmInstallResultInfo, INpmsResult, } from "../declarations"; @@ -17,7 +18,21 @@ import { } from "../common/declarations"; import { injector } from "../common/yok"; -export class Bun extends BasePackageManager { +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 Bun 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,10 +85,10 @@ export class Bun 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, }); @@ -152,4 +164,4 @@ export class Bun extends BasePackageManager { } } -injector.register("bun", Bun); +injector.register("bun", BunPackageManager); diff --git a/lib/package-managers/index.ts b/lib/package-managers/index.ts index 3230e14bd8..668f30e9cb 100644 --- a/lib/package-managers/index.ts +++ b/lib/package-managers/index.ts @@ -5,7 +5,8 @@ import { IPackageManager, INodePackageManager, IOptions, - INodePackageManagerInstallOptions, + IPackageInstallOptions, + IPackageUninstallOptions, INpmInstallResultInfo, INpmsResult, INpmPackageNameParts, @@ -50,18 +51,18 @@ export class PackageManager implements IPackageManager { 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, + options?: IPackageUninstallOptions, path?: string ): Promise { - return this.packageManager.uninstall(packageName, config, path); + return this.packageManager.uninstall(packageName, options, path); } @exported("packageManager") @invokeInit() diff --git a/lib/package-managers/npm.ts b/lib/package-managers/npm.ts index 1e508f7c27..839ffae6d4 100644 --- a/lib/package-managers/npm.ts +++ b/lib/package-managers/npm.ts @@ -4,7 +4,8 @@ import { exported, cache } from "../common/decorators"; import { CACACHE_DIRECTORY_NAME } from "../constants"; import * as _ from "lodash"; import { - INodePackageManagerInstallOptions, + IPackageInstallOptions, + IPackageUninstallOptions, INpmInstallResultInfo, INpmsResult, } from "../declarations"; @@ -17,7 +18,21 @@ import { } from "../common/declarations"; import { injector } from "../common/yok"; -export class NPM extends BasePackageManager { +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", + }; + constructor( $childProcess: IChildProcess, private $errors: IErrors, @@ -34,19 +49,16 @@ export class NPM 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 NPM 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,10 +115,10 @@ export class NPM 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, }); @@ -172,4 +184,4 @@ export class NPM extends BasePackageManager { } } -injector.register("npm", NPM); +injector.register("npm", NpmPackageManager); diff --git a/lib/package-managers/package-installation-manager.ts b/lib/package-managers/package-installation-manager.ts index d34fe6290f..edc5f95250 100644 --- a/lib/package-managers/package-installation-manager.ts +++ b/lib/package-managers/package-installation-manager.ts @@ -3,6 +3,7 @@ import * as constants from "../constants"; import { INpmInstallOptions, INpmInstallResultInfo, + IPackageInstallOptions, IPackageInstallationManager, IPackageManager, IStaticConfig, @@ -152,13 +153,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); @@ -277,7 +278,7 @@ export class PackageInstallationManager implements IPackageInstallationManager { packageName: string, pathToSave: string, version: string, - dependencyType: string + dev: boolean ): Promise { const possiblePackageName = path.resolve(packageName); if (this.$fs.exists(possiblePackageName)) { @@ -290,7 +291,7 @@ export class PackageInstallationManager implements IPackageInstallationManager { packageName, pathToSave, version, - dependencyType + dev ); const installedPackageName = installResultInfo.name; @@ -307,17 +308,17 @@ export class PackageInstallationManager implements IPackageInstallationManager { 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, diff --git a/lib/package-managers/pnpm.ts b/lib/package-managers/pnpm.ts index c14814b1d2..137f26c556 100644 --- a/lib/package-managers/pnpm.ts +++ b/lib/package-managers/pnpm.ts @@ -4,7 +4,8 @@ import { BasePackageManager } from "./base-package-manager"; import { exported } from "../common/decorators"; import { CACACHE_DIRECTORY_NAME } from "../constants"; import { - INodePackageManagerInstallOptions, + IPackageInstallOptions, + IPackageUninstallOptions, INpmInstallResultInfo, INpmsResult, } from "../declarations"; @@ -18,7 +19,16 @@ import { } from "../common/declarations"; import { injector } from "../common/yok"; -export class PNPM extends BasePackageManager { +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 +45,16 @@ export class PNPM 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,12 +88,10 @@ export class PNPM 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, }); @@ -199,4 +198,4 @@ export class PNPM extends BasePackageManager { } } -injector.register("pnpm", PNPM); +injector.register("pnpm", PnpmPackageManager); diff --git a/lib/package-managers/yarn.ts b/lib/package-managers/yarn.ts index 253f5efbd5..dd1b916b66 100644 --- a/lib/package-managers/yarn.ts +++ b/lib/package-managers/yarn.ts @@ -3,7 +3,8 @@ import * as _ from "lodash"; import { BasePackageManager } from "./base-package-manager"; import { exported } from "../common/decorators"; import { - INodePackageManagerInstallOptions, + IPackageInstallOptions, + IPackageUninstallOptions, INpmInstallResultInfo, INpmsResult, } from "../declarations"; @@ -17,7 +18,16 @@ import { } from "../common/declarations"; import { injector } from "../common/yok"; -export class Yarn extends BasePackageManager { +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 +44,16 @@ export class Yarn 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,10 +79,10 @@ export class Yarn 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, }); @@ -147,4 +154,4 @@ export class Yarn extends BasePackageManager { } } -injector.register("yarn", Yarn); +injector.register("yarn", YarnPackageManager); diff --git a/lib/package-managers/yarn2.ts b/lib/package-managers/yarn2.ts index d717cdf793..1e4d1736aa 100644 --- a/lib/package-managers/yarn2.ts +++ b/lib/package-managers/yarn2.ts @@ -3,7 +3,8 @@ import * as _ from "lodash"; import { BasePackageManager } from "./base-package-manager"; import { exported } from "../common/decorators"; import { - INodePackageManagerInstallOptions, + IPackageInstallOptions, + IPackageUninstallOptions, INpmInstallResultInfo, INpmsResult, } from "../declarations"; @@ -17,7 +18,18 @@ import { } from "../common/declarations"; import { injector } from "../common/yok"; -export class Yarn2 extends BasePackageManager { +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 +58,16 @@ export class Yarn2 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,10 +93,10 @@ export class Yarn2 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, }); @@ -165,4 +170,4 @@ export class Yarn2 extends BasePackageManager { } } -injector.register("yarn2", Yarn2); +injector.register("yarn2", Yarn2PackageManager); diff --git a/lib/services/extensibility-service.ts b/lib/services/extensibility-service.ts index 9c2884c534..4a9b123c9d 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); 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..8717ed8bab 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, @@ -59,7 +59,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, diff --git a/test/bun-package-manager.ts b/test/bun-package-manager.ts index 8569831ab2..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 { Bun } from "../lib/package-managers/bun"; +import { BunPackageManager } from "../lib/package-managers/bun"; 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("bun", Bun); + injector.register("bun", BunPackageManager); injector.register("pacoteService", { manifest: () => Promise.resolve(), }); @@ -50,7 +50,7 @@ describe("node-package-manager", () => { ].forEach((testCase) => { it(testCase.name, async () => { const testInjector = createTestInjector(); - const npm = testInjector.resolve("bun"); + const npm = testInjector.resolve("bun"); const templateNameParts = await npm.getPackageNameParts( testCase.templateFullName ); @@ -85,7 +85,7 @@ describe("node-package-manager", () => { ].forEach((testCase) => { it(testCase.name, async () => { const testInjector = createTestInjector(); - const npm = testInjector.resolve("bun"); + const npm = testInjector.resolve("bun"); const templateFullName = await npm.getPackageFullName({ name: testCase.templateName, version: testCase.templateVersion, diff --git a/test/controllers/add-platform-controller.ts b/test/controllers/add-platform-controller.ts index fa869f692e..be90db2cbf 100644 --- a/test/controllers/add-platform-controller.ts +++ b/test/controllers/add-platform-controller.ts @@ -6,11 +6,11 @@ import { format } from "util"; import * as _ from "lodash"; import { AddPlaformErrors } from "../../lib/constants"; import { PackageManager } from "../../lib/package-managers"; -import { NPM } from "../../lib/package-managers/npm"; -import { Yarn } from "../../lib/package-managers/yarn"; -import { Yarn2 } from "../../lib/package-managers/yarn2"; -import { PNPM } from "../../lib/package-managers/pnpm"; -import { Bun } from "../../lib/package-managers/bun"; +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,11 +29,11 @@ function createInjector(data?: { latestFrameworkVersion: string }) { trackEventActionInGoogleAnalytics: () => ({}), }); injector.register("packageManager", PackageManager); - injector.register("npm", NPM); - injector.register("yarn", Yarn); - injector.register("yarn2", Yarn2); - injector.register("pnpm", PNPM); - injector.register("bun", Bun); + injector.register("npm", NpmPackageManager); + injector.register("yarn", YarnPackageManager); + injector.register("yarn2", Yarn2PackageManager); + injector.register("pnpm", PnpmPackageManager); + injector.register("bun", BunPackageManager); injector.register("userSettingsService", { getSettingValue: async (settingName: string): Promise => undefined, diff --git a/test/ios-project-service.ts b/test/ios-project-service.ts index 399557963e..09b124c0fe 100644 --- a/test/ios-project-service.ts +++ b/test/ios-project-service.ts @@ -25,8 +25,8 @@ import { AndroidDeviceDiscovery } from "../lib/common/mobile/mobile-core/android import { Utils } from "../lib/common/utils"; import { CocoaPodsService } from "../lib/services/cocoapods-service"; import { PackageManager } from "../lib/package-managers"; -import { NPM } from "../lib/package-managers/npm"; -import { Yarn } from "../lib/package-managers/yarn"; +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"; @@ -183,8 +183,8 @@ function createTestInjector( }); testInjector.register("packageManager", PackageManager); testInjector.register("projectConfigService", ProjectConfigServiceStub); - testInjector.register("npm", NPM); - testInjector.register("yarn", Yarn); + testInjector.register("npm", NpmPackageManager); + testInjector.register("yarn", YarnPackageManager); testInjector.register("xcconfigService", XcconfigService); testInjector.register("settingsService", SettingsService); testInjector.register("httpClient", {}); diff --git a/test/node-package-manager.ts b/test/node-package-manager.ts index 79af0c71d0..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 { NPM } from "../lib/package-managers/npm"; +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", NPM); + 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 c1a5a462a6..23bceea684 100644 --- a/test/package-installation-manager.ts +++ b/test/package-installation-manager.ts @@ -46,11 +46,11 @@ function createTestInjector(): IInjector { testInjector.register("userSettingsService", { getSettingValue: async (settingName: string): Promise => undefined, }); - testInjector.register("npm", NpmLib.NPM); - testInjector.register("yarn", YarnLib.Yarn); - testInjector.register("yarn2", Yarn2Lib.Yarn2); - testInjector.register("pnpm", PnpmLib.PNPM); - testInjector.register("bun", BunLib.Bun); + testInjector.register("npm", NpmLib.NpmPackageManager); + testInjector.register("yarn", YarnLib.YarnPackageManager); + testInjector.register("yarn2", Yarn2Lib.Yarn2PackageManager); + testInjector.register("pnpm", PnpmLib.PnpmPackageManager); + testInjector.register("bun", BunLib.BunPackageManager); testInjector.register("packageManager", PackageManagerLib.PackageManager); testInjector.register("projectConfigService", ProjectConfigServiceStub); testInjector.register( diff --git a/test/package-manager-flags.ts b/test/package-manager-flags.ts new file mode 100644 index 0000000000..a2bbb0c9fb --- /dev/null +++ b/test/package-manager-flags.ts @@ -0,0 +1,241 @@ +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("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 3372674ed3..4c7eb6c57e 100644 --- a/test/plugins-service.ts +++ b/test/plugins-service.ts @@ -2,11 +2,11 @@ import { Yok } from "../lib/common/yok"; import * as stubs from "./stubs"; import { PackageManager } from "../lib/package-managers"; import { PackageInstallationManager } from "../lib/package-managers/package-installation-manager"; -import { NPM } from "../lib/package-managers/npm"; -import { Yarn } from "../lib/package-managers/yarn"; -import { Yarn2 } from "../lib/package-managers/yarn2"; -import { PNPM } from "../lib/package-managers/pnpm"; -import { Bun } from "../lib/package-managers/bun"; +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"; @@ -75,11 +75,11 @@ function createTestInjector() { "projectConfigService", stubs.PackageInstallationManagerStub, ); - testInjector.register("npm", NPM); - testInjector.register("yarn", Yarn); - testInjector.register("yarn2", Yarn2); - testInjector.register("pnpm", PNPM); - testInjector.register("bun", Bun); + testInjector.register("npm", NpmPackageManager); + testInjector.register("yarn", YarnPackageManager); + testInjector.register("yarn2", Yarn2PackageManager); + testInjector.register("pnpm", PnpmPackageManager); + testInjector.register("bun", BunPackageManager); testInjector.register("fs", FileSystem); // const fileSystemStub = new stubs.FileSystemStub(); // fileSystemStub.exists = (fileName: string) => { diff --git a/test/pnpm-package-manager.ts b/test/pnpm-package-manager.ts index 6c6a4645bb..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 { PNPM } from "../lib/package-managers/pnpm"; +import { PnpmPackageManager } from "../lib/package-managers/pnpm"; import { IInjector } from "../lib/common/definitions/yok"; class RecordingChildProcessStub extends stubs.ChildProcessStub { @@ -66,7 +66,7 @@ function createTestInjector(): IInjector { injector.register("childProcess", RecordingChildProcessStub); injector.register("httpClient", {}); injector.register("fs", SelectiveFileSystemStub); - injector.register("pnpm", PNPM); + injector.register("pnpm", PnpmPackageManager); injector.register("pacoteService", { manifest: () => Promise.resolve({ name: "left-pad", version: "1.3.0" }), }); @@ -80,7 +80,7 @@ describe("pnpm-package-manager", () => { describe("install", () => { it("passes --shamefully-hoist when the project has no pnpm layout config", async () => { const testInjector = createTestInjector(); - const pnpm = testInjector.resolve("pnpm"); + const pnpm = testInjector.resolve("pnpm"); const childProcess = testInjector.resolve("childProcess"); @@ -94,7 +94,7 @@ describe("pnpm-package-manager", () => { it("omits --shamefully-hoist when a pnpm-workspace.yaml governs the project", async () => { const testInjector = createTestInjector(); - const pnpm = testInjector.resolve("pnpm"); + const pnpm = testInjector.resolve("pnpm"); const childProcess = testInjector.resolve("childProcess"); const fs = testInjector.resolve("fs"); @@ -107,7 +107,7 @@ describe("pnpm-package-manager", () => { it("omits --shamefully-hoist when an ancestor pnpm-workspace.yaml governs the project", async () => { const testInjector = createTestInjector(); - const pnpm = testInjector.resolve("pnpm"); + const pnpm = testInjector.resolve("pnpm"); const childProcess = testInjector.resolve("childProcess"); const fs = testInjector.resolve("fs"); @@ -120,7 +120,7 @@ describe("pnpm-package-manager", () => { it("omits --shamefully-hoist when an .npmrc sets a layout key", async () => { const testInjector = createTestInjector(); - const pnpm = testInjector.resolve("pnpm"); + const pnpm = testInjector.resolve("pnpm"); const childProcess = testInjector.resolve("childProcess"); const fs = testInjector.resolve("fs"); @@ -137,7 +137,7 @@ describe("pnpm-package-manager", () => { ["hoist-pattern[]", "public-hoist-pattern[]"].forEach((layoutKey) => { it(`omits --shamefully-hoist when an .npmrc sets array-valued ${layoutKey}`, async () => { const testInjector = createTestInjector(); - const pnpm = testInjector.resolve("pnpm"); + const pnpm = testInjector.resolve("pnpm"); const childProcess = testInjector.resolve("childProcess"); const fs = testInjector.resolve("fs"); @@ -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"]); }); @@ -154,7 +154,7 @@ describe("pnpm-package-manager", () => { it("keeps --shamefully-hoist when an .npmrc has no layout key", async () => { const testInjector = createTestInjector(); - const pnpm = testInjector.resolve("pnpm"); + const pnpm = testInjector.resolve("pnpm"); const childProcess = testInjector.resolve("childProcess"); const fs = testInjector.resolve("fs"); @@ -172,7 +172,7 @@ describe("pnpm-package-manager", () => { it("maps ignoreScripts to --ignore-scripts and drops internal options pnpm rejects", async () => { const testInjector = createTestInjector(); - const pnpm = testInjector.resolve("pnpm"); + const pnpm = testInjector.resolve("pnpm"); const childProcess = testInjector.resolve("childProcess"); @@ -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"); @@ -191,13 +191,13 @@ describe("pnpm-package-manager", () => { it("spawns non-interactive installs with stdin closed", async () => { const testInjector = createTestInjector(); - const pnpm = testInjector.resolve("pnpm"); + const pnpm = testInjector.resolve("pnpm"); const childProcess = testInjector.resolve("childProcess"); setIsInteractive(() => false); try { - await pnpm.install(projectDir, projectDir, {} as any); + await pnpm.install(projectDir, projectDir, {}); } finally { setIsInteractive(undefined); } @@ -213,17 +213,16 @@ describe("pnpm-package-manager", () => { it("appends the package name when installing a single package", async () => { const testInjector = createTestInjector(); - const pnpm = testInjector.resolve("pnpm"); + const pnpm = testInjector.resolve("pnpm"); 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", ]); }); }); @@ -231,7 +230,7 @@ describe("pnpm-package-manager", () => { describe("getCachePath", () => { it("uses the configured cache directory when pnpm reports one", async () => { const testInjector = createTestInjector(); - const pnpm = testInjector.resolve("pnpm"); + const pnpm = testInjector.resolve("pnpm"); const childProcess = testInjector.resolve("childProcess"); childProcess.execResponses["pnpm config get cache"] = "/custom/cache\n"; @@ -243,7 +242,7 @@ describe("pnpm-package-manager", () => { it("falls back to the store's parent directory when the cache key is unset", async () => { const testInjector = createTestInjector(); - const pnpm = testInjector.resolve("pnpm"); + const pnpm = testInjector.resolve("pnpm"); const childProcess = testInjector.resolve("childProcess"); childProcess.execResponses["pnpm config get cache"] = "undefined\n"; 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/extensibility-service.ts b/test/services/extensibility-service.ts index e5acaf36d8..0ea1ba29af 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 { NPM } from "../../lib/package-managers/npm"; +import { NpmPackageManager } from "../../lib/package-managers/npm"; import { PackageManager } from "../../lib/package-managers"; -import { Yarn } from "../../lib/package-managers/yarn"; -import { Yarn2 } from "../../lib/package-managers/yarn2"; -import { PNPM } from "../../lib/package-managers/pnpm"; -import { Bun } from "../../lib/package-managers/bun"; +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"; @@ -75,11 +75,11 @@ describe("extensibilityService", () => { testInjector.register("userSettingsService", { getSettingValue: async (settingName: string): Promise => undefined, }); - testInjector.register("npm", NPM); - testInjector.register("yarn", Yarn); - testInjector.register("yarn2", Yarn2); - testInjector.register("pnpm", PNPM); - testInjector.register("bun", Bun); + testInjector.register("npm", NpmPackageManager); + testInjector.register("yarn", YarnPackageManager); + testInjector.register("yarn2", Yarn2PackageManager); + testInjector.register("pnpm", PnpmPackageManager); + testInjector.register("bun", BunPackageManager); testInjector.register("settingsService", SettingsService); testInjector.register("requireService", { require: (pathToRequire: string): any => undefined, @@ -245,15 +245,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 () => { diff --git a/test/stubs.ts b/test/stubs.ts index 8011d1c0be..371295ca24 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -17,7 +17,7 @@ import { INpmInstallOptions, INodePackageManager, INpmInstallResultInfo, - INodePackageManagerInstallOptions, + IPackageInstallOptions, INpmPackageNameParts, INpmsResult, IAndroidToolsInfoData, @@ -460,7 +460,7 @@ export class NodePackageManagerStub implements INodePackageManager { public async install( packageName: string, pathToSave: string, - config: INodePackageManagerInstallOptions, + options: IPackageInstallOptions, ): Promise { return { name: packageName, From f3f4e1b44a1aae44285dbf7224933d99d2db9739 Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Wed, 16 Sep 2026 11:40:01 +0200 Subject: [PATCH 14/17] refactor: manager-agnostic view/search and resolution-based package lookups view() now takes an optional registry field instead of an npm flag bag, and search() takes only the keywords; each package manager spells the field selection itself (yarn berry uses --fields). The generic npm flag-string builder is gone with them, so nothing in the package manager layer emits npm syntax on behalf of another manager any more. Project-facing package lookups go through Node resolution from the project directory instead of joining node_modules/ by hand: test-init peer dependency discovery, the installed path returned by PackageInstallationManager, the local inspector check, the core modules short-import scan in doctor, and the installed core modules versions in versions-service. plugins-service no longer creates an empty node_modules directory before enumerating dependencies. --- PublicAPI.md | 21 ++++---- lib/commands/test-init.ts | 7 +-- lib/contracts/package-manager.ts | 19 +++---- lib/declarations.d.ts | 13 +++-- lib/package-managers/base-package-manager.ts | 51 ++---------------- lib/package-managers/bun.ts | 19 +++---- lib/package-managers/index.ts | 14 ++--- lib/package-managers/npm.ts | 15 ++---- .../package-installation-manager.ts | 38 +++---------- lib/package-managers/pnpm.ts | 19 ++----- lib/package-managers/yarn.ts | 16 ++---- lib/package-managers/yarn2.ts | 18 +++---- lib/services/android-plugin-build-service.ts | 8 ++- lib/services/doctor-service.ts | 21 ++++---- lib/services/plugins-service.ts | 15 +----- lib/services/versions-service.ts | 32 +++++------ test/package-installation-manager.ts | 6 +-- test/services/android-plugin-build-service.ts | 6 +-- test/services/doctor-service.ts | 53 ++++++++++++++++--- test/stubs.ts | 4 +- 20 files changed, 158 insertions(+), 237 deletions(-) diff --git a/PublicAPI.md b/PublicAPI.md index 0d59215163..351d537418 100644 --- a/PublicAPI.md +++ b/PublicAPI.md @@ -556,16 +556,15 @@ 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); @@ -578,17 +577,17 @@ 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/commands/test-init.ts b/lib/commands/test-init.ts index 9dd5879e3b..8f33e62b01 100644 --- a/lib/commands/test-init.ts +++ b/lib/commands/test-init.ts @@ -2,6 +2,7 @@ import * as path from "path"; import * as _ from "lodash"; import { TESTING_FRAMEWORKS, ProjectTypes } from "../constants"; import { fromWindowsRelativePathToUnix } from "../common/helpers"; +import { resolvePackageJSONPath } from "../helpers/package-path-helper"; import { IProjectData, ITestInitializationService, @@ -137,9 +138,9 @@ export class TestInitCommand extends Command({ 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 modulePackageJsonContent = this.$fs.readJson( + resolvePackageJSONPath(mod.name, { paths: [projectDir] }), + ); const modulePeerDependencies = modulePackageJsonContent.peerDependencies || {}; const modulePeerDependenciesMeta = diff --git a/lib/contracts/package-manager.ts b/lib/contracts/package-manager.ts index ef58e3e10f..e5b09c8b57 100644 --- a/lib/contracts/package-manager.ts +++ b/lib/contracts/package-manager.ts @@ -1,5 +1,4 @@ import { Contract } from "../common/di/contract"; -import type { IDictionary } from "../common/declarations"; import type { IPackageInstallOptions, IPackageUninstallOptions, @@ -42,11 +41,11 @@ export abstract class PackageManager { /** * 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. @@ -75,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. diff --git a/lib/declarations.d.ts b/lib/declarations.d.ts index c51b2a8ce9..cc3b1f33fe 100644 --- a/lib/declarations.d.ts +++ b/lib/declarations.d.ts @@ -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. diff --git a/lib/package-managers/base-package-manager.ts b/lib/package-managers/base-package-manager.ts index 789b9c7ab2..3bcb7e52da 100644 --- a/lib/package-managers/base-package-manager.ts +++ b/lib/package-managers/base-package-manager.ts @@ -7,12 +7,7 @@ import { INpmsResult, INpmPackageNameParts, } from "../declarations"; -import { - IDictionary, - IChildProcess, - IFileSystem, - IHostInfo, -} from "../common/declarations"; +import { IChildProcess, IFileSystem, IHostInfo } from "../common/declarations"; /** * How one package manager spells each IPackageInstallOptions flag on its @@ -43,11 +38,8 @@ export abstract class BasePackageManager implements INodePackageManager { 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; @@ -70,7 +62,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) @@ -183,41 +175,6 @@ export abstract class BasePackageManager implements INodePackageManager { return result; } - 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; - } - - return array.join(" "); - } - private isTgz(packageName: string): boolean { return packageName.indexOf(".tgz") >= 0; } diff --git a/lib/package-managers/bun.ts b/lib/package-managers/bun.ts index ddb46464b4..65911014a5 100644 --- a/lib/package-managers/bun.ts +++ b/lib/package-managers/bun.ts @@ -94,17 +94,13 @@ export class BunPackageManager extends BasePackageManager { }); } - // 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); } @@ -116,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-managers/index.ts b/lib/package-managers/index.ts index 668f30e9cb..cfbd6bbce7 100644 --- a/lib/package-managers/index.ts +++ b/lib/package-managers/index.ts @@ -14,7 +14,6 @@ import { import { IErrors, IUserSettingsService, - IDictionary, } from "../common/declarations"; import { injector } from "../common/yok"; import { IProjectConfigService } from "../definitions/project"; @@ -66,16 +65,13 @@ export class PackageManager implements IPackageManager { } @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() @@ -122,7 +118,7 @@ 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( diff --git a/lib/package-managers/npm.ts b/lib/package-managers/npm.ts index 839ffae6d4..9b55fe3336 100644 --- a/lib/package-managers/npm.ts +++ b/lib/package-managers/npm.ts @@ -125,21 +125,16 @@ export class NpmPackageManager extends BasePackageManager { } @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); } diff --git a/lib/package-managers/package-installation-manager.ts b/lib/package-managers/package-installation-manager.ts index edc5f95250..7fe8d9722a 100644 --- a/lib/package-managers/package-installation-manager.ts +++ b/lib/package-managers/package-installation-manager.ts @@ -1,5 +1,6 @@ import * as path from "path"; import * as constants from "../constants"; +import { resolvePackagePath } from "../helpers/package-path-helper"; import { INpmInstallOptions, INpmInstallResultInfo, @@ -67,9 +68,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; @@ -190,14 +189,11 @@ 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 = resolvePackagePath(inspectorNpmPackageName, { + paths: [projectDir], + }); + if (inspectorPath) { return inspectorPath; } @@ -266,14 +262,6 @@ 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, @@ -293,15 +281,7 @@ export class PackageInstallationManager implements IPackageInstallationManager { version, dev ); - const installedPackageName = installResultInfo.name; - - const pathToInstalledPackage = path.join( - pathToSave, - "node_modules", - installedPackageName - ); - - return pathToInstalledPackage; + return resolvePackagePath(installResultInfo.name, { paths: [pathToSave] }); } private async npmInstall( @@ -335,9 +315,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/package-managers/pnpm.ts b/lib/package-managers/pnpm.ts index 137f26c556..1321e50a9e 100644 --- a/lib/package-managers/pnpm.ts +++ b/lib/package-managers/pnpm.ts @@ -15,7 +15,6 @@ import { IFileSystem, IHostInfo, Server, - IDictionary, } from "../common/declarations"; import { injector } from "../common/yok"; @@ -98,15 +97,11 @@ export class PnpmPackageManager extends BasePackageManager { } @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); } @@ -119,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/package-managers/yarn.ts b/lib/package-managers/yarn.ts index dd1b916b66..aecffc77ca 100644 --- a/lib/package-managers/yarn.ts +++ b/lib/package-managers/yarn.ts @@ -14,7 +14,6 @@ import { IFileSystem, IHostInfo, Server, - IDictionary, } from "../common/declarations"; import { injector } from "../common/yok"; @@ -89,15 +88,11 @@ export class YarnPackageManager extends BasePackageManager { } @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); } @@ -111,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/package-managers/yarn2.ts b/lib/package-managers/yarn2.ts index 1e4d1736aa..4cbb9df362 100644 --- a/lib/package-managers/yarn2.ts +++ b/lib/package-managers/yarn2.ts @@ -14,7 +14,6 @@ import { IFileSystem, IHostInfo, Server, - IDictionary, } from "../common/declarations"; import { injector } from "../common/yok"; @@ -103,15 +102,13 @@ export class Yarn2PackageManager extends BasePackageManager { } @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); } @@ -125,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..9dfd557271 100644 --- a/lib/services/android-plugin-build-service.ts +++ b/lib/services/android-plugin-build-service.ts @@ -492,9 +492,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) { @@ -590,7 +588,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 +603,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/doctor-service.ts b/lib/services/doctor-service.ts index d57018efec..f5d188aef6 100644 --- a/lib/services/doctor-service.ts +++ b/lib/services/doctor-service.ts @@ -1,13 +1,10 @@ import { EOL } from "os"; import * as path from "path"; +import { resolvePackagePath } from "../helpers/package-path-helper"; 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"; @@ -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,12 @@ export class DoctorServiceImpl implements DoctorService { } private getShortImportRegExp(projectDir: string): RegExp { - const pathToTnsCoreModules = path.join( - projectDir, - NODE_MODULES_FOLDER_NAME, - TNS_CORE_MODULES_NAME, - ); + const pathToTnsCoreModules = resolvePackagePath(TNS_CORE_MODULES_NAME, { + paths: [projectDir], + }); + if (!pathToTnsCoreModules) { + return null; + } const coreModulesSubDirs = this.$fs .readDirectory(pathToTnsCoreModules) .filter((entry) => diff --git a/lib/services/plugins-service.ts b/lib/services/plugins-service.ts index 8717ed8bab..7ba2bc593c 100644 --- a/lib/services/plugins-service.ts +++ b/lib/services/plugins-service.ts @@ -84,7 +84,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 && @@ -702,10 +702,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"); } @@ -754,17 +750,10 @@ 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) => diff --git a/lib/services/versions-service.ts b/lib/services/versions-service.ts index 3710ab52bc..e9e93a0f6d 100644 --- a/lib/services/versions-service.ts +++ b/lib/services/versions-service.ts @@ -2,6 +2,7 @@ import * as constants from "../constants"; import * as helpers from "../common/helpers"; import * as semver from "semver"; import * as path from "path"; +import { resolvePackagePath } from "../helpers/package-path-helper"; import { IVersionsService, IPackageInstallationManager } from "../declarations"; import { IProjectData, IProjectDataService } from "../definitions/project"; import { IPluginsService, IBasePluginData } from "../definitions/plugins"; @@ -63,18 +64,12 @@ 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) => + resolvePackagePath(packageName, { + paths: [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 +78,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 +98,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/test/package-installation-manager.ts b/test/package-installation-manager.ts index 23bceea684..7a1622fb90 100644 --- a/test/package-installation-manager.ts +++ b/test/package-installation-manager.ts @@ -67,12 +67,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/services/android-plugin-build-service.ts b/test/services/android-plugin-build-service.ts index c2107cf4c1..fdd314138d 100644 --- a/test/services/android-plugin-build-service.ts +++ b/test/services/android-plugin-build-service.ts @@ -149,9 +149,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 +170,7 @@ describe("androidPluginBuildService", () => { } } - if (config && config["dist-tags"]) { + if (field === "dist-tags") { result = { latest: "4.1.2", }; diff --git a/test/services/doctor-service.ts b/test/services/doctor-service.ts index 862480c02c..ec02c0f882 100644 --- a/test/services/doctor-service.ts +++ b/test/services/doctor-service.ts @@ -3,6 +3,8 @@ import { Yok } from "../../lib/common/yok"; import { LoggerStub, FileSystemStub } from "../stubs"; import { assert } from "chai"; import * as path from "path"; +import * as os from "os"; +import * as nodeFs from "fs"; import * as sinon from "sinon"; import * as _ from "lodash"; import { IProjectDataService } from "../../lib/definitions/project"; @@ -365,15 +367,54 @@ const Observable = require("tns-core-modules-widgets/data/observable").Observabl } }; - testData.forEach(({ filesContents, expectedShortImports }) => { - fs.readText = (filePath) => filesContents[filePath]; + const projectDir = nodeFs.mkdtempSync( + path.join(os.tmpdir(), "ns-doctor-service-"), + ); + const coreModulesDir = path.join( + projectDir, + "node_modules", + "tns-core-modules", + ); + nodeFs.mkdirSync(coreModulesDir, { recursive: true }); + nodeFs.writeFileSync( + path.join(coreModulesDir, "package.json"), + JSON.stringify({ name: "tns-core-modules", version: "6.0.0" }), + ); + + try { + testData.forEach(({ filesContents, expectedShortImports }) => { + fs.readText = (filePath) => filesContents[filePath]; + + const shortImports = doctorService.getDeprecatedShortImportsInFiles( + _.keys(filesContents), + projectDir, + ); + assert.deepStrictEqual(shortImports, expectedShortImports); + }); + } finally { + nodeFs.rmSync(projectDir, { recursive: true, force: true }); + } + }); + it("getDeprecatedShortImportsInFiles returns no results when tns-core-modules is not installed", () => { + const testInjector = createTestInjector(); + const doctorService = + testInjector.resolve("doctorService"); + const fs = testInjector.resolve("fs"); + fs.readText = () => 'const application = require("application");'; + + const projectDir = nodeFs.mkdtempSync( + path.join(os.tmpdir(), "ns-doctor-service-"), + ); + try { const shortImports = doctorService.getDeprecatedShortImportsInFiles( - _.keys(filesContents), - "projectDir", + ["file1"], + projectDir, ); - assert.deepStrictEqual(shortImports, expectedShortImports); - }); + assert.deepStrictEqual(shortImports, []); + } finally { + nodeFs.rmSync(projectDir, { recursive: true, force: true }); + } }); }); diff --git a/test/stubs.ts b/test/stubs.ts index 371295ca24..4793c06cbb 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -476,11 +476,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 {}; } From c8d2a28a53cbd8926dc4c995f9ec63424b909223 Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Wed, 16 Sep 2026 12:10:16 +0200 Subject: [PATCH 15/17] feat(package-managers): resolve installed packages through the package manager Add getInstalledPackagePath(packageName, fromDir) to the package manager contract. The base implementation walks node_modules the way Node does; a package manager with a different on-disk layout can override it. Services whose call chains are already async now ask the package manager where a package lives instead of resolving it themselves: plugins-service, doctor short-import scan, versions-service, prepare-controller's runtime package.json lookup, android-plugin-build-service's local gradle versions, the preview command, test-init and PackageInstallationManager. Sites reached only from synchronous code (getRuntimePackage in project-data-service, the bundler executable lookup, the vitest and karma readiness checks, the transitive walk in node-modules-dependencies-builder) keep using the resolution helper directly. --- PublicAPI.md | 20 ++++++ lib/commands/preview.ts | 12 ++-- lib/commands/test-init.ts | 7 +- lib/contracts/doctor-service.ts | 4 +- lib/contracts/package-manager.ts | 11 +++ lib/controllers/prepare-controller.ts | 23 ++++--- lib/declarations.d.ts | 11 +++ lib/package-managers/base-package-manager.ts | 8 +++ lib/package-managers/index.ts | 9 +++ .../package-installation-manager.ts | 13 ++-- lib/services/android-plugin-build-service.ts | 20 +++--- lib/services/doctor-service.ts | 28 ++++---- lib/services/plugins-service.ts | 68 ++++++++----------- lib/services/versions-service.ts | 23 ++++--- test/contracts.ts | 2 +- test/controllers/prepare-controller.ts | 3 + test/package-manager-flags.ts | 31 +++++++++ test/services/android-plugin-build-service.ts | 6 ++ test/services/doctor-service.ts | 68 ++++++++----------- test/stubs.ts | 7 ++ 20 files changed, 239 insertions(+), 135 deletions(-) diff --git a/PublicAPI.md b/PublicAPI.md index 351d537418..02dfc2055f 100644 --- a/PublicAPI.md +++ b/PublicAPI.md @@ -571,6 +571,26 @@ tns.npm.search(["nativescript", "cloud"]).then(output => { }); ``` +### 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 {Promise} The absolute path of the package directory, or null when it is not installed. + */ +getInstalledPackagePath(packageName: string, fromDir: string): Promise; +``` + +* Usage: +```JavaScript +tns.packageManager.getInstalledPackagePath("@nativescript/core", "/tmp/myProject").then(pathToPackage => { + console.log(pathToPackage ? `Installed at ${pathToPackage}` : "Not installed"); +}); +``` + ### view Provides information about a given package. diff --git a/lib/commands/preview.ts b/lib/commands/preview.ts index ce1444dd0b..7b5b183915 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"; @@ -38,7 +37,7 @@ export class PreviewCommand extends Command({ await this.installLatestPreviewCLI(); } - const previewCLIPath = this.getPreviewCLIPath(); + const previewCLIPath = await this.getPreviewCLIPath(); if (!previewCLIPath) { await this.failMissingPreviewCLI(); @@ -59,10 +58,11 @@ export class PreviewCommand extends Command({ ); } - private getPreviewCLIPath(): string { - return resolvePackagePath(PREVIEW_CLI_PACKAGE, { - paths: [this.$projectData.projectDir], - }); + private getPreviewCLIPath(): Promise { + 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 8f33e62b01..015559ff36 100644 --- a/lib/commands/test-init.ts +++ b/lib/commands/test-init.ts @@ -2,7 +2,6 @@ import * as path from "path"; import * as _ from "lodash"; import { TESTING_FRAMEWORKS, ProjectTypes } from "../constants"; import { fromWindowsRelativePathToUnix } from "../common/helpers"; -import { resolvePackageJSONPath } from "../helpers/package-path-helper"; import { IProjectData, ITestInitializationService, @@ -138,8 +137,12 @@ export class TestInitCommand extends Command({ path: this.$options.path, }); + const modulePath = await this.$packageManager.getInstalledPackagePath( + mod.name, + projectDir, + ); const modulePackageJsonContent = this.$fs.readJson( - resolvePackageJSONPath(mod.name, { paths: [projectDir] }), + path.join(modulePath, "package.json"), ); const modulePeerDependencies = modulePackageJsonContent.peerDependencies || {}; diff --git a/lib/contracts/doctor-service.ts b/lib/contracts/doctor-service.ts index 9a64fbd7a4..4c5a42840e 100644 --- a/lib/contracts/doctor-service.ts +++ b/lib/contracts/doctor-service.ts @@ -34,5 +34,7 @@ export abstract class DoctorService { }): Promise; /** Checks and notifies users of deprecated short imports in their app. */ - abstract checkForDeprecatedShortImportsInAppDir(projectDir: string): void; + abstract checkForDeprecatedShortImportsInAppDir( + projectDir: string, + ): Promise; } diff --git a/lib/contracts/package-manager.ts b/lib/contracts/package-manager.ts index e5b09c8b57..2619e3297d 100644 --- a/lib/contracts/package-manager.ts +++ b/lib/contracts/package-manager.ts @@ -99,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 {Promise} The absolute path of the package directory, or null when it is not installed. + */ + abstract getInstalledPackagePath( + packageName: string, + fromDir: string, + ): Promise; + /** * 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..f2e21afb1f 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,15 @@ export class PrepareController SCOPED_ANDROID_RUNTIME_NAME; } // try reading from installed runtime first before reading from the npm registry... - const installedRuntimePackageJSONPath = resolvePackageJSONPath( - runtimePackageName, - { - paths: [projectData.projectDir], - }, - ); + const installedRuntimePath = + await this.$packageManager.getInstalledPackagePath( + runtimePackageName, + 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 cc3b1f33fe..c7c9c54126 100644 --- a/lib/declarations.d.ts +++ b/lib/declarations.d.ts @@ -111,6 +111,17 @@ 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 {Promise} The absolute path of the package directory, or null when it is not installed. + */ + getInstalledPackagePath( + packageName: string, + fromDir: string, + ): Promise; } /** @deprecated Kept so existing annotations compile; use the {@link PackageManager} contract. */ diff --git a/lib/package-managers/base-package-manager.ts b/lib/package-managers/base-package-manager.ts index 3bcb7e52da..45a23d4e18 100644 --- a/lib/package-managers/base-package-manager.ts +++ b/lib/package-managers/base-package-manager.ts @@ -1,4 +1,5 @@ import { isInteractive } from "../common/helpers"; +import { resolvePackagePath } from "../helpers/package-path-helper"; import { INodePackageManager, IPackageInstallOptions, @@ -144,6 +145,13 @@ export abstract class BasePackageManager implements INodePackageManager { }; } + public async getInstalledPackagePath( + packageName: string, + fromDir: string, + ): Promise { + return resolvePackagePath(packageName, { paths: [fromDir] }) || null; + } + protected getInstallFlags(options: IPackageInstallOptions): string[] { return this.mapFlags(options, this.installFlags); } diff --git a/lib/package-managers/index.ts b/lib/package-managers/index.ts index cfbd6bbce7..d19f5f530c 100644 --- a/lib/package-managers/index.ts +++ b/lib/package-managers/index.ts @@ -108,6 +108,15 @@ export class PackageManager implements IPackageManager { return this.packageManager.getCachePath(); } + @exported("packageManager") + @invokeInit() + public getInstalledPackagePath( + packageName: string, + fromDir: string + ): Promise { + return this.packageManager.getInstalledPackagePath(packageName, fromDir); + } + public async getTagVersion( packageName: string, tag: string diff --git a/lib/package-managers/package-installation-manager.ts b/lib/package-managers/package-installation-manager.ts index 7fe8d9722a..ffa2b04e8b 100644 --- a/lib/package-managers/package-installation-manager.ts +++ b/lib/package-managers/package-installation-manager.ts @@ -1,6 +1,5 @@ import * as path from "path"; import * as constants from "../constants"; -import { resolvePackagePath } from "../helpers/package-path-helper"; import { INpmInstallOptions, INpmInstallResultInfo, @@ -190,9 +189,10 @@ export class PackageInstallationManager implements IPackageInstallationManager { projectDir: string ): Promise { // local installation takes precedence over cache - const inspectorPath = resolvePackagePath(inspectorNpmPackageName, { - paths: [projectDir], - }); + const inspectorPath = await this.$packageManager.getInstalledPackagePath( + inspectorNpmPackageName, + projectDir + ); if (inspectorPath) { return inspectorPath; } @@ -281,7 +281,10 @@ export class PackageInstallationManager implements IPackageInstallationManager { version, dev ); - return resolvePackagePath(installResultInfo.name, { paths: [pathToSave] }); + return this.$packageManager.getInstalledPackagePath( + installResultInfo.name, + pathToSave + ); } private async npmInstall( diff --git a/lib/services/android-plugin-build-service.ts b/lib/services/android-plugin-build-service.ts index 9dfd557271..31a4cecf96 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 { @@ -507,7 +506,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { return runtimeVersion; } - private getLocalGradleVersions(): IRuntimeGradleVersions { + private async getLocalGradleVersions(): Promise { // partial interface of the runtime package.json // including new 8.2+ format and legacy interface IRuntimePackageJSON { @@ -527,19 +526,18 @@ 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( - packageName, - { - paths: [this.$projectData.projectDir], - }, - ); + const installedRuntimePath = + await this.$packageManager.getInstalledPackagePath( + packageName, + this.$projectData.projectDir, + ); - if (!installedRuntimePackageJSONPath) { + if (!installedRuntimePath) { return null; } const installedRuntimePackageJSON: IRuntimePackageJSON = this.$fs.readJson( - installedRuntimePackageJSONPath, + path.join(installedRuntimePath, "package.json"), ); if (!installedRuntimePackageJSON) { @@ -575,7 +573,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { versions: { gradle: string; gradleAndroid: string }; } = null; - const localVersionInfo = this.getLocalGradleVersions(); + const localVersionInfo = await this.getLocalGradleVersions(); if (localVersionInfo) { return localVersionInfo; diff --git a/lib/services/doctor-service.ts b/lib/services/doctor-service.ts index f5d188aef6..3a2b355713 100644 --- a/lib/services/doctor-service.ts +++ b/lib/services/doctor-service.ts @@ -1,6 +1,5 @@ import { EOL } from "os"; import * as path from "path"; -import { resolvePackagePath } from "../helpers/package-path-helper"; import * as _ from "lodash"; import * as helpers from "../common/helpers"; import { cache } from "../common/decorators"; @@ -8,7 +7,7 @@ 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, @@ -70,6 +69,7 @@ export class DoctorServiceImpl implements DoctorService { private $terminalSpinnerService: ITerminalSpinnerService, private $versionsService: IVersionsService, private $settingsService: ISettingsService, + private $packageManager: IPackageManager, ) {} public async printWarnings(configOptions?: { @@ -139,7 +139,7 @@ export class DoctorServiceImpl implements DoctorService { } // todo: check for deprecated imports from `tns-core-modules` - this.checkForDeprecatedShortImportsInAppDir(configOptions.projectDir); + await this.checkForDeprecatedShortImportsInAppDir(configOptions.projectDir); await this.$injector .resolve( @@ -241,12 +241,14 @@ export class DoctorServiceImpl implements DoctorService { return !hasWarnings; } - public checkForDeprecatedShortImportsInAppDir(projectDir: string): void { + public async checkForDeprecatedShortImportsInAppDir( + projectDir: string, + ): Promise { if (projectDir) { try { const files = this.$projectDataService.getAppExecutableFiles(projectDir); - const shortImports = this.getDeprecatedShortImportsInFiles( + const shortImports = await this.getDeprecatedShortImportsInFiles( files, projectDir, ); @@ -269,11 +271,11 @@ export class DoctorServiceImpl implements DoctorService { } } - protected getDeprecatedShortImportsInFiles( + protected async getDeprecatedShortImportsInFiles( files: string[], projectDir: string, - ): { file: string; line: string }[] { - const shortImportRegExp = this.getShortImportRegExp(projectDir); + ): Promise<{ file: string; line: string }[]> { + const shortImportRegExp = await this.getShortImportRegExp(projectDir); const shortImports: { file: string; line: string }[] = []; if (!shortImportRegExp) { return shortImports; @@ -304,10 +306,12 @@ export class DoctorServiceImpl implements DoctorService { return shortImports; } - private getShortImportRegExp(projectDir: string): RegExp { - const pathToTnsCoreModules = resolvePackagePath(TNS_CORE_MODULES_NAME, { - paths: [projectDir], - }); + private async getShortImportRegExp(projectDir: string): Promise { + const pathToTnsCoreModules = + await this.$packageManager.getInstalledPackagePath( + TNS_CORE_MODULES_NAME, + projectDir, + ); if (!pathToTnsCoreModules) { return null; } diff --git a/lib/services/plugins-service.ts b/lib/services/plugins-service.ts index 7ba2bc593c..dffe4497b5 100644 --- a/lib/services/plugins-service.ts +++ b/lib/services/plugins-service.ts @@ -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 { @@ -100,7 +96,7 @@ export class PluginsService implements IPluginsService { this.npmInstallOptions, ) ).name; - const pathToRealNpmPackageJson = this.getPackageJsonFilePathForModule( + const pathToRealNpmPackageJson = await this.getPackageJsonFilePathForModule( name, projectData.projectDir, ); @@ -148,7 +144,7 @@ export class PluginsService implements IPluginsService { platformData: IPlatformData, ): Promise => { const pluginData = this.convertToPluginData( - this.getNodeModuleData(pluginName, projectData.projectDir), + await this.getNodeModuleData(pluginName, projectData.projectDir), projectData.projectDir, ); @@ -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 = await 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)) { @@ -706,14 +694,15 @@ This framework comes from ${dependencyName} plugin, which is installed multiple return path.join(projectDir, "package.json"); } - private getPackageJsonFilePathForModule( + private async getPackageJsonFilePathForModule( moduleName: string, projectDir: string, - ): string { - const pathToJsonFile = resolvePackageJSONPath(moduleName, { - paths: [projectDir], - }); - return pathToJsonFile; + ): Promise { + const pathToModule = await this.$packageManager.getInstalledPackagePath( + moduleName, + projectDir, + ); + return pathToModule && path.join(pathToModule, "package.json"); } private getDependencies(projectDir: string): string[] { @@ -721,13 +710,13 @@ This framework comes from ${dependencyName} plugin, which is installed multiple return _.keys(require(packageJsonFilePath).dependencies); } - private getNodeModuleData( + private async getNodeModuleData( module: string, projectDir: string, - ): INodeModuleData { + ): Promise { // module can be modulePath or moduleName if (!this.$fs.exists(module) || path.basename(module) !== "package.json") { - const resolvedPath = this.getPackageJsonFilePathForModule( + const resolvedPath = await this.getPackageJsonFilePathForModule( module, projectDir, ); @@ -756,9 +745,12 @@ This framework comes from ${dependencyName} plugin, which is installed multiple await this.ensureAllDependenciesAreInstalled(projectData); const nodeModules = this.getDependencies(projectData.projectDir); - return _.map(nodeModules, (nodeModuleName) => - this.getNodeModuleData(nodeModuleName, projectData.projectDir), - ).filter(Boolean); + const modules = await Promise.all( + nodeModules.map((nodeModuleName) => + this.getNodeModuleData(nodeModuleName, projectData.projectDir), + ), + ); + return modules.filter(Boolean); } private async executeNpmCommand( diff --git a/lib/services/versions-service.ts b/lib/services/versions-service.ts index e9e93a0f6d..dc14694ca1 100644 --- a/lib/services/versions-service.ts +++ b/lib/services/versions-service.ts @@ -2,8 +2,11 @@ import * as constants from "../constants"; import * as helpers from "../common/helpers"; import * as semver from "semver"; import * as path from "path"; -import { resolvePackagePath } from "../helpers/package-path-helper"; -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"; @@ -29,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, @@ -65,11 +69,12 @@ class VersionsService implements IVersionsService { if (this.projectData) { const resolve = (packageName: string) => - resolvePackagePath(packageName, { - paths: [this.projectData.projectDir], - }); - let scopedPackagePath = resolve(constants.SCOPED_TNS_CORE_MODULES); - let tnsCoreModulesPath = resolve(constants.TNS_CORE_MODULES_NAME); + this.$packageManager.getInstalledPackagePath( + packageName, + this.projectData.projectDir + ); + let scopedPackagePath = await resolve(constants.SCOPED_TNS_CORE_MODULES); + let tnsCoreModulesPath = await resolve(constants.TNS_CORE_MODULES_NAME); const dependsOnNonScopedPackage = !!this.projectData.dependencies[ constants.TNS_CORE_MODULES_NAME @@ -86,8 +91,8 @@ class VersionsService implements IVersionsService { await this.$pluginsService.ensureAllDependenciesAreInstalled( this.projectData ); - scopedPackagePath = resolve(constants.SCOPED_TNS_CORE_MODULES); - tnsCoreModulesPath = resolve(constants.TNS_CORE_MODULES_NAME); + scopedPackagePath = await resolve(constants.SCOPED_TNS_CORE_MODULES); + tnsCoreModulesPath = await resolve(constants.TNS_CORE_MODULES_NAME); } if (dependsOnNonScopedPackage && tnsCoreModulesPath) { diff --git a/test/contracts.ts b/test/contracts.ts index e07581e01e..1be08d6c2a 100644 --- a/test/contracts.ts +++ b/test/contracts.ts @@ -60,7 +60,7 @@ describe("contracts tranche", () => { async canExecuteLocalBuild(): Promise { return true; } - checkForDeprecatedShortImportsInAppDir(): void {} + async checkForDeprecatedShortImportsInAppDir(): Promise {} } const injector = new Injector([provide(DoctorService, StubDoctorService)]); diff --git a/test/controllers/prepare-controller.ts b/test/controllers/prepare-controller.ts index e3982de1e4..050e8bf5b0 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: async (): Promise => null, + }); injector.register("nodeModulesDependenciesBuilder", { getProductionDependencies: () => [], diff --git a/test/package-manager-flags.ts b/test/package-manager-flags.ts index a2bbb0c9fb..27e9a9ac11 100644 --- a/test/package-manager-flags.ts +++ b/test/package-manager-flags.ts @@ -223,6 +223,37 @@ describe("package manager flag mapping", () => { }); }); + 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 = await 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( + await manager.getInstalledPackagePath( + "definitely-not-installed-package", + repoRoot, + ), + ); + }); + } + }); + describe("uninstall", () => { const expected: { [name: string]: string } = { npm: "npm uninstall left-pad --save", diff --git a/test/services/android-plugin-build-service.ts b/test/services/android-plugin-build-service.ts index fdd314138d..0f8332d5c9 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,11 @@ describe("androidPluginBuildService", () => { addProjectRuntime?: boolean; }): any { return { + getInstalledPackagePath: async ( + packageName: string, + fromDir: string, + ): Promise => + resolvePackagePath(packageName, { paths: [fromDir] }) || null, getRegistryPackageData: async (packageName: string): Promise => { const result: any = []; result["dist-tags"] = { latest: "4.1.2" }; diff --git a/test/services/doctor-service.ts b/test/services/doctor-service.ts index ec02c0f882..efe7654871 100644 --- a/test/services/doctor-service.ts +++ b/test/services/doctor-service.ts @@ -3,12 +3,10 @@ import { Yok } from "../../lib/common/yok"; import { LoggerStub, FileSystemStub } from "../stubs"; import { assert } from "chai"; import * as path from "path"; -import * as os from "os"; -import * as nodeFs from "fs"; 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, @@ -47,6 +45,7 @@ class DoctorServiceInheritor extends DoctorService { $terminalSpinnerService: ITerminalSpinnerService, $versionsService: IVersionsService, $settingsService: ISettingsService, + $packageManager: IPackageManager, ) { super( $analyticsService, @@ -59,13 +58,14 @@ class DoctorServiceInheritor extends DoctorService { $terminalSpinnerService, $versionsService, $settingsService, + $packageManager, ); } public getDeprecatedShortImportsInFiles( files: string[], projectDir: string, - ): { file: string; line: string }[] { + ): Promise<{ file: string; line: string }[]> { return super.getDeprecatedShortImportsInFiles(files, projectDir); } } @@ -95,6 +95,15 @@ describe("doctorService", () => { }, }); testInjector.register("versionsService", {}); + testInjector.register("packageManager", { + getInstalledPackagePath: async ( + packageName: string, + fromDir: string, + ): Promise => + packageName === "tns-core-modules" + ? path.join(fromDir, "node_modules", packageName) + : null, + }); testInjector.register("settingsService", { getProfileDir: (): string => "", }); @@ -351,7 +360,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"); @@ -367,54 +376,33 @@ const Observable = require("tns-core-modules-widgets/data/observable").Observabl } }; - const projectDir = nodeFs.mkdtempSync( - path.join(os.tmpdir(), "ns-doctor-service-"), - ); - const coreModulesDir = path.join( - projectDir, - "node_modules", - "tns-core-modules", - ); - nodeFs.mkdirSync(coreModulesDir, { recursive: true }); - nodeFs.writeFileSync( - path.join(coreModulesDir, "package.json"), - JSON.stringify({ name: "tns-core-modules", version: "6.0.0" }), - ); + for (const { filesContents, expectedShortImports } of testData) { + fs.readText = (filePath) => filesContents[filePath]; - try { - testData.forEach(({ filesContents, expectedShortImports }) => { - fs.readText = (filePath) => filesContents[filePath]; - - const shortImports = doctorService.getDeprecatedShortImportsInFiles( + const shortImports = + await doctorService.getDeprecatedShortImportsInFiles( _.keys(filesContents), - projectDir, + "projectDir", ); - assert.deepStrictEqual(shortImports, expectedShortImports); - }); - } finally { - nodeFs.rmSync(projectDir, { recursive: true, force: true }); + assert.deepStrictEqual(shortImports, expectedShortImports); } }); - it("getDeprecatedShortImportsInFiles returns no results when tns-core-modules is not installed", () => { + it("getDeprecatedShortImportsInFiles returns no results when tns-core-modules is not installed", async () => { const testInjector = createTestInjector(); + const packageManager = testInjector.resolve("packageManager"); + packageManager.getInstalledPackagePath = async (): Promise => + null; const doctorService = testInjector.resolve("doctorService"); const fs = testInjector.resolve("fs"); fs.readText = () => 'const application = require("application");'; - const projectDir = nodeFs.mkdtempSync( - path.join(os.tmpdir(), "ns-doctor-service-"), + const shortImports = await doctorService.getDeprecatedShortImportsInFiles( + ["file1"], + "projectDir", ); - try { - const shortImports = doctorService.getDeprecatedShortImportsInFiles( - ["file1"], - projectDir, - ); - assert.deepStrictEqual(shortImports, []); - } finally { - nodeFs.rmSync(projectDir, { recursive: true, force: true }); - } + assert.deepStrictEqual(shortImports, []); }); }); diff --git a/test/stubs.ts b/test/stubs.ts index 4793c06cbb..533714ee1f 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -457,6 +457,13 @@ export class PackageInstallationManagerStub implements IPackageInstallationManag export class NodePackageManagerStub implements INodePackageManager { constructor() {} + public async getInstalledPackagePath( + packageName: string, + fromDir: string, + ): Promise { + return null; + } + public async install( packageName: string, pathToSave: string, From 14a41fa58466e37a16595f5ddcf45c45581d9753 Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Wed, 16 Sep 2026 12:45:35 +0200 Subject: [PATCH 16/17] refactor: resolve bundler, test runner and extension packages via the package manager The bundler executable lookup, the vitest and karma readiness checks and the extensibility service's "is this extension installed?" check now ask the package manager where a package lives. Their tests stub that one method instead of faking directory listings or patching Node's module resolution. getRuntimePackage in project-data-service and the transitive walk in node-modules-dependencies-builder stay on the resolution helper: both feed synchronous code paths (getPlatformData, getAllProductionPlugins) with dozens of callers, and threading async through those is a separate change. --- lib/commands/test.ts | 2 +- lib/definitions/project.d.ts | 2 +- .../bundler/bundler-compiler-service.ts | 50 ++++++------- lib/services/extensibility-service.ts | 7 +- lib/services/test-execution-service.ts | 11 +-- lib/services/vitest-execution-service.ts | 20 +++-- test/extension-manifests.ts | 6 ++ .../bundler/bundler-compiler-service.ts | 1 + test/services/extensibility-service.ts | 74 ++++++++----------- test/services/test-execution-service.ts | 59 ++++++--------- 10 files changed, 111 insertions(+), 121 deletions(-) diff --git a/lib/commands/test.ts b/lib/commands/test.ts index 0829bb97ec..6b7f1743e2 100644 --- a/lib/commands/test.ts +++ b/lib/commands/test.ts @@ -106,7 +106,7 @@ async function canExecuteTestCommand( if ($vitestExecutionService.isVitestProject($projectData)) { const canStartTestRun = - $vitestExecutionService.canStartTestRun($projectData); + await $vitestExecutionService.canStartTestRun($projectData); if (!canStartTestRun) { $errors.fail({ formatStr: diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index cd2a931617..468bf6c66d 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -522,7 +522,7 @@ interface ITestExecutionService { interface IVitestExecutionService { isVitestProject(projectData: IProjectData): boolean; - canStartTestRun(projectData: IProjectData): boolean; + canStartTestRun(projectData: IProjectData): Promise; startTestRun(platform: string, projectData: IProjectData): Promise; } diff --git a/lib/services/bundler/bundler-compiler-service.ts b/lib/services/bundler/bundler-compiler-service.ts index ccf7df4bb7..b613860817 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,13 @@ export class BundlerCompilerService additionalNodeArgs.unshift("--max_old_space_size=4096"); } + const bundlerExecutablePath = + await this.getBundlerExecutablePath(projectData); + const isModernBundler = await 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); @@ -726,7 +725,7 @@ export class BundlerCompilerService // go after `--` so vite's CLI doesn't choke on unknown options. const args = [ ...additionalNodeArgs, - this.getBundlerExecutablePath(projectData), + await this.getBundlerExecutablePath(projectData), "serve", `--config=${projectData.bundlerConfigPath}`, `--mode=development`, @@ -1166,21 +1165,24 @@ export class BundlerCompilerService }); } - private getBundlerExecutablePath(projectData: IProjectData): string { + private async getBundlerExecutablePath( + projectData: IProjectData, + ): Promise { 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 = await resolve("vite"); if (packagePath) { return path.resolve(packagePath, "bin", "vite.js"); } - } else if (this.isModernBundler(projectData)) { - const packagePath = resolvePackagePath(this.getBundlerPackageName(), { - paths: [projectData.projectDir], - }); + } else if (await this.isModernBundler(projectData)) { + const packagePath = await resolve(this.getBundlerPackageName()); if (packagePath) { return path.resolve(packagePath, "dist", "bin", "index.js"); @@ -1200,9 +1202,7 @@ export class BundlerCompilerService ); } - const packagePath = resolvePackagePath("webpack", { - paths: [projectData.projectDir], - }); + const packagePath = await resolve("webpack"); if (!packagePath) { return ""; @@ -1224,21 +1224,21 @@ export class BundlerCompilerService ); } - private isModernBundler(projectData: IProjectData): boolean { + private async isModernBundler(projectData: IProjectData): Promise { const bundler = this.getBundler(); switch (bundler) { case "rspack": return true; default: - const packageJSONPath = resolvePackageJSONPath( + const packagePath = await 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/extensibility-service.ts b/lib/services/extensibility-service.ts index 4a9b123c9d..aee3ef6cbf 100644 --- a/lib/services/extensibility-service.ts +++ b/lib/services/extensibility-service.ts @@ -512,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 = await 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/test-execution-service.ts b/lib/services/test-execution-service.ts index 8ba4eda276..e640dcf1fe 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 = await this.$packageManager.getInstalledPackagePath( + "karma", + projectData.projectDir, + ); canStartKarmaServer = canStartKarmaServer && !!pathToKarma; diff --git a/lib/services/vitest-execution-service.ts b/lib/services/vitest-execution-service.ts index f4811780e8..41b5593791 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,16 +18,17 @@ export class VitestExecutionService implements IVitestExecutionService { private $fs: IFileSystem, private $logger: ILogger, private $options: IOptions, + private $packageManager: IPackageManager, ) {} public isVitestProject(projectData: IProjectData): boolean { return !!this.getConfigPath(projectData); } - public canStartTestRun(projectData: IProjectData): boolean { + public async canStartTestRun(projectData: IProjectData): Promise { return ( this.isVitestProject(projectData) && - !!resolvePackagePath("vitest", { paths: [projectData.projectDir] }) + !!(await 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 = await 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): Promise { + return this.$packageManager.getInstalledPackagePath( + "vitest", + projectData.projectDir, + ); + } + } injector.register("vitestExecutionService", VitestExecutionService); diff --git a/test/extension-manifests.ts b/test/extension-manifests.ts index 9a213310d9..faaba51cfa 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,11 @@ describe("extension manifests", () => { install: async (): Promise => { throw new Error("Extensions are expected to be installed already."); }, + getInstalledPackagePath: async ( + packageName: string, + fromDir: string, + ): Promise => + resolvePackagePath(packageName, { paths: [fromDir] }) || null, uninstall: async (): Promise => undefined, searchNpms: async (): Promise => ({ results: [] }), getRegistryPackageData: async (): Promise => ({}), diff --git a/test/services/bundler/bundler-compiler-service.ts b/test/services/bundler/bundler-compiler-service.ts index 1ec3a6b057..36fe0bf39b 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: async (): Promise => null, }); testInjector.register("bundlerCompilerService", BundlerCompilerService); testInjector.register("childProcess", {}); diff --git a/test/services/extensibility-service.ts b/test/services/extensibility-service.ts index 0ea1ba29af..eae286db8d 100644 --- a/test/services/extensibility-service.ts +++ b/test/services/extensibility-service.ts @@ -87,6 +87,17 @@ describe("extensibilityService", () => { return testInjector; }; + const stubInstalledExtensions = ( + testInjector: IInjector, + resolve: (extensionName: string, fromDir: string) => string, + ): void => { + const packageManager = testInjector.resolve("packageManager"); + packageManager.getInstalledPackagePath = async ( + packageName: string, + fromDir: string, + ): Promise => resolve(packageName, fromDir); + }; + const getExpectedInstallationPathForExtension = ( testInjector: IInjector, extensionName: string, @@ -320,14 +331,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); @@ -359,20 +365,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); @@ -415,14 +416,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); @@ -469,7 +465,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"); @@ -480,14 +476,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); @@ -532,14 +524,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..7daa0d3f7c 100644 --- a/test/services/test-execution-service.ts +++ b/test/services/test-execution-service.ts @@ -8,10 +8,21 @@ 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: async ( + packageName: string, + fromDir: string, + ): Promise => + installedPackages.indexOf(packageName) !== -1 + ? `${fromDir}/node_modules/${packageName}` + : null, + }); return injector.resolve("testExecutionService"); } @@ -28,8 +39,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 +60,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 +71,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 +82,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 +94,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; }); }); }); From 2000f91e1079e7a2a9d2ce8f2050c69681aedd91 Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Wed, 16 Sep 2026 13:04:40 +0200 Subject: [PATCH 17/17] refactor(package-managers): select the package manager and resolve packages synchronously Nothing about locating an installed package is asynchronous; the only async link was the dispatcher reading the "packageManager" user setting through the settings lock. JsonFileSettingsService gains a lock-free getSettingValueSync for settings that only change through explicit user commands, and the dispatcher now picks its implementation lazily and synchronously, dropping the @cache/@invokeInit init dance. getInstalledPackagePath is therefore synchronous on the contract, which unwinds the async that had been threaded through doctor, plugins-service, the bundler, the test runners, preview and android-plugin-build-service, and lets the last two direct users of the resolution helper move onto the contract: getRuntimePackage in project-data-service (resolved lazily via the injector, as the service is constructed everywhere) and the transitive walk in node-modules-dependencies-builder. --- PublicAPI.md | 9 +- lib/commands/preview.ts | 4 +- lib/commands/test-init.ts | 2 +- lib/commands/test.ts | 2 +- .../json-file-settings-service.d.ts | 5 ++ .../services/json-file-settings-service.ts | 22 +++++ .../services/json-file-settings-service.ts | 31 +++++++ lib/contracts/doctor-service.ts | 4 +- lib/contracts/package-manager.ts | 4 +- lib/controllers/prepare-controller.ts | 9 +- lib/declarations.d.ts | 7 +- lib/definitions/project.d.ts | 2 +- lib/package-managers/base-package-manager.ts | 5 +- lib/package-managers/index.ts | 90 +++++++++---------- .../package-installation-manager.ts | 2 +- lib/services/android-plugin-build-service.ts | 13 ++- .../bundler/bundler-compiler-service.ts | 23 +++-- lib/services/doctor-service.ts | 25 +++--- lib/services/extensibility-service.ts | 2 +- lib/services/plugins-service.ts | 27 +++--- lib/services/project-data-service.ts | 17 ++-- lib/services/test-execution-service.ts | 2 +- lib/services/user-settings-service.ts | 4 + lib/services/versions-service.ts | 8 +- lib/services/vitest-execution-service.ts | 8 +- .../node-modules-dependencies-builder.ts | 22 +++-- test/contracts.ts | 2 +- test/controllers/add-platform-controller.ts | 1 + test/controllers/prepare-controller.ts | 2 +- test/extension-manifests.ts | 5 +- test/ios-project-service.ts | 1 + test/package-installation-manager.ts | 1 + test/package-manager-flags.ts | 7 +- test/plugins-service.ts | 1 + test/services/android-plugin-build-service.ts | 5 +- .../bundler/bundler-compiler-service.ts | 2 +- test/services/doctor-service.ts | 14 ++- test/services/extensibility-service.ts | 5 +- test/services/test-execution-service.ts | 5 +- test/stubs.ts | 5 +- .../node-modules-dependencies-builder.ts | 5 ++ 41 files changed, 218 insertions(+), 192 deletions(-) diff --git a/PublicAPI.md b/PublicAPI.md index 02dfc2055f..dd6d8f6151 100644 --- a/PublicAPI.md +++ b/PublicAPI.md @@ -579,16 +579,15 @@ Locates a package the way the selected package manager laid it out on disk, so c /** * @param {string} packageName The name of the package. * @param {string} fromDir The directory whose dependencies are searched, usually the project directory. - * @return {Promise} The absolute path of the package directory, or null when it is not installed. + * @return {string} The absolute path of the package directory, or null when it is not installed. */ -getInstalledPackagePath(packageName: string, fromDir: string): Promise; +getInstalledPackagePath(packageName: string, fromDir: string): string; ``` * Usage: ```JavaScript -tns.packageManager.getInstalledPackagePath("@nativescript/core", "/tmp/myProject").then(pathToPackage => { - console.log(pathToPackage ? `Installed at ${pathToPackage}` : "Not installed"); -}); +const pathToPackage = tns.packageManager.getInstalledPackagePath("@nativescript/core", "/tmp/myProject"); +console.log(pathToPackage ? `Installed at ${pathToPackage}` : "Not installed"); ``` ### view diff --git a/lib/commands/preview.ts b/lib/commands/preview.ts index 7b5b183915..f4f45ca6bf 100644 --- a/lib/commands/preview.ts +++ b/lib/commands/preview.ts @@ -37,7 +37,7 @@ export class PreviewCommand extends Command({ await this.installLatestPreviewCLI(); } - const previewCLIPath = await this.getPreviewCLIPath(); + const previewCLIPath = this.getPreviewCLIPath(); if (!previewCLIPath) { await this.failMissingPreviewCLI(); @@ -58,7 +58,7 @@ export class PreviewCommand extends Command({ ); } - private getPreviewCLIPath(): Promise { + private getPreviewCLIPath(): string { return this.$packageManager.getInstalledPackagePath( PREVIEW_CLI_PACKAGE, this.$projectData.projectDir, diff --git a/lib/commands/test-init.ts b/lib/commands/test-init.ts index 015559ff36..e5d4e2dbf2 100644 --- a/lib/commands/test-init.ts +++ b/lib/commands/test-init.ts @@ -137,7 +137,7 @@ export class TestInitCommand extends Command({ path: this.$options.path, }); - const modulePath = await this.$packageManager.getInstalledPackagePath( + const modulePath = this.$packageManager.getInstalledPackagePath( mod.name, projectDir, ); diff --git a/lib/commands/test.ts b/lib/commands/test.ts index 6b7f1743e2..0829bb97ec 100644 --- a/lib/commands/test.ts +++ b/lib/commands/test.ts @@ -106,7 +106,7 @@ async function canExecuteTestCommand( if ($vitestExecutionService.isVitestProject($projectData)) { const canStartTestRun = - await $vitestExecutionService.canStartTestRun($projectData); + $vitestExecutionService.canStartTestRun($projectData); if (!canStartTestRun) { $errors.fail({ formatStr: 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/contracts/doctor-service.ts b/lib/contracts/doctor-service.ts index 4c5a42840e..9a64fbd7a4 100644 --- a/lib/contracts/doctor-service.ts +++ b/lib/contracts/doctor-service.ts @@ -34,7 +34,5 @@ export abstract class DoctorService { }): Promise; /** Checks and notifies users of deprecated short imports in their app. */ - abstract checkForDeprecatedShortImportsInAppDir( - projectDir: string, - ): Promise; + abstract checkForDeprecatedShortImportsInAppDir(projectDir: string): void; } diff --git a/lib/contracts/package-manager.ts b/lib/contracts/package-manager.ts index 2619e3297d..010fffbd46 100644 --- a/lib/contracts/package-manager.ts +++ b/lib/contracts/package-manager.ts @@ -103,12 +103,12 @@ export abstract class PackageManager { * 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 {Promise} The absolute path of the package directory, or null when it is not installed. + * @return {string} The absolute path of the package directory, or null when it is not installed. */ abstract getInstalledPackagePath( packageName: string, fromDir: string, - ): Promise; + ): string; /** * Gets the name of the package manager used for the current process. diff --git a/lib/controllers/prepare-controller.ts b/lib/controllers/prepare-controller.ts index f2e21afb1f..fc730ddeab 100644 --- a/lib/controllers/prepare-controller.ts +++ b/lib/controllers/prepare-controller.ts @@ -494,11 +494,10 @@ export class PrepareController SCOPED_ANDROID_RUNTIME_NAME; } // try reading from installed runtime first before reading from the npm registry... - const installedRuntimePath = - await this.$packageManager.getInstalledPackagePath( - runtimePackageName, - projectData.projectDir, - ); + const installedRuntimePath = this.$packageManager.getInstalledPackagePath( + runtimePackageName, + projectData.projectDir, + ); if (installedRuntimePath) { installedRuntimePackageJSON = this.$fs.readJson( diff --git a/lib/declarations.d.ts b/lib/declarations.d.ts index c7c9c54126..a686771506 100644 --- a/lib/declarations.d.ts +++ b/lib/declarations.d.ts @@ -116,12 +116,9 @@ interface INodePackageManager { * 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 {Promise} The absolute path of the package directory, or null when it is not installed. + * @return {string} The absolute path of the package directory, or null when it is not installed. */ - getInstalledPackagePath( - packageName: string, - fromDir: string, - ): Promise; + getInstalledPackagePath(packageName: string, fromDir: string): string; } /** @deprecated Kept so existing annotations compile; use the {@link PackageManager} contract. */ diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index 468bf6c66d..cd2a931617 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -522,7 +522,7 @@ interface ITestExecutionService { interface IVitestExecutionService { isVitestProject(projectData: IProjectData): boolean; - canStartTestRun(projectData: IProjectData): Promise; + canStartTestRun(projectData: IProjectData): boolean; startTestRun(platform: string, projectData: IProjectData): Promise; } diff --git a/lib/package-managers/base-package-manager.ts b/lib/package-managers/base-package-manager.ts index 45a23d4e18..f44a7b85c4 100644 --- a/lib/package-managers/base-package-manager.ts +++ b/lib/package-managers/base-package-manager.ts @@ -145,10 +145,7 @@ export abstract class BasePackageManager implements INodePackageManager { }; } - public async getInstalledPackagePath( - packageName: string, - fromDir: string, - ): Promise { + public getInstalledPackagePath(packageName: string, fromDir: string): string { return resolvePackagePath(packageName, { paths: [fromDir] }) || null; } diff --git a/lib/package-managers/index.ts b/lib/package-managers/index.ts index d19f5f530c..180e855841 100644 --- a/lib/package-managers/index.ts +++ b/lib/package-managers/index.ts @@ -1,4 +1,4 @@ -import { cache, exported, invokeInit } from "../common/decorators"; +import { exported } from "../common/decorators"; import { performanceLog } from "../common/decorators"; import { PackageManagers } from "../constants"; import { @@ -11,15 +11,13 @@ import { INpmsResult, INpmPackageNameParts, } from "../declarations"; -import { - IErrors, - IUserSettingsService, -} from "../common/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,95 +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, - options: IPackageInstallOptions + options: IPackageInstallOptions, ): Promise { return this.packageManager.install(packageName, pathToSave, options); } + @exported("packageManager") - @invokeInit() public uninstall( packageName: string, options?: IPackageUninstallOptions, - path?: string + path?: string, ): Promise { return this.packageManager.uninstall(packageName, options, path); } + @exported("packageManager") - @invokeInit() public view(packageName: string, field?: string): Promise { return this.packageManager.view(packageName, field); } + @exported("packageManager") - @invokeInit() 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") - @invokeInit() - public getInstalledPackagePath( - packageName: string, - fromDir: string - ): Promise { + 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) { @@ -131,7 +113,7 @@ export class PackageManager implements IPackageManager { 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]; @@ -140,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}`, ); } @@ -156,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; } @@ -164,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/package-managers/package-installation-manager.ts b/lib/package-managers/package-installation-manager.ts index ffa2b04e8b..807c4fc724 100644 --- a/lib/package-managers/package-installation-manager.ts +++ b/lib/package-managers/package-installation-manager.ts @@ -189,7 +189,7 @@ export class PackageInstallationManager implements IPackageInstallationManager { projectDir: string ): Promise { // local installation takes precedence over cache - const inspectorPath = await this.$packageManager.getInstalledPackagePath( + const inspectorPath = this.$packageManager.getInstalledPackagePath( inspectorNpmPackageName, projectDir ); diff --git a/lib/services/android-plugin-build-service.ts b/lib/services/android-plugin-build-service.ts index 31a4cecf96..0d4021ac6e 100644 --- a/lib/services/android-plugin-build-service.ts +++ b/lib/services/android-plugin-build-service.ts @@ -506,7 +506,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { return runtimeVersion; } - private async getLocalGradleVersions(): Promise { + private getLocalGradleVersions(): IRuntimeGradleVersions { // partial interface of the runtime package.json // including new 8.2+ format and legacy interface IRuntimePackageJSON { @@ -526,11 +526,10 @@ 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 installedRuntimePath = - await this.$packageManager.getInstalledPackagePath( - packageName, - this.$projectData.projectDir, - ); + const installedRuntimePath = this.$packageManager.getInstalledPackagePath( + packageName, + this.$projectData.projectDir, + ); if (!installedRuntimePath) { return null; @@ -573,7 +572,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { versions: { gradle: string; gradleAndroid: string }; } = null; - const localVersionInfo = await this.getLocalGradleVersions(); + const localVersionInfo = this.getLocalGradleVersions(); if (localVersionInfo) { return localVersionInfo; diff --git a/lib/services/bundler/bundler-compiler-service.ts b/lib/services/bundler/bundler-compiler-service.ts index b613860817..2c05eb6183 100644 --- a/lib/services/bundler/bundler-compiler-service.ts +++ b/lib/services/bundler/bundler-compiler-service.ts @@ -569,9 +569,8 @@ export class BundlerCompilerService additionalNodeArgs.unshift("--max_old_space_size=4096"); } - const bundlerExecutablePath = - await this.getBundlerExecutablePath(projectData); - const isModernBundler = await this.isModernBundler(projectData); + const bundlerExecutablePath = this.getBundlerExecutablePath(projectData); + const isModernBundler = this.isModernBundler(projectData); const args = [ ...additionalNodeArgs, bundlerExecutablePath, @@ -725,7 +724,7 @@ export class BundlerCompilerService // go after `--` so vite's CLI doesn't choke on unknown options. const args = [ ...additionalNodeArgs, - await this.getBundlerExecutablePath(projectData), + this.getBundlerExecutablePath(projectData), "serve", `--config=${projectData.bundlerConfigPath}`, `--mode=development`, @@ -1165,9 +1164,7 @@ export class BundlerCompilerService }); } - private async getBundlerExecutablePath( - projectData: IProjectData, - ): Promise { + private getBundlerExecutablePath(projectData: IProjectData): string { const bundler = this.getBundler(); const resolve = (packageName: string) => this.$packageManager.getInstalledPackagePath( @@ -1176,13 +1173,13 @@ export class BundlerCompilerService ); if (bundler === "vite") { - const packagePath = await resolve("vite"); + const packagePath = resolve("vite"); if (packagePath) { return path.resolve(packagePath, "bin", "vite.js"); } - } else if (await this.isModernBundler(projectData)) { - const packagePath = await resolve(this.getBundlerPackageName()); + } else if (this.isModernBundler(projectData)) { + const packagePath = resolve(this.getBundlerPackageName()); if (packagePath) { return path.resolve(packagePath, "dist", "bin", "index.js"); @@ -1202,7 +1199,7 @@ export class BundlerCompilerService ); } - const packagePath = await resolve("webpack"); + const packagePath = resolve("webpack"); if (!packagePath) { return ""; @@ -1224,13 +1221,13 @@ export class BundlerCompilerService ); } - private async isModernBundler(projectData: IProjectData): Promise { + private isModernBundler(projectData: IProjectData): boolean { const bundler = this.getBundler(); switch (bundler) { case "rspack": return true; default: - const packagePath = await this.$packageManager.getInstalledPackagePath( + const packagePath = this.$packageManager.getInstalledPackagePath( this.getBundlerPackageName(), projectData.projectDir, ); diff --git a/lib/services/doctor-service.ts b/lib/services/doctor-service.ts index 3a2b355713..3c41698968 100644 --- a/lib/services/doctor-service.ts +++ b/lib/services/doctor-service.ts @@ -139,7 +139,7 @@ export class DoctorServiceImpl implements DoctorService { } // todo: check for deprecated imports from `tns-core-modules` - await this.checkForDeprecatedShortImportsInAppDir(configOptions.projectDir); + this.checkForDeprecatedShortImportsInAppDir(configOptions.projectDir); await this.$injector .resolve( @@ -241,14 +241,12 @@ export class DoctorServiceImpl implements DoctorService { return !hasWarnings; } - public async checkForDeprecatedShortImportsInAppDir( - projectDir: string, - ): Promise { + public checkForDeprecatedShortImportsInAppDir(projectDir: string): void { if (projectDir) { try { const files = this.$projectDataService.getAppExecutableFiles(projectDir); - const shortImports = await this.getDeprecatedShortImportsInFiles( + const shortImports = this.getDeprecatedShortImportsInFiles( files, projectDir, ); @@ -271,11 +269,11 @@ export class DoctorServiceImpl implements DoctorService { } } - protected async getDeprecatedShortImportsInFiles( + protected getDeprecatedShortImportsInFiles( files: string[], projectDir: string, - ): Promise<{ file: string; line: string }[]> { - const shortImportRegExp = await this.getShortImportRegExp(projectDir); + ): { file: string; line: string }[] { + const shortImportRegExp = this.getShortImportRegExp(projectDir); const shortImports: { file: string; line: string }[] = []; if (!shortImportRegExp) { return shortImports; @@ -306,12 +304,11 @@ export class DoctorServiceImpl implements DoctorService { return shortImports; } - private async getShortImportRegExp(projectDir: string): Promise { - const pathToTnsCoreModules = - await this.$packageManager.getInstalledPackagePath( - TNS_CORE_MODULES_NAME, - projectDir, - ); + private getShortImportRegExp(projectDir: string): RegExp { + const pathToTnsCoreModules = this.$packageManager.getInstalledPackagePath( + TNS_CORE_MODULES_NAME, + projectDir, + ); if (!pathToTnsCoreModules) { return null; } diff --git a/lib/services/extensibility-service.ts b/lib/services/extensibility-service.ts index aee3ef6cbf..54de9ba694 100644 --- a/lib/services/extensibility-service.ts +++ b/lib/services/extensibility-service.ts @@ -512,7 +512,7 @@ export class ExtensibilityService implements IExtensibilityService { extensionName: string, ): Promise { this.$logger.trace(`Asserting extension ${extensionName} is installed.`); - const installedPath = await this.$packageManager.getInstalledPackagePath( + const installedPath = this.$packageManager.getInstalledPackagePath( extensionName, this.pathToExtensions, ); diff --git a/lib/services/plugins-service.ts b/lib/services/plugins-service.ts index dffe4497b5..34880da5b2 100644 --- a/lib/services/plugins-service.ts +++ b/lib/services/plugins-service.ts @@ -96,7 +96,7 @@ export class PluginsService implements IPluginsService { this.npmInstallOptions, ) ).name; - const pathToRealNpmPackageJson = await this.getPackageJsonFilePathForModule( + const pathToRealNpmPackageJson = this.getPackageJsonFilePathForModule( name, projectData.projectDir, ); @@ -144,7 +144,7 @@ export class PluginsService implements IPluginsService { platformData: IPlatformData, ): Promise => { const pluginData = this.convertToPluginData( - await this.getNodeModuleData(pluginName, projectData.projectDir), + this.getNodeModuleData(pluginName, projectData.projectDir), projectData.projectDir, ); @@ -297,7 +297,7 @@ export class PluginsService implements IPluginsService { const notInstalledDependencies: string[] = []; for (const dep of allDependencies) { this.$logger.trace(`Checking if ${dep} is installed...`); - const pathToPackage = await this.$packageManager.getInstalledPackagePath( + const pathToPackage = this.$packageManager.getInstalledPackagePath( dep, projectData.projectDir, ); @@ -694,11 +694,11 @@ This framework comes from ${dependencyName} plugin, which is installed multiple return path.join(projectDir, "package.json"); } - private async getPackageJsonFilePathForModule( + private getPackageJsonFilePathForModule( moduleName: string, projectDir: string, - ): Promise { - const pathToModule = await this.$packageManager.getInstalledPackagePath( + ): string { + const pathToModule = this.$packageManager.getInstalledPackagePath( moduleName, projectDir, ); @@ -710,13 +710,13 @@ This framework comes from ${dependencyName} plugin, which is installed multiple return _.keys(require(packageJsonFilePath).dependencies); } - private async getNodeModuleData( + private getNodeModuleData( module: string, projectDir: string, - ): Promise { + ): INodeModuleData { // module can be modulePath or moduleName if (!this.$fs.exists(module) || path.basename(module) !== "package.json") { - const resolvedPath = await this.getPackageJsonFilePathForModule( + const resolvedPath = this.getPackageJsonFilePathForModule( module, projectDir, ); @@ -745,12 +745,11 @@ This framework comes from ${dependencyName} plugin, which is installed multiple await this.ensureAllDependenciesAreInstalled(projectData); const nodeModules = this.getDependencies(projectData.projectDir); - const modules = await Promise.all( - nodeModules.map((nodeModuleName) => + return nodeModules + .map((nodeModuleName) => this.getNodeModuleData(nodeModuleName, projectData.projectDir), - ), - ); - return modules.filter(Boolean); + ) + .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 e640dcf1fe..2733aea4c5 100644 --- a/lib/services/test-execution-service.ts +++ b/lib/services/test-execution-service.ts @@ -144,7 +144,7 @@ export class TestExecutionService implements ITestExecutionService { } }); - const pathToKarma = await this.$packageManager.getInstalledPackagePath( + const pathToKarma = this.$packageManager.getInstalledPackagePath( "karma", projectData.projectDir, ); 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 dc14694ca1..19c9f2621d 100644 --- a/lib/services/versions-service.ts +++ b/lib/services/versions-service.ts @@ -73,8 +73,8 @@ class VersionsService implements IVersionsService { packageName, this.projectData.projectDir ); - let scopedPackagePath = await resolve(constants.SCOPED_TNS_CORE_MODULES); - let tnsCoreModulesPath = await resolve(constants.TNS_CORE_MODULES_NAME); + 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 @@ -91,8 +91,8 @@ class VersionsService implements IVersionsService { await this.$pluginsService.ensureAllDependenciesAreInstalled( this.projectData ); - scopedPackagePath = await resolve(constants.SCOPED_TNS_CORE_MODULES); - tnsCoreModulesPath = await resolve(constants.TNS_CORE_MODULES_NAME); + scopedPackagePath = resolve(constants.SCOPED_TNS_CORE_MODULES); + tnsCoreModulesPath = resolve(constants.TNS_CORE_MODULES_NAME); } if (dependsOnNonScopedPackage && tnsCoreModulesPath) { diff --git a/lib/services/vitest-execution-service.ts b/lib/services/vitest-execution-service.ts index 41b5593791..80c09e8235 100644 --- a/lib/services/vitest-execution-service.ts +++ b/lib/services/vitest-execution-service.ts @@ -25,10 +25,10 @@ export class VitestExecutionService implements IVitestExecutionService { return !!this.getConfigPath(projectData); } - public async canStartTestRun(projectData: IProjectData): Promise { + public canStartTestRun(projectData: IProjectData): boolean { return ( this.isVitestProject(projectData) && - !!(await this.getVitestPackagePath(projectData)) + !!this.getVitestPackagePath(projectData) ); } @@ -36,7 +36,7 @@ export class VitestExecutionService implements IVitestExecutionService { platform: string, projectData: IProjectData, ): Promise { - const vitestPackagePath = await this.getVitestPackagePath(projectData); + const vitestPackagePath = this.getVitestPackagePath(projectData); if (!vitestPackagePath) { this.$errors.fail( "Unable to find 'vitest' in the project. Run '$ ns test init --framework vitest' first.", @@ -89,7 +89,7 @@ export class VitestExecutionService implements IVitestExecutionService { return null; } - private getVitestPackagePath(projectData: IProjectData): Promise { + private getVitestPackagePath(projectData: IProjectData): string { return this.$packageManager.getInstalledPackagePath( "vitest", projectData.projectDir, 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/contracts.ts b/test/contracts.ts index 1be08d6c2a..e07581e01e 100644 --- a/test/contracts.ts +++ b/test/contracts.ts @@ -60,7 +60,7 @@ describe("contracts tranche", () => { async canExecuteLocalBuild(): Promise { return true; } - async checkForDeprecatedShortImportsInAppDir(): Promise {} + checkForDeprecatedShortImportsInAppDir(): void {} } const injector = new Injector([provide(DoctorService, StubDoctorService)]); diff --git a/test/controllers/add-platform-controller.ts b/test/controllers/add-platform-controller.ts index be90db2cbf..934c84d26e 100644 --- a/test/controllers/add-platform-controller.ts +++ b/test/controllers/add-platform-controller.ts @@ -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 050e8bf5b0..553bcbe7bc 100644 --- a/test/controllers/prepare-controller.ts +++ b/test/controllers/prepare-controller.ts @@ -52,7 +52,7 @@ function createTestInjector(data: { hasNativeChanges: boolean }): IInjector { injector.register("mobileHelper", MobileHelper); injector.register("prepareController", PrepareController); injector.register("packageManager", { - getInstalledPackagePath: async (): Promise => null, + getInstalledPackagePath: (): string => null, }); injector.register("nodeModulesDependenciesBuilder", { diff --git a/test/extension-manifests.ts b/test/extension-manifests.ts index faaba51cfa..33bbe53d80 100644 --- a/test/extension-manifests.ts +++ b/test/extension-manifests.ts @@ -185,10 +185,7 @@ describe("extension manifests", () => { install: async (): Promise => { throw new Error("Extensions are expected to be installed already."); }, - getInstalledPackagePath: async ( - packageName: string, - fromDir: string, - ): Promise => + getInstalledPackagePath: (packageName: string, fromDir: string): string => resolvePackagePath(packageName, { paths: [fromDir] }) || null, uninstall: async (): Promise => undefined, searchNpms: async (): Promise => ({ results: [] }), diff --git a/test/ios-project-service.ts b/test/ios-project-service.ts index 09b124c0fe..9d610a3d3f 100644 --- a/test/ios-project-service.ts +++ b/test/ios-project-service.ts @@ -180,6 +180,7 @@ function createTestInjector( ); testInjector.register("userSettingsService", { getSettingValue: async (settingName: string): Promise => undefined, + getSettingValueSync: (settingName: string): void => undefined, }); testInjector.register("packageManager", PackageManager); testInjector.register("projectConfigService", ProjectConfigServiceStub); diff --git a/test/package-installation-manager.ts b/test/package-installation-manager.ts index 7a1622fb90..df8773c66e 100644 --- a/test/package-installation-manager.ts +++ b/test/package-installation-manager.ts @@ -45,6 +45,7 @@ function createTestInjector(): IInjector { }); testInjector.register("userSettingsService", { getSettingValue: async (settingName: string): Promise => undefined, + getSettingValueSync: (settingName: string): void => undefined, }); testInjector.register("npm", NpmLib.NpmPackageManager); testInjector.register("yarn", YarnLib.YarnPackageManager); diff --git a/test/package-manager-flags.ts b/test/package-manager-flags.ts index 27e9a9ac11..23da5b55c2 100644 --- a/test/package-manager-flags.ts +++ b/test/package-manager-flags.ts @@ -232,10 +232,7 @@ describe("package manager flag mapping", () => { name, ctor, ).resolve(name); - const resolved = await manager.getInstalledPackagePath( - "lodash", - repoRoot, - ); + const resolved = manager.getInstalledPackagePath("lodash", repoRoot); assert.equal(resolved, path.join(repoRoot, "node_modules", "lodash")); }); @@ -245,7 +242,7 @@ describe("package manager flag mapping", () => { ctor, ).resolve(name); assert.isNull( - await manager.getInstalledPackagePath( + manager.getInstalledPackagePath( "definitely-not-installed-package", repoRoot, ), diff --git a/test/plugins-service.ts b/test/plugins-service.ts index 4c7eb6c57e..94b33dce79 100644 --- a/test/plugins-service.ts +++ b/test/plugins-service.ts @@ -69,6 +69,7 @@ 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( diff --git a/test/services/android-plugin-build-service.ts b/test/services/android-plugin-build-service.ts index 0f8332d5c9..8e5a8c3748 100644 --- a/test/services/android-plugin-build-service.ts +++ b/test/services/android-plugin-build-service.ts @@ -129,10 +129,7 @@ describe("androidPluginBuildService", () => { addProjectRuntime?: boolean; }): any { return { - getInstalledPackagePath: async ( - packageName: string, - fromDir: string, - ): Promise => + getInstalledPackagePath: (packageName: string, fromDir: string): string => resolvePackagePath(packageName, { paths: [fromDir] }) || null, getRegistryPackageData: async (packageName: string): Promise => { const result: any = []; diff --git a/test/services/bundler/bundler-compiler-service.ts b/test/services/bundler/bundler-compiler-service.ts index 36fe0bf39b..94b1c84986 100644 --- a/test/services/bundler/bundler-compiler-service.ts +++ b/test/services/bundler/bundler-compiler-service.ts @@ -58,7 +58,7 @@ function createTestInjector( const testInjector = new Yok(); testInjector.register("packageManager", { getPackageManagerName: async () => packageManager, - getInstalledPackagePath: async (): Promise => null, + 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 efe7654871..2adfc0eb2f 100644 --- a/test/services/doctor-service.ts +++ b/test/services/doctor-service.ts @@ -65,7 +65,7 @@ class DoctorServiceInheritor extends DoctorService { public getDeprecatedShortImportsInFiles( files: string[], projectDir: string, - ): Promise<{ file: string; line: string }[]> { + ): { file: string; line: string }[] { return super.getDeprecatedShortImportsInFiles(files, projectDir); } } @@ -96,10 +96,7 @@ describe("doctorService", () => { }); testInjector.register("versionsService", {}); testInjector.register("packageManager", { - getInstalledPackagePath: async ( - packageName: string, - fromDir: string, - ): Promise => + getInstalledPackagePath: (packageName: string, fromDir: string): string => packageName === "tns-core-modules" ? path.join(fromDir, "node_modules", packageName) : null, @@ -380,7 +377,7 @@ const Observable = require("tns-core-modules-widgets/data/observable").Observabl fs.readText = (filePath) => filesContents[filePath]; const shortImports = - await doctorService.getDeprecatedShortImportsInFiles( + doctorService.getDeprecatedShortImportsInFiles( _.keys(filesContents), "projectDir", ); @@ -391,14 +388,13 @@ const Observable = require("tns-core-modules-widgets/data/observable").Observabl it("getDeprecatedShortImportsInFiles returns no results when tns-core-modules is not installed", async () => { const testInjector = createTestInjector(); const packageManager = testInjector.resolve("packageManager"); - packageManager.getInstalledPackagePath = async (): Promise => - null; + packageManager.getInstalledPackagePath = (): string => null; const doctorService = testInjector.resolve("doctorService"); const fs = testInjector.resolve("fs"); fs.readText = () => 'const application = require("application");'; - const shortImports = await doctorService.getDeprecatedShortImportsInFiles( + const shortImports = doctorService.getDeprecatedShortImportsInFiles( ["file1"], "projectDir", ); diff --git a/test/services/extensibility-service.ts b/test/services/extensibility-service.ts index eae286db8d..132acff774 100644 --- a/test/services/extensibility-service.ts +++ b/test/services/extensibility-service.ts @@ -74,6 +74,7 @@ describe("extensibilityService", () => { }); testInjector.register("userSettingsService", { getSettingValue: async (settingName: string): Promise => undefined, + getSettingValueSync: (settingName: string): void => undefined, }); testInjector.register("npm", NpmPackageManager); testInjector.register("yarn", YarnPackageManager); @@ -92,10 +93,10 @@ describe("extensibilityService", () => { resolve: (extensionName: string, fromDir: string) => string, ): void => { const packageManager = testInjector.resolve("packageManager"); - packageManager.getInstalledPackagePath = async ( + packageManager.getInstalledPackagePath = ( packageName: string, fromDir: string, - ): Promise => resolve(packageName, fromDir); + ): string => resolve(packageName, fromDir); }; const getExpectedInstallationPathForExtension = ( diff --git a/test/services/test-execution-service.ts b/test/services/test-execution-service.ts index 7daa0d3f7c..bd85b8b3f6 100644 --- a/test/services/test-execution-service.ts +++ b/test/services/test-execution-service.ts @@ -15,10 +15,7 @@ function getTestExecutionService( injector.register("testExecutionService", TestExecutionService); injector.register("runController", {}); injector.register("packageManager", { - getInstalledPackagePath: async ( - packageName: string, - fromDir: string, - ): Promise => + getInstalledPackagePath: (packageName: string, fromDir: string): string => installedPackages.indexOf(packageName) !== -1 ? `${fromDir}/node_modules/${packageName}` : null, diff --git a/test/stubs.ts b/test/stubs.ts index 533714ee1f..6be3af35fc 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -457,10 +457,7 @@ export class PackageInstallationManagerStub implements IPackageInstallationManag export class NodePackageManagerStub implements INodePackageManager { constructor() {} - public async getInstalledPackagePath( - packageName: string, - fromDir: string, - ): Promise { + public getInstalledPackagePath(packageName: string, fromDir: string): string { return null; } 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; };