From 684b83d6716cdf52db994f47c2df346d185e873a Mon Sep 17 00:00:00 2001 From: Jason Cassidy Date: Fri, 26 Sep 2025 16:38:41 +0100 Subject: [PATCH 1/4] feat: plugin hooks command --- lib/bootstrap.ts | 1 + lib/commands/plugin/hooks-plugin.ts | 71 ++++++++++ lib/services/plugins-service.ts | 209 ++++++++++++++-------------- package-lock.json | 93 ++++++++++++- package.json | 3 +- 5 files changed, 267 insertions(+), 110 deletions(-) create mode 100644 lib/commands/plugin/hooks-plugin.ts diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index ad181a1062..9676767b06 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -262,6 +262,7 @@ injector.requireCommand("plugin|remove", "./commands/plugin/remove-plugin"); injector.requireCommand("plugin|update", "./commands/plugin/update-plugin"); injector.requireCommand("plugin|build", "./commands/plugin/build-plugin"); injector.requireCommand("plugin|create", "./commands/plugin/create-plugin"); +injector.requireCommand("plugin|hooks", "./commands/plugin/hooks-plugin"); injector.require("doctorService", "./services/doctor-service"); injector.require("xcprojService", "./services/xcproj-service"); diff --git a/lib/commands/plugin/hooks-plugin.ts b/lib/commands/plugin/hooks-plugin.ts new file mode 100644 index 0000000000..51c95a7a0e --- /dev/null +++ b/lib/commands/plugin/hooks-plugin.ts @@ -0,0 +1,71 @@ +import * as _ from "lodash"; +import { IProjectData } from "../../definitions/project"; +import { IPluginsService, IPluginData } from "../../definitions/plugins"; +import { ICommand, ICommandParameter } from "../../common/definitions/commands"; +import { IErrors, IFileSystem } from "../../common/declarations"; +import { injector } from "../../common/yok"; +import path = require("path"); +import { HOOKS_DIR_NAME } from "../../constants"; +import { createTable } from "../../common/helpers"; +import nsHooks = require("@nativescript/hook"); +export class HooksPluginCommand implements ICommand { + public allowedParameters: ICommandParameter[] = []; + + constructor( + private $pluginsService: IPluginsService, + private $projectData: IProjectData, + private $errors: IErrors, + private $fs: IFileSystem, + private $logger: ILogger, + ) { + this.$projectData.initializeProjectData(); + } + + public async execute(args: string[]): Promise { + const isList: boolean = + args.length > 0 && args[0] === "list" ? true : false; + const plugins: IPluginData[] = + await this.$pluginsService.getAllInstalledPlugins(this.$projectData); + if (plugins && plugins.length > 0) { + const hooksDir = path.join(this.$projectData.projectDir, HOOKS_DIR_NAME); + console.log(hooksDir); + const pluginsWithHooks: IPluginData[] = []; + for (const plugin of plugins) { + if (plugin.nativescript?.hooks?.length > 0) { + pluginsWithHooks.push(plugin); + } + } + + if (isList) { + const headers: string[] = ["Plugin", "HookName", "HookPath"]; + const hookDataData: string[][] = pluginsWithHooks.flatMap((plugin) => + plugin.nativescript.hooks.map( + (hook: { type: string; script: string }) => { + return [plugin.name, hook.type, hook.script]; + }, + ), + ); + const hookDataTable: any = createTable(headers, hookDataData); + this.$logger.info("Hooks:"); + this.$logger.info(hookDataTable.toString()); + } else { + if (pluginsWithHooks.length === 0) { + if (!this.$fs.exists(hooksDir)) { + this.$fs.createDirectory(hooksDir); + } + } + for (const plugin of pluginsWithHooks) { + nsHooks(plugin.fullPath).postinstall(); + } + } + } + } + + public async canExecute(args: string[]): Promise { + if (args?.length > 50) { + this.$errors.fail(`Plugin is already installed.`); + } + return true; + } +} +injector.registerCommand(["plugin|hooks"], HooksPluginCommand); diff --git a/lib/services/plugins-service.ts b/lib/services/plugins-service.ts index e9b5e6a54c..f5b850cdbf 100644 --- a/lib/services/plugins-service.ts +++ b/lib/services/plugins-service.ts @@ -67,7 +67,7 @@ export class PluginsService implements IPluginsService { ignoreScripts: this.$options.ignoreScripts, path: this.$options.path, }, - PluginsService.NPM_CONFIG + PluginsService.NPM_CONFIG, ); } @@ -80,7 +80,7 @@ export class PluginsService implements IPluginsService { private $filesHashService: IFilesHashService, private $injector: IInjector, private $mobileHelper: Mobile.IMobileHelper, - private $nodeModulesDependenciesBuilder: INodeModulesDependenciesBuilder + private $nodeModulesDependenciesBuilder: INodeModulesDependenciesBuilder, ) {} public async add(plugin: string, projectData: IProjectData): Promise { @@ -97,26 +97,26 @@ export class PluginsService implements IPluginsService { await this.$packageManager.install( plugin, projectData.projectDir, - this.npmInstallOptions + this.npmInstallOptions, ) ).name; const pathToRealNpmPackageJson = this.getPackageJsonFilePathForModule( name, - projectData.projectDir + projectData.projectDir, ); const realNpmPackageJson = this.$fs.readJson(pathToRealNpmPackageJson); if (realNpmPackageJson.nativescript) { const pluginData = this.convertToPluginData( realNpmPackageJson, - projectData.projectDir + projectData.projectDir, ); // Validate const action = async ( pluginDestinationPath: string, platform: constants.PlatformTypes, - platformData: IPlatformData + platformData: IPlatformData, ): Promise => { this.isPluginDataValidForPlatform(pluginData, platform, projectData); }; @@ -124,61 +124,61 @@ export class PluginsService implements IPluginsService { await this.executeForAllInstalledPlatforms(action, projectData); this.$logger.info( - `Successfully installed plugin ${realNpmPackageJson.name}.` + `Successfully installed plugin ${realNpmPackageJson.name}.`, ); } else { await this.$packageManager.uninstall( realNpmPackageJson.name, { save: true }, - projectData.projectDir + projectData.projectDir, ); this.$errors.fail( - `${plugin} is not a valid NativeScript plugin. Verify that the plugin package.json file contains a nativescript key and try again.` + `${plugin} is not a valid NativeScript plugin. Verify that the plugin package.json file contains a nativescript key and try again.`, ); } } public async remove( pluginName: string, - projectData: IProjectData + projectData: IProjectData, ): Promise { const removePluginNativeCodeAction = async ( modulesDestinationPath: string, platform: string, - platformData: IPlatformData + platformData: IPlatformData, ): Promise => { const pluginData = this.convertToPluginData( this.getNodeModuleData(pluginName, projectData.projectDir), - projectData.projectDir + projectData.projectDir, ); await platformData.platformProjectService.removePluginNativeCode( pluginData, - projectData + projectData, ); }; await this.executeForAllInstalledPlatforms( removePluginNativeCodeAction, - projectData + projectData, ); await this.executeNpmCommand( PluginsService.UNINSTALL_COMMAND_NAME, pluginName, - projectData + projectData, ); let showMessage = true; const action = async ( modulesDestinationPath: string, platform: string, - platformData: IPlatformData + platformData: IPlatformData, ): Promise => { shelljs.rm("-rf", path.join(modulesDestinationPath, pluginName)); this.$logger.info( - `Successfully removed plugin ${pluginName} for ${platform}.` + `Successfully removed plugin ${pluginName} for ${platform}.`, ); showMessage = false; }; @@ -194,7 +194,7 @@ export class PluginsService implements IPluginsService { plugin: string, version: string, isDev: boolean, - projectDir: string + projectDir: string, ) { const packageJsonPath = this.getPackageJsonFilePath(projectDir); let packageJsonContent = this.$fs.readJson(packageJsonPath); @@ -206,7 +206,7 @@ export class PluginsService implements IPluginsService { ) { const result = this.removeDependencyFromPackageJsonContent( plugin, - packageJsonContent + packageJsonContent, ); packageJsonContent = result.packageJsonContent; } @@ -222,7 +222,7 @@ export class PluginsService implements IPluginsService { const packageJsonContent = this.$fs.readJson(packageJsonPath); const result = this.removeDependencyFromPackageJsonContent( plugin, - packageJsonContent + packageJsonContent, ); if (result.hasModifiedPackageJson) { @@ -237,7 +237,7 @@ export class PluginsService implements IPluginsService { }: IPreparePluginNativeCodeData): Promise { const platformData = this.$platformsDataService.getPlatformData( platform, - projectData + projectData, ); const pluginPlatformsFolderPath = @@ -245,31 +245,31 @@ export class PluginsService implements IPluginsService { if (this.$fs.exists(pluginPlatformsFolderPath)) { const pathToPluginsBuildFile = path.join( platformData.projectRoot, - constants.PLUGINS_BUILD_DATA_FILENAME + constants.PLUGINS_BUILD_DATA_FILENAME, ); const allPluginsNativeHashes = this.getAllPluginsNativeHashes( - pathToPluginsBuildFile + pathToPluginsBuildFile, ); const oldPluginNativeHashes = allPluginsNativeHashes[pluginData.name]; const currentPluginNativeHashes = await this.getPluginNativeHashes( - pluginPlatformsFolderPath + pluginPlatformsFolderPath, ); if ( !oldPluginNativeHashes || this.$filesHashService.hasChangesInShasums( oldPluginNativeHashes, - currentPluginNativeHashes + currentPluginNativeHashes, ) ) { await platformData.platformProjectService.preparePluginNativeCode( pluginData, - projectData + projectData, ); const updatedPluginNativeHashes = await this.getPluginNativeHashes( - pluginPlatformsFolderPath + pluginPlatformsFolderPath, ); this.setPluginNativeHashes({ @@ -283,13 +283,13 @@ export class PluginsService implements IPluginsService { } public async ensureAllDependenciesAreInstalled( - projectData: IProjectData + projectData: IProjectData, ): Promise { const packageJsonContent = this.$fs.readJson( - this.getPackageJsonFilePath(projectData.projectDir) + this.getPackageJsonFilePath(projectData.projectDir), ); const allDependencies = _.keys(packageJsonContent.dependencies).concat( - _.keys(packageJsonContent.devDependencies) + _.keys(packageJsonContent.devDependencies), ); const notInstalledDependencies = allDependencies @@ -316,7 +316,7 @@ export class PluginsService implements IPluginsService { "Npm install will be called from CLI. Force option is: ", this.$options.force, " Not installed dependencies are: ", - notInstalledDependencies + notInstalledDependencies, ); await this.$packageManager.install( projectData.projectDir, @@ -326,34 +326,34 @@ export class PluginsService implements IPluginsService { frameworkPath: this.$options.frameworkPath, ignoreScripts: this.$options.ignoreScripts, path: this.$options.path, - } + }, ); } } public async getAllInstalledPlugins( - projectData: IProjectData + projectData: IProjectData, ): Promise { const nodeModules = (await this.getAllInstalledModules(projectData)).map( (nodeModuleData) => - this.convertToPluginData(nodeModuleData, projectData.projectDir) + this.convertToPluginData(nodeModuleData, projectData.projectDir), ); return _.filter( nodeModules, - (nodeModuleData) => nodeModuleData && nodeModuleData.isPlugin + (nodeModuleData) => nodeModuleData && nodeModuleData.isPlugin, ); } public getAllProductionPlugins( projectData: IProjectData, platform: string, - dependencies?: IDependencyData[] + dependencies?: IDependencyData[], ): IPluginData[] { dependencies = dependencies || this.$nodeModulesDependenciesBuilder.getProductionDependencies( projectData.projectDir, - projectData.ignoredDependencies + projectData.ignoredDependencies, ); if (_.isEmpty(dependencies)) { @@ -361,12 +361,12 @@ export class PluginsService implements IPluginsService { } let productionPlugins: IDependencyData[] = dependencies.filter( - (d) => !!d.nativescript + (d) => !!d.nativescript, ); productionPlugins = this.ensureValidProductionPlugins( productionPlugins, projectData.projectDir, - platform + platform, ); return productionPlugins .map((plugin) => this.convertToPluginData(plugin, projectData.projectDir)) @@ -379,17 +379,17 @@ export class PluginsService implements IPluginsService { } public getDependenciesFromPackageJson( - projectDir: string + projectDir: string, ): IPackageJsonDepedenciesResult { const packageJson = this.$fs.readJson( - this.getPackageJsonFilePath(projectDir) + this.getPackageJsonFilePath(projectDir), ); const dependencies: IBasePluginData[] = this.getBasicPluginInformation( - packageJson.dependencies + packageJson.dependencies, ); const devDependencies: IBasePluginData[] = this.getBasicPluginInformation( - packageJson.devDependencies + packageJson.devDependencies, ); return { @@ -407,27 +407,27 @@ export class PluginsService implements IPluginsService { ( productionDependencies: IDependencyData[], projectDir: string, - platform: string + platform: string, ) => IDependencyData[] >( this._ensureValidProductionPlugins, ( productionDependencies: IDependencyData[], projectDir: string, - platform: string + platform: string, ) => { let key = _.sortBy(productionDependencies, (p) => p.directory) .map((d) => JSON.stringify(d, null, 2)) .join("\n"); key += projectDir + platform; return key; - } + }, ); private _ensureValidProductionPlugins( productionDependencies: IDependencyData[], projectDir: string, - platform: string + platform: string, ): IDependencyData[] { let clonedProductionDependencies = _.cloneDeep(productionDependencies); platform = platform.toLowerCase(); @@ -438,7 +438,7 @@ export class PluginsService implements IPluginsService { clonedProductionDependencies = this.ensureValidProductionPluginsForIOS( clonedProductionDependencies, projectDir, - platform + platform, ); } @@ -446,11 +446,11 @@ export class PluginsService implements IPluginsService { } private ensureValidProductionPluginsForAndroid( - productionDependencies: IDependencyData[] + productionDependencies: IDependencyData[], ): void { const dependenciesGroupedByName = _.groupBy( productionDependencies, - (p) => p.name + (p) => p.name, ); _.each( dependenciesGroupedByName, @@ -459,36 +459,36 @@ export class PluginsService implements IPluginsService { // the dependency exists multiple times in node_modules const dependencyOccurrencesGroupedByVersion = _.groupBy( dependencyOccurrences, - (g) => g.version + (g) => g.version, ); const versions = _.keys(dependencyOccurrencesGroupedByVersion); if (versions.length === 1) { // all dependencies with this name have the same version this.$logger.trace( `Detected same versions (${_.first( - versions + versions, )}) of the ${dependencyName} installed at locations: ${_.map( dependencyOccurrences, - (d) => d.directory - ).join(", ")}` + (d) => d.directory, + ).join(", ")}`, ); } else { this.$logger.trace( `Detected different versions of the ${dependencyName} installed at locations: ${_.map( dependencyOccurrences, - (d) => d.directory - ).join(", ")}\nThis can cause build failures.` + (d) => d.directory, + ).join(", ")}\nThis can cause build failures.`, ); } } - } + }, ); } private ensureValidProductionPluginsForIOS( productionDependencies: IDependencyData[], projectDir: string, - platform: string + platform: string, ): IDependencyData[] { const dependenciesWithFrameworks: any[] = []; _.each(productionDependencies, (d) => { @@ -510,7 +510,7 @@ export class PluginsService implements IPluginsService { if (dependenciesWithFrameworks.length > 0) { const dependenciesGroupedByFrameworkName = _.groupBy( dependenciesWithFrameworks, - (d) => d.frameworkName + (d) => d.frameworkName, ); _.each( dependenciesGroupedByFrameworkName, @@ -519,16 +519,16 @@ export class PluginsService implements IPluginsService { // A framework exists multiple times in node_modules const groupedByName = _.groupBy( dependencyOccurrences, - (d) => d.name + (d) => d.name, ); const pluginsNames = _.keys(groupedByName); if (pluginsNames.length > 1) { // fail - the same framework is installed by different dependencies. const locations = dependencyOccurrences.map( - (d) => d.frameworkLocation + (d) => d.frameworkLocation, ); let msg = `Detected the framework ${frameworkName} is installed from multiple plugins at locations:\n${locations.join( - "\n" + "\n", )}\n`; msg += this.getHelpMessage(projectDir); this.$errors.fail(msg); @@ -537,33 +537,33 @@ export class PluginsService implements IPluginsService { const dependencyName = _.first(pluginsNames); const dependencyOccurrencesGroupedByVersion = _.groupBy( dependencyOccurrences, - (g) => g.version + (g) => g.version, ); const versions = _.keys(dependencyOccurrencesGroupedByVersion); if (versions.length === 1) { // all dependencies with this name have the same version this.$logger.warn( `Detected the framework ${frameworkName} is installed multiple times from the same versions of plugin (${_.first( - versions + versions, )}) at locations: ${_.map( dependencyOccurrences, - (d) => d.directory - ).join(", ")}` + (d) => d.directory, + ).join(", ")}`, ); const selectedPackage = _.minBy( dependencyOccurrences, - (d) => d.depth + (d) => d.depth, ); this.$logger.info( color.green( - `CLI will use only the native code from '${selectedPackage.directory}'.` - ) + `CLI will use only the native code from '${selectedPackage.directory}'.`, + ), ); _.each(dependencyOccurrences, (dependency) => { if (dependency !== selectedPackage) { productionDependencies.splice( productionDependencies.indexOf(dependency), - 1 + 1, ); } }); @@ -573,12 +573,12 @@ export class PluginsService implements IPluginsService { dependencyName, frameworkName, dependencyOccurrencesGroupedByVersion, - projectDir + projectDir, ); this.$errors.fail(message); } } - } + }, ); } @@ -589,13 +589,13 @@ export class PluginsService implements IPluginsService { dependencyName: string, frameworkName: string, dependencyOccurrencesGroupedByVersion: IDictionary, - projectDir: string + projectDir: string, ): string { let message = `Cannot use the same framework ${frameworkName} multiple times in your application. This framework comes from ${dependencyName} plugin, which is installed multiple times in node_modules:\n`; _.each(dependencyOccurrencesGroupedByVersion, (dependencies, version) => { message += dependencies.map( - (d) => `* Path: ${d.directory}, version: ${d.version}\n` + (d) => `* Path: ${d.directory}, version: ${d.version}\n`, ); }); @@ -621,16 +621,16 @@ This framework comes from ${dependencyName} plugin, which is installed multiple private convertToPluginData( cacheData: IDependencyData | INodeModuleData, - projectDir: string + projectDir: string, ): IPluginData { try { - const pluginData: any = {}; + const pluginData: IPluginData = {}; pluginData.name = cacheData.name; pluginData.version = cacheData.version; pluginData.fullPath = (cacheData).directory || path.dirname( - this.getPackageJsonFilePathForModule(cacheData.name, projectDir) + this.getPackageJsonFilePathForModule(cacheData.name, projectDir), ); pluginData.isPlugin = !!cacheData.nativescript; pluginData.pluginPlatformsFolderPath = (platform: string) => { @@ -640,7 +640,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple return path.join( pluginData.fullPath, "platforms", - platform.toLowerCase() + platform.toLowerCase(), ); }; const data = cacheData.nativescript; @@ -648,12 +648,13 @@ This framework comes from ${dependencyName} plugin, which is installed multiple if (pluginData.isPlugin) { pluginData.platformsData = data.platforms; pluginData.pluginVariables = data.variables; + pluginData.nativescript = data; } return pluginData; } catch (err) { this.$logger.trace( "NOTE: There appears to be a problem with this dependency:", - cacheData.name + cacheData.name, ); this.$logger.trace(err); return null; @@ -662,7 +663,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple private removeDependencyFromPackageJsonContent( dependency: string, - packageJsonContent: any + packageJsonContent: any, ): { hasModifiedPackageJson: boolean; packageJsonContent: any } { let hasModifiedPackageJson = false; @@ -705,7 +706,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple private getPackageJsonFilePathForModule( moduleName: string, - projectDir: string + projectDir: string, ): string { const pathToJsonFile = resolvePackageJSONPath(moduleName, { paths: [projectDir], @@ -720,7 +721,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple private getNodeModuleData( module: string, - projectDir: string + projectDir: string, ): INodeModuleData { // module can be modulePath or moduleName if (!this.$fs.exists(module) || path.basename(module) !== "package.json") { @@ -740,37 +741,37 @@ 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) + this.getNodeModulesPath(projectData.projectDir), ); } private async getAllInstalledModules( - projectData: IProjectData + projectData: IProjectData, ): Promise { await this.ensure(projectData); const nodeModules = this.getDependencies(projectData.projectDir); return _.map(nodeModules, (nodeModuleName) => - this.getNodeModuleData(nodeModuleName, projectData.projectDir) + this.getNodeModuleData(nodeModuleName, projectData.projectDir), ); } private async executeNpmCommand( npmCommandName: string, npmCommandArguments: string, - projectData: IProjectData + projectData: IProjectData, ): Promise { if (npmCommandName === PluginsService.INSTALL_COMMAND_NAME) { await this.$packageManager.install( npmCommandArguments, projectData.projectDir, - this.npmInstallOptions + this.npmInstallOptions, ); } else if (npmCommandName === PluginsService.UNINSTALL_COMMAND_NAME) { await this.$packageManager.uninstall( npmCommandArguments, PluginsService.NPM_CONFIG, - projectData.projectDir + projectData.projectDir, ); } @@ -785,31 +786,31 @@ This framework comes from ${dependencyName} plugin, which is installed multiple action: ( _pluginDestinationPath: string, pl: string, - _platformData: IPlatformData + _platformData: IPlatformData, ) => Promise, - projectData: IProjectData + projectData: IProjectData, ): Promise { const availablePlatforms = this.$mobileHelper.platformNames.map((p) => - p.toLowerCase() + p.toLowerCase(), ); for (const platform of availablePlatforms) { const isPlatformInstalled = this.$fs.exists( - path.join(projectData.platformsDir, platform.toLowerCase()) + path.join(projectData.platformsDir, platform.toLowerCase()), ); if (isPlatformInstalled) { const platformData = this.$platformsDataService.getPlatformData( platform.toLowerCase(), - projectData + projectData, ); const pluginDestinationPath = path.join( platformData.appDestinationDirectoryPath, this.$options.hostProjectModuleName, - "tns_modules" + "tns_modules", ); await action( pluginDestinationPath, platform.toLowerCase(), - platformData + platformData, ); } } @@ -817,11 +818,11 @@ This framework comes from ${dependencyName} plugin, which is installed multiple private getInstalledFrameworkVersion( platform: constants.PlatformTypes, - projectData: IProjectData + projectData: IProjectData, ): string { const runtimePackage = this.$projectDataService.getRuntimePackage( projectData.projectDir, - platform + platform, ); // const platformData = this.$platformsDataService.getPlatformData(platform, projectData); // const frameworkData = this.$projectDataService.getNSValue(projectData.projectDir, platformData.frameworkPackageName); @@ -831,27 +832,27 @@ This framework comes from ${dependencyName} plugin, which is installed multiple private isPluginDataValidForPlatform( pluginData: IPluginData, platform: constants.PlatformTypes, - projectData: IProjectData + projectData: IProjectData, ): boolean { let isValid = true; const installedFrameworkVersion = this.getInstalledFrameworkVersion( platform, - projectData + projectData, ); const pluginPlatformsData = pluginData.platformsData; if (pluginPlatformsData) { const versionRequiredByPlugin = (pluginPlatformsData)[platform]; if (!versionRequiredByPlugin) { this.$logger.warn( - `${pluginData.name} is not supported for ${platform}.` + `${pluginData.name} is not supported for ${platform}.`, ); isValid = false; } else if ( semver.gt(versionRequiredByPlugin, installedFrameworkVersion) ) { this.$logger.warn( - `${pluginData.name} requires at least version ${versionRequiredByPlugin} of platform ${platform}. Currently installed version is ${installedFrameworkVersion}.` + `${pluginData.name} requires at least version ${versionRequiredByPlugin} of platform ${platform}. Currently installed version is ${installedFrameworkVersion}.`, ); isValid = false; } @@ -861,7 +862,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple } private async getPluginNativeHashes( - pluginPlatformsDir: string + pluginPlatformsDir: string, ): Promise { let data: IStringDictionary = {}; if (this.$fs.exists(pluginPlatformsDir)) { @@ -874,7 +875,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple } private getAllPluginsNativeHashes( - pathToPluginsBuildFile: string + pathToPluginsBuildFile: string, ): IDictionary { if (this.$options.hostProjectPath) { // TODO: force rebuild plugins for now until we decide where to put .ns-plugins-build-data.json when embedding @@ -903,7 +904,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple opts.currentPluginNativeHashes; this.$fs.writeJson( opts.pathToPluginsBuildFile, - opts.allPluginsNativeHashes + opts.allPluginsNativeHashes, ); } } diff --git a/package-lock.json b/package-lock.json index 8b404d2676..9efb3ad391 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,22 @@ { "name": "nativescript", - "version": "9.0.0", + "version": "9.0.0-alpha.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nativescript", - "version": "9.0.0", + "version": "9.0.0-alpha.11", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@foxt/js-srp": "^0.0.3-patch2", + "@foxt/js-srp": "0.0.3-patch2", "@nativescript/doctor": "2.0.17", - "@npmcli/arborist": "^9.1.4", + "@nativescript/hook": "file:../nativescript-hook/nativescript-hook-3.0.5-alpha.tgz", + "@npmcli/arborist": "9.1.4", "@nstudio/trapezedev-project": "7.2.3", "@rigor789/resolve-package-path": "1.0.7", - "archiver": "^7.0.1", + "archiver": "7.0.1", "axios": "1.11.0", "byline": "5.0.0", "chokidar": "4.0.3", @@ -922,6 +923,88 @@ "yauzl": "3.2.0" } }, + "node_modules/@nativescript/hook": { + "version": "3.0.5-alpha", + "resolved": "file:../nativescript-hook/nativescript-hook-3.0.5-alpha.tgz", + "integrity": "sha512-tSjPTjSSy9IMv5yzuZjBTRVEgCkxdnwNIFS716/4mRXaW+m2DrZsNhbPWIkfgsFBnhW0LFAfolg7zNbb882nLw==", + "license": "Apache-2.0", + "dependencies": { + "glob": "^11.0.0", + "mkdirp": "^3.0.1" + } + }, + "node_modules/@nativescript/hook/node_modules/glob": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@nativescript/hook/node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@nativescript/hook/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@nativescript/hook/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@nativescript/hook/node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/package.json b/package.json index 4d8c78eba3..2662744887 100644 --- a/package.json +++ b/package.json @@ -56,9 +56,10 @@ "dependencies": { "@foxt/js-srp": "0.0.3-patch2", "@nativescript/doctor": "2.0.17", + "@nativescript/hook": "file:../nativescript-hook/nativescript-hook-3.0.5-alpha.tgz", "@npmcli/arborist": "9.1.4", - "@rigor789/resolve-package-path": "1.0.7", "@nstudio/trapezedev-project": "7.2.3", + "@rigor789/resolve-package-path": "1.0.7", "archiver": "7.0.1", "axios": "1.11.0", "byline": "5.0.0", From 766e7a65ac069067d1db13a6ebe4c621899a5e37 Mon Sep 17 00:00:00 2001 From: Jason Cassidy Date: Tue, 30 Sep 2025 14:45:10 +0100 Subject: [PATCH 2/4] feat: hooks command --- docs/man_pages/project/hooks/hooks.md | 35 +++++ docs/man_pages/start.md | 1 + lib/bootstrap.ts | 10 +- lib/commands/hooks/common.ts | 118 +++++++++++++++ lib/commands/hooks/hooks-lock.ts | 135 ++++++++++++++++++ .../hooks-plugin.ts => hooks/hooks.ts} | 57 ++++++-- test/plugins-service.ts | 25 ++++ 7 files changed, 368 insertions(+), 13 deletions(-) create mode 100644 docs/man_pages/project/hooks/hooks.md create mode 100644 lib/commands/hooks/common.ts create mode 100644 lib/commands/hooks/hooks-lock.ts rename lib/commands/{plugin/hooks-plugin.ts => hooks/hooks.ts} (62%) diff --git a/docs/man_pages/project/hooks/hooks.md b/docs/man_pages/project/hooks/hooks.md new file mode 100644 index 0000000000..0c952aedc7 --- /dev/null +++ b/docs/man_pages/project/hooks/hooks.md @@ -0,0 +1,35 @@ +<% if (isJekyll) { %>--- +title: ns hooks +position: 1 +---<% } %> + +# ns create + +### Description + +Manages lifecycle hooks from installed plugins. + +### Commands + +Usage | Synopsis +---------|--------- +Install | `$ ns hooks install` +List | `$ ns hooks list` +Lock | `$ ns hooks lock` +Verify | `$ ns hooks verify` + +#### Install + +Installs hooks from each installed plugin dependency. + +#### List + +Lists the plugins which have hooks and which scripts they install + +#### Lock + +Generates a `hooks-lock.json` containing the hooks that are in the current versions of the plugins. + +#### Verify + +Verifies that the hooks contained in the installed plugins match those listed in the `hooks-lock.json` file. diff --git a/docs/man_pages/start.md b/docs/man_pages/start.md index 83b9da83e8..646952bb68 100644 --- a/docs/man_pages/start.md +++ b/docs/man_pages/start.md @@ -51,6 +51,7 @@ Command | Description [plugin](lib-management/plugin.html) | Lets you manage the plugins for your project. [open](project/configuration/open.md) | Opens the native project in Xcode/Android Studio. [widget ios](project/configuration/widget.md) | Adds a new iOS widget to the project. +[hooks](project/hooks/hooks.html) | Installs lifecycle hooks from plugins. ## Publishing Commands Command | Description ---|--- diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 9676767b06..3281d84930 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -262,7 +262,15 @@ injector.requireCommand("plugin|remove", "./commands/plugin/remove-plugin"); injector.requireCommand("plugin|update", "./commands/plugin/update-plugin"); injector.requireCommand("plugin|build", "./commands/plugin/build-plugin"); injector.requireCommand("plugin|create", "./commands/plugin/create-plugin"); -injector.requireCommand("plugin|hooks", "./commands/plugin/hooks-plugin"); + +injector.requireCommand( + ["hooks|*list", "hooks|install"], + "./commands/hooks/hooks", +); +injector.requireCommand( + ["hooks|lock", "hooks|verify"], + "./commands/hooks/hooks-lock", +); injector.require("doctorService", "./services/doctor-service"); injector.require("xcprojService", "./services/xcproj-service"); diff --git a/lib/commands/hooks/common.ts b/lib/commands/hooks/common.ts new file mode 100644 index 0000000000..4532a73326 --- /dev/null +++ b/lib/commands/hooks/common.ts @@ -0,0 +1,118 @@ +import * as _ from "lodash"; +import { IProjectData } from "../../definitions/project"; +import { IPluginData } from "../../definitions/plugins"; +import { ICommandParameter } from "../../common/definitions/commands"; +import { IErrors, IFileSystem } from "../../common/declarations"; +import path = require("path"); +import * as crypto from "crypto"; + +export const LOCK_FILE_NAME = "nativescript-lock.json"; +export interface OutputHook { + type: string; + hash: string; +} + +export interface OutputPlugin { + name: string; + hooks: OutputHook[]; +} + +export class HooksVerify { + public allowedParameters: ICommandParameter[] = []; + + constructor( + protected $projectData: IProjectData, + protected $errors: IErrors, + protected $fs: IFileSystem, + protected $logger: ILogger, + ) { + this.$projectData.initializeProjectData(); + } + + protected async verifyHooksLock( + plugins: IPluginData[], + hooksLockPath: string, + ): Promise { + let lockFileContent: string; + let hooksLock: OutputPlugin[]; + + try { + lockFileContent = this.$fs.readText(hooksLockPath, "utf8"); + hooksLock = JSON.parse(lockFileContent); + } catch (err) { + this.$errors.fail( + `❌ Failed to read or parse ${LOCK_FILE_NAME} at ${hooksLockPath}`, + ); + } + + const lockMap = new Map>(); // pluginName -> hookType -> hash + + for (const plugin of hooksLock) { + const hookMap = new Map(); + for (const hook of plugin.hooks) { + hookMap.set(hook.type, hook.hash); + } + lockMap.set(plugin.name, hookMap); + } + + let isValid = true; + + for (const plugin of plugins) { + const pluginLockHooks = lockMap.get(plugin.name); + + if (!pluginLockHooks) { + this.$logger.error( + `❌ Plugin '${plugin.name}' not found in ${LOCK_FILE_NAME}`, + ); + isValid = false; + continue; + } + + for (const hook of plugin.nativescript?.hooks || []) { + const expectedHash = pluginLockHooks.get(hook.type); + + if (!expectedHash) { + this.$logger.error( + `❌ Missing hook '${hook.type}' for plugin '${plugin.name}' in ${LOCK_FILE_NAME}`, + ); + isValid = false; + continue; + } + + let fileContent: string | Buffer; + + try { + fileContent = this.$fs.readFile( + path.join(plugin.fullPath, hook.script), + ); + } catch (err) { + this.$logger.error( + `❌ Cannot read script file '${hook.script}' for hook '${hook.type}' in plugin '${plugin.name}'`, + ); + isValid = false; + continue; + } + + const actualHash = crypto + .createHash("sha256") + .update(fileContent) + .digest("hex"); + + if (actualHash !== expectedHash) { + this.$logger.error( + `❌ Hash mismatch for '${hook.script}' (${hook.type} in ${plugin.name}):`, + ); + this.$logger.error(` Expected: ${expectedHash}`); + this.$logger.error(` Actual: ${actualHash}`); + isValid = false; + } + } + } + + if (isValid) { + this.$logger.info("✅ All hooks verified successfully. No issues found."); + } else { + this.$errors.fail("❌ One or more hooks failed verification."); + } + } +} diff --git a/lib/commands/hooks/hooks-lock.ts b/lib/commands/hooks/hooks-lock.ts new file mode 100644 index 0000000000..27399ac622 --- /dev/null +++ b/lib/commands/hooks/hooks-lock.ts @@ -0,0 +1,135 @@ +import { IProjectData } from "../../definitions/project"; +import { IPluginsService, IPluginData } from "../../definitions/plugins"; +import { ICommand, ICommandParameter } from "../../common/definitions/commands"; +import { IErrors, IFileSystem } from "../../common/declarations"; +import { injector } from "../../common/yok"; +import path = require("path"); +import * as crypto from "crypto"; +import { + HooksVerify, + LOCK_FILE_NAME, + OutputHook, + OutputPlugin, +} from "./common"; + +export class HooksLockPluginCommand implements ICommand { + public allowedParameters: ICommandParameter[] = []; + + constructor( + private $pluginsService: IPluginsService, + private $projectData: IProjectData, + private $errors: IErrors, + private $fs: IFileSystem, + private $logger: ILogger, + ) { + this.$projectData.initializeProjectData(); + } + + public async execute(): Promise { + const plugins: IPluginData[] = + await this.$pluginsService.getAllInstalledPlugins(this.$projectData); + if (plugins && plugins.length > 0) { + const pluginsWithHooks: IPluginData[] = []; + for (const plugin of plugins) { + if (plugin.nativescript?.hooks?.length > 0) { + pluginsWithHooks.push(plugin); + } + } + + await this.writeHooksLockFile( + pluginsWithHooks, + this.$projectData.projectDir, + ); + } else { + this.$logger.info("No plugins with hooks found."); + } + } + + public async canExecute(args: string[]): Promise { + return true; + } + + private async writeHooksLockFile( + plugins: IPluginData[], + outputDir: string, + ): Promise { + const output: OutputPlugin[] = []; + + for (const plugin of plugins) { + const hooks: OutputHook[] = []; + + for (const hook of plugin.nativescript?.hooks || []) { + try { + const fileContent = this.$fs.readFile( + path.join(plugin.fullPath, hook.script), + ); + const hash = crypto + .createHash("sha256") + .update(fileContent) + .digest("hex"); + + hooks.push({ + type: hook.type, + hash, + }); + } catch (err) { + this.$logger.warn( + `Warning: Failed to read script '${hook.script}' for plugin '${plugin.name}'. Skipping this hook.`, + ); + continue; + } + } + + output.push({ name: plugin.name, hooks }); + } + + const filePath = path.resolve(outputDir, LOCK_FILE_NAME); + + try { + this.$fs.writeFile(filePath, JSON.stringify(output, null, 2), "utf8"); + this.$logger.info(`✅ ${LOCK_FILE_NAME} written to: ${filePath}`); + } catch (err) { + this.$errors.fail(`❌ Failed to write ${LOCK_FILE_NAME}: ${err}`); + } + } +} + +export class HooksVerifyPluginCommand extends HooksVerify implements ICommand { + public allowedParameters: ICommandParameter[] = []; + + constructor( + private $pluginsService: IPluginsService, + $projectData: IProjectData, + $errors: IErrors, + $fs: IFileSystem, + $logger: ILogger, + ) { + super($projectData, $errors, $fs, $logger); + } + + public async execute(): Promise { + const plugins: IPluginData[] = + await this.$pluginsService.getAllInstalledPlugins(this.$projectData); + if (plugins && plugins.length > 0) { + const pluginsWithHooks: IPluginData[] = []; + for (const plugin of plugins) { + if (plugin.nativescript?.hooks?.length > 0) { + pluginsWithHooks.push(plugin); + } + } + await this.verifyHooksLock( + pluginsWithHooks, + path.join(this.$projectData.projectDir, LOCK_FILE_NAME), + ); + } else { + this.$logger.info("No plugins with hooks found."); + } + } + + public async canExecute(args: string[]): Promise { + return true; + } +} + +injector.registerCommand(["hooks|lock"], HooksLockPluginCommand); +injector.registerCommand(["hooks|verify"], HooksVerifyPluginCommand); diff --git a/lib/commands/plugin/hooks-plugin.ts b/lib/commands/hooks/hooks.ts similarity index 62% rename from lib/commands/plugin/hooks-plugin.ts rename to lib/commands/hooks/hooks.ts index 51c95a7a0e..4971c648cf 100644 --- a/lib/commands/plugin/hooks-plugin.ts +++ b/lib/commands/hooks/hooks.ts @@ -1,24 +1,25 @@ -import * as _ from "lodash"; import { IProjectData } from "../../definitions/project"; import { IPluginsService, IPluginData } from "../../definitions/plugins"; import { ICommand, ICommandParameter } from "../../common/definitions/commands"; -import { IErrors, IFileSystem } from "../../common/declarations"; import { injector } from "../../common/yok"; +import { IErrors, IFileSystem } from "../../common/declarations"; import path = require("path"); import { HOOKS_DIR_NAME } from "../../constants"; import { createTable } from "../../common/helpers"; import nsHooks = require("@nativescript/hook"); -export class HooksPluginCommand implements ICommand { +import { HooksVerify, LOCK_FILE_NAME } from "./common"; + +export class HooksPluginCommand extends HooksVerify implements ICommand { public allowedParameters: ICommandParameter[] = []; constructor( private $pluginsService: IPluginsService, - private $projectData: IProjectData, - private $errors: IErrors, - private $fs: IFileSystem, - private $logger: ILogger, + $projectData: IProjectData, + $errors: IErrors, + $fs: IFileSystem, + $logger: ILogger, ) { - this.$projectData.initializeProjectData(); + super($projectData, $errors, $fs, $logger); } public async execute(args: string[]): Promise { @@ -28,7 +29,6 @@ export class HooksPluginCommand implements ICommand { await this.$pluginsService.getAllInstalledPlugins(this.$projectData); if (plugins && plugins.length > 0) { const hooksDir = path.join(this.$projectData.projectDir, HOOKS_DIR_NAME); - console.log(hooksDir); const pluginsWithHooks: IPluginData[] = []; for (const plugin of plugins) { if (plugin.nativescript?.hooks?.length > 0) { @@ -49,6 +49,17 @@ export class HooksPluginCommand implements ICommand { this.$logger.info("Hooks:"); this.$logger.info(hookDataTable.toString()); } else { + if ( + this.$fs.exists( + path.join(this.$projectData.projectDir, LOCK_FILE_NAME), + ) + ) { + await this.verifyHooksLock( + pluginsWithHooks, + path.join(this.$projectData.projectDir, LOCK_FILE_NAME), + ); + } + if (pluginsWithHooks.length === 0) { if (!this.$fs.exists(hooksDir)) { this.$fs.createDirectory(hooksDir); @@ -62,10 +73,32 @@ export class HooksPluginCommand implements ICommand { } public async canExecute(args: string[]): Promise { - if (args?.length > 50) { - this.$errors.fail(`Plugin is already installed.`); + if (args.length > 0 && args[0] !== "list") { + this.$errors.failWithHelp( + `Invalid argument ${args[0]}. Supported argument is "list".`, + ); } return true; } } -injector.registerCommand(["plugin|hooks"], HooksPluginCommand); + +export class HooksListPluginCommand extends HooksPluginCommand { + public allowedParameters: ICommandParameter[] = []; + + constructor( + $pluginsService: IPluginsService, + $projectData: IProjectData, + $errors: IErrors, + $fs: IFileSystem, + $logger: ILogger, + ) { + super($pluginsService, $projectData, $errors, $fs, $logger); + } + + public async execute(): Promise { + await super.execute(["list"]); + } +} + +injector.registerCommand(["hooks|install"], HooksPluginCommand); +injector.registerCommand(["hooks|*list"], HooksListPluginCommand); diff --git a/test/plugins-service.ts b/test/plugins-service.ts index 0ea539bfdd..85bc40641d 100644 --- a/test/plugins-service.ts +++ b/test/plugins-service.ts @@ -891,6 +891,7 @@ describe("Plugins service", () => { fullPath: pluginDir, isPlugin: true, platformsData: { android: "6.0.0", ios: "6.0.0" }, + nativescript: { platforms: { android: "6.0.0", ios: "6.0.0" } }, pluginVariables: undefined, }); }); @@ -1000,6 +1001,12 @@ describe("Plugins service", () => { android: "5.0.0", ios: "5.0.0", }, + nativescript: { + platforms: { + android: "5.0.0", + ios: "5.0.0", + }, + }, version: "6.3.2", }, { @@ -1011,6 +1018,12 @@ describe("Plugins service", () => { android: "6.2.0", ios: "6.2.0", }, + nativescript: { + platforms: { + android: "6.2.0", + ios: "6.2.0", + }, + }, version: "2.2.1", }, ], @@ -1072,6 +1085,12 @@ describe("Plugins service", () => { android: "6.0.0", ios: "6.0.0", }, + nativescript: { + platforms: { + android: "6.0.0", + ios: "6.0.0", + }, + }, version: "8.0.1", }, { @@ -1083,6 +1102,12 @@ describe("Plugins service", () => { android: "6.0.0", ios: "6.0.0", }, + nativescript: { + platforms: { + android: "6.0.0", + ios: "6.0.0", + }, + }, version: "4.0.0", }, ], From 57e2f38ed0ff3ac9e868f2661cbe12bde8e4fa47 Mon Sep 17 00:00:00 2001 From: Jason Cassidy Date: Tue, 30 Sep 2025 14:57:59 +0100 Subject: [PATCH 3/4] fix: use existing hooks --- lib/definitions/hooks.d.ts | 1 + package-lock.json | 12 ++++++------ package.json | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 lib/definitions/hooks.d.ts diff --git a/lib/definitions/hooks.d.ts b/lib/definitions/hooks.d.ts new file mode 100644 index 0000000000..c2c3932fc7 --- /dev/null +++ b/lib/definitions/hooks.d.ts @@ -0,0 +1 @@ +declare module "@nativescript/hook"; diff --git a/package-lock.json b/package-lock.json index 9efb3ad391..6c1ee9bdb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { "name": "nativescript", - "version": "9.0.0-alpha.11", + "version": "9.0.0-alpha.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nativescript", - "version": "9.0.0-alpha.11", + "version": "9.0.0-alpha.12", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@foxt/js-srp": "0.0.3-patch2", "@nativescript/doctor": "2.0.17", - "@nativescript/hook": "file:../nativescript-hook/nativescript-hook-3.0.5-alpha.tgz", + "@nativescript/hook": "3.0.4", "@npmcli/arborist": "9.1.4", "@nstudio/trapezedev-project": "7.2.3", "@rigor789/resolve-package-path": "1.0.7", @@ -924,9 +924,9 @@ } }, "node_modules/@nativescript/hook": { - "version": "3.0.5-alpha", - "resolved": "file:../nativescript-hook/nativescript-hook-3.0.5-alpha.tgz", - "integrity": "sha512-tSjPTjSSy9IMv5yzuZjBTRVEgCkxdnwNIFS716/4mRXaW+m2DrZsNhbPWIkfgsFBnhW0LFAfolg7zNbb882nLw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@nativescript/hook/-/hook-3.0.4.tgz", + "integrity": "sha512-oahiN7V0D+fgl9o8mjGRgExujTpgSBB0DAFr3eX91qdlJZV8ywJ6mnvtHZyEI2j46yPgAE8jmNIw/Z/d3aWetw==", "license": "Apache-2.0", "dependencies": { "glob": "^11.0.0", diff --git a/package.json b/package.json index 62f43e6cda..58d96496a1 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "dependencies": { "@foxt/js-srp": "0.0.3-patch2", "@nativescript/doctor": "2.0.17", - "@nativescript/hook": "file:../nativescript-hook/nativescript-hook-3.0.5-alpha.tgz", + "@nativescript/hook": "3.0.4", "@npmcli/arborist": "9.1.4", "@nstudio/trapezedev-project": "7.2.3", "@rigor789/resolve-package-path": "1.0.7", From c4967e711ad53eefad45f4bbc1fa407b2233c07f Mon Sep 17 00:00:00 2001 From: Jason Cassidy Date: Fri, 3 Oct 2025 16:36:09 +0100 Subject: [PATCH 4/4] chore:cleanup --- lib/services/plugins-service.ts | 206 ++++++++++++++++---------------- 1 file changed, 103 insertions(+), 103 deletions(-) diff --git a/lib/services/plugins-service.ts b/lib/services/plugins-service.ts index f5b850cdbf..2fadc8fe16 100644 --- a/lib/services/plugins-service.ts +++ b/lib/services/plugins-service.ts @@ -67,7 +67,7 @@ export class PluginsService implements IPluginsService { ignoreScripts: this.$options.ignoreScripts, path: this.$options.path, }, - PluginsService.NPM_CONFIG, + PluginsService.NPM_CONFIG ); } @@ -80,7 +80,7 @@ export class PluginsService implements IPluginsService { private $filesHashService: IFilesHashService, private $injector: IInjector, private $mobileHelper: Mobile.IMobileHelper, - private $nodeModulesDependenciesBuilder: INodeModulesDependenciesBuilder, + private $nodeModulesDependenciesBuilder: INodeModulesDependenciesBuilder ) {} public async add(plugin: string, projectData: IProjectData): Promise { @@ -97,26 +97,26 @@ export class PluginsService implements IPluginsService { await this.$packageManager.install( plugin, projectData.projectDir, - this.npmInstallOptions, + this.npmInstallOptions ) ).name; const pathToRealNpmPackageJson = this.getPackageJsonFilePathForModule( name, - projectData.projectDir, + projectData.projectDir ); const realNpmPackageJson = this.$fs.readJson(pathToRealNpmPackageJson); if (realNpmPackageJson.nativescript) { const pluginData = this.convertToPluginData( realNpmPackageJson, - projectData.projectDir, + projectData.projectDir ); // Validate const action = async ( pluginDestinationPath: string, platform: constants.PlatformTypes, - platformData: IPlatformData, + platformData: IPlatformData ): Promise => { this.isPluginDataValidForPlatform(pluginData, platform, projectData); }; @@ -124,61 +124,61 @@ export class PluginsService implements IPluginsService { await this.executeForAllInstalledPlatforms(action, projectData); this.$logger.info( - `Successfully installed plugin ${realNpmPackageJson.name}.`, + `Successfully installed plugin ${realNpmPackageJson.name}.` ); } else { await this.$packageManager.uninstall( realNpmPackageJson.name, { save: true }, - projectData.projectDir, + projectData.projectDir ); this.$errors.fail( - `${plugin} is not a valid NativeScript plugin. Verify that the plugin package.json file contains a nativescript key and try again.`, + `${plugin} is not a valid NativeScript plugin. Verify that the plugin package.json file contains a nativescript key and try again.` ); } } public async remove( pluginName: string, - projectData: IProjectData, + projectData: IProjectData ): Promise { const removePluginNativeCodeAction = async ( modulesDestinationPath: string, platform: string, - platformData: IPlatformData, + platformData: IPlatformData ): Promise => { const pluginData = this.convertToPluginData( this.getNodeModuleData(pluginName, projectData.projectDir), - projectData.projectDir, + projectData.projectDir ); await platformData.platformProjectService.removePluginNativeCode( pluginData, - projectData, + projectData ); }; await this.executeForAllInstalledPlatforms( removePluginNativeCodeAction, - projectData, + projectData ); await this.executeNpmCommand( PluginsService.UNINSTALL_COMMAND_NAME, pluginName, - projectData, + projectData ); let showMessage = true; const action = async ( modulesDestinationPath: string, platform: string, - platformData: IPlatformData, + platformData: IPlatformData ): Promise => { shelljs.rm("-rf", path.join(modulesDestinationPath, pluginName)); this.$logger.info( - `Successfully removed plugin ${pluginName} for ${platform}.`, + `Successfully removed plugin ${pluginName} for ${platform}.` ); showMessage = false; }; @@ -194,7 +194,7 @@ export class PluginsService implements IPluginsService { plugin: string, version: string, isDev: boolean, - projectDir: string, + projectDir: string ) { const packageJsonPath = this.getPackageJsonFilePath(projectDir); let packageJsonContent = this.$fs.readJson(packageJsonPath); @@ -206,7 +206,7 @@ export class PluginsService implements IPluginsService { ) { const result = this.removeDependencyFromPackageJsonContent( plugin, - packageJsonContent, + packageJsonContent ); packageJsonContent = result.packageJsonContent; } @@ -222,7 +222,7 @@ export class PluginsService implements IPluginsService { const packageJsonContent = this.$fs.readJson(packageJsonPath); const result = this.removeDependencyFromPackageJsonContent( plugin, - packageJsonContent, + packageJsonContent ); if (result.hasModifiedPackageJson) { @@ -237,7 +237,7 @@ export class PluginsService implements IPluginsService { }: IPreparePluginNativeCodeData): Promise { const platformData = this.$platformsDataService.getPlatformData( platform, - projectData, + projectData ); const pluginPlatformsFolderPath = @@ -245,31 +245,31 @@ export class PluginsService implements IPluginsService { if (this.$fs.exists(pluginPlatformsFolderPath)) { const pathToPluginsBuildFile = path.join( platformData.projectRoot, - constants.PLUGINS_BUILD_DATA_FILENAME, + constants.PLUGINS_BUILD_DATA_FILENAME ); const allPluginsNativeHashes = this.getAllPluginsNativeHashes( - pathToPluginsBuildFile, + pathToPluginsBuildFile ); const oldPluginNativeHashes = allPluginsNativeHashes[pluginData.name]; const currentPluginNativeHashes = await this.getPluginNativeHashes( - pluginPlatformsFolderPath, + pluginPlatformsFolderPath ); if ( !oldPluginNativeHashes || this.$filesHashService.hasChangesInShasums( oldPluginNativeHashes, - currentPluginNativeHashes, + currentPluginNativeHashes ) ) { await platformData.platformProjectService.preparePluginNativeCode( pluginData, - projectData, + projectData ); const updatedPluginNativeHashes = await this.getPluginNativeHashes( - pluginPlatformsFolderPath, + pluginPlatformsFolderPath ); this.setPluginNativeHashes({ @@ -283,13 +283,13 @@ export class PluginsService implements IPluginsService { } public async ensureAllDependenciesAreInstalled( - projectData: IProjectData, + projectData: IProjectData ): Promise { const packageJsonContent = this.$fs.readJson( - this.getPackageJsonFilePath(projectData.projectDir), + this.getPackageJsonFilePath(projectData.projectDir) ); const allDependencies = _.keys(packageJsonContent.dependencies).concat( - _.keys(packageJsonContent.devDependencies), + _.keys(packageJsonContent.devDependencies) ); const notInstalledDependencies = allDependencies @@ -316,7 +316,7 @@ export class PluginsService implements IPluginsService { "Npm install will be called from CLI. Force option is: ", this.$options.force, " Not installed dependencies are: ", - notInstalledDependencies, + notInstalledDependencies ); await this.$packageManager.install( projectData.projectDir, @@ -326,34 +326,34 @@ export class PluginsService implements IPluginsService { frameworkPath: this.$options.frameworkPath, ignoreScripts: this.$options.ignoreScripts, path: this.$options.path, - }, + } ); } } public async getAllInstalledPlugins( - projectData: IProjectData, + projectData: IProjectData ): Promise { const nodeModules = (await this.getAllInstalledModules(projectData)).map( (nodeModuleData) => - this.convertToPluginData(nodeModuleData, projectData.projectDir), + this.convertToPluginData(nodeModuleData, projectData.projectDir) ); return _.filter( nodeModules, - (nodeModuleData) => nodeModuleData && nodeModuleData.isPlugin, + (nodeModuleData) => nodeModuleData && nodeModuleData.isPlugin ); } public getAllProductionPlugins( projectData: IProjectData, platform: string, - dependencies?: IDependencyData[], + dependencies?: IDependencyData[] ): IPluginData[] { dependencies = dependencies || this.$nodeModulesDependenciesBuilder.getProductionDependencies( projectData.projectDir, - projectData.ignoredDependencies, + projectData.ignoredDependencies ); if (_.isEmpty(dependencies)) { @@ -361,12 +361,12 @@ export class PluginsService implements IPluginsService { } let productionPlugins: IDependencyData[] = dependencies.filter( - (d) => !!d.nativescript, + (d) => !!d.nativescript ); productionPlugins = this.ensureValidProductionPlugins( productionPlugins, projectData.projectDir, - platform, + platform ); return productionPlugins .map((plugin) => this.convertToPluginData(plugin, projectData.projectDir)) @@ -379,17 +379,17 @@ export class PluginsService implements IPluginsService { } public getDependenciesFromPackageJson( - projectDir: string, + projectDir: string ): IPackageJsonDepedenciesResult { const packageJson = this.$fs.readJson( - this.getPackageJsonFilePath(projectDir), + this.getPackageJsonFilePath(projectDir) ); const dependencies: IBasePluginData[] = this.getBasicPluginInformation( - packageJson.dependencies, + packageJson.dependencies ); const devDependencies: IBasePluginData[] = this.getBasicPluginInformation( - packageJson.devDependencies, + packageJson.devDependencies ); return { @@ -407,27 +407,27 @@ export class PluginsService implements IPluginsService { ( productionDependencies: IDependencyData[], projectDir: string, - platform: string, + platform: string ) => IDependencyData[] >( this._ensureValidProductionPlugins, ( productionDependencies: IDependencyData[], projectDir: string, - platform: string, + platform: string ) => { let key = _.sortBy(productionDependencies, (p) => p.directory) .map((d) => JSON.stringify(d, null, 2)) .join("\n"); key += projectDir + platform; return key; - }, + } ); private _ensureValidProductionPlugins( productionDependencies: IDependencyData[], projectDir: string, - platform: string, + platform: string ): IDependencyData[] { let clonedProductionDependencies = _.cloneDeep(productionDependencies); platform = platform.toLowerCase(); @@ -438,7 +438,7 @@ export class PluginsService implements IPluginsService { clonedProductionDependencies = this.ensureValidProductionPluginsForIOS( clonedProductionDependencies, projectDir, - platform, + platform ); } @@ -446,11 +446,11 @@ export class PluginsService implements IPluginsService { } private ensureValidProductionPluginsForAndroid( - productionDependencies: IDependencyData[], + productionDependencies: IDependencyData[] ): void { const dependenciesGroupedByName = _.groupBy( productionDependencies, - (p) => p.name, + (p) => p.name ); _.each( dependenciesGroupedByName, @@ -459,36 +459,36 @@ export class PluginsService implements IPluginsService { // the dependency exists multiple times in node_modules const dependencyOccurrencesGroupedByVersion = _.groupBy( dependencyOccurrences, - (g) => g.version, + (g) => g.version ); const versions = _.keys(dependencyOccurrencesGroupedByVersion); if (versions.length === 1) { // all dependencies with this name have the same version this.$logger.trace( `Detected same versions (${_.first( - versions, + versions )}) of the ${dependencyName} installed at locations: ${_.map( dependencyOccurrences, - (d) => d.directory, - ).join(", ")}`, + (d) => d.directory + ).join(", ")}` ); } else { this.$logger.trace( `Detected different versions of the ${dependencyName} installed at locations: ${_.map( dependencyOccurrences, - (d) => d.directory, - ).join(", ")}\nThis can cause build failures.`, + (d) => d.directory + ).join(", ")}\nThis can cause build failures.` ); } } - }, + } ); } private ensureValidProductionPluginsForIOS( productionDependencies: IDependencyData[], projectDir: string, - platform: string, + platform: string ): IDependencyData[] { const dependenciesWithFrameworks: any[] = []; _.each(productionDependencies, (d) => { @@ -510,7 +510,7 @@ export class PluginsService implements IPluginsService { if (dependenciesWithFrameworks.length > 0) { const dependenciesGroupedByFrameworkName = _.groupBy( dependenciesWithFrameworks, - (d) => d.frameworkName, + (d) => d.frameworkName ); _.each( dependenciesGroupedByFrameworkName, @@ -519,16 +519,16 @@ export class PluginsService implements IPluginsService { // A framework exists multiple times in node_modules const groupedByName = _.groupBy( dependencyOccurrences, - (d) => d.name, + (d) => d.name ); const pluginsNames = _.keys(groupedByName); if (pluginsNames.length > 1) { // fail - the same framework is installed by different dependencies. const locations = dependencyOccurrences.map( - (d) => d.frameworkLocation, + (d) => d.frameworkLocation ); let msg = `Detected the framework ${frameworkName} is installed from multiple plugins at locations:\n${locations.join( - "\n", + "\n" )}\n`; msg += this.getHelpMessage(projectDir); this.$errors.fail(msg); @@ -537,33 +537,33 @@ export class PluginsService implements IPluginsService { const dependencyName = _.first(pluginsNames); const dependencyOccurrencesGroupedByVersion = _.groupBy( dependencyOccurrences, - (g) => g.version, + (g) => g.version ); const versions = _.keys(dependencyOccurrencesGroupedByVersion); if (versions.length === 1) { // all dependencies with this name have the same version this.$logger.warn( `Detected the framework ${frameworkName} is installed multiple times from the same versions of plugin (${_.first( - versions, + versions )}) at locations: ${_.map( dependencyOccurrences, - (d) => d.directory, - ).join(", ")}`, + (d) => d.directory + ).join(", ")}` ); const selectedPackage = _.minBy( dependencyOccurrences, - (d) => d.depth, + (d) => d.depth ); this.$logger.info( color.green( - `CLI will use only the native code from '${selectedPackage.directory}'.`, - ), + `CLI will use only the native code from '${selectedPackage.directory}'.` + ) ); _.each(dependencyOccurrences, (dependency) => { if (dependency !== selectedPackage) { productionDependencies.splice( productionDependencies.indexOf(dependency), - 1, + 1 ); } }); @@ -573,12 +573,12 @@ export class PluginsService implements IPluginsService { dependencyName, frameworkName, dependencyOccurrencesGroupedByVersion, - projectDir, + projectDir ); this.$errors.fail(message); } } - }, + } ); } @@ -589,13 +589,13 @@ export class PluginsService implements IPluginsService { dependencyName: string, frameworkName: string, dependencyOccurrencesGroupedByVersion: IDictionary, - projectDir: string, + projectDir: string ): string { let message = `Cannot use the same framework ${frameworkName} multiple times in your application. This framework comes from ${dependencyName} plugin, which is installed multiple times in node_modules:\n`; _.each(dependencyOccurrencesGroupedByVersion, (dependencies, version) => { message += dependencies.map( - (d) => `* Path: ${d.directory}, version: ${d.version}\n`, + (d) => `* Path: ${d.directory}, version: ${d.version}\n` ); }); @@ -621,7 +621,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple private convertToPluginData( cacheData: IDependencyData | INodeModuleData, - projectDir: string, + projectDir: string ): IPluginData { try { const pluginData: IPluginData = {}; @@ -630,7 +630,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple pluginData.fullPath = (cacheData).directory || path.dirname( - this.getPackageJsonFilePathForModule(cacheData.name, projectDir), + this.getPackageJsonFilePathForModule(cacheData.name, projectDir) ); pluginData.isPlugin = !!cacheData.nativescript; pluginData.pluginPlatformsFolderPath = (platform: string) => { @@ -640,7 +640,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple return path.join( pluginData.fullPath, "platforms", - platform.toLowerCase(), + platform.toLowerCase() ); }; const data = cacheData.nativescript; @@ -654,7 +654,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple } catch (err) { this.$logger.trace( "NOTE: There appears to be a problem with this dependency:", - cacheData.name, + cacheData.name ); this.$logger.trace(err); return null; @@ -663,7 +663,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple private removeDependencyFromPackageJsonContent( dependency: string, - packageJsonContent: any, + packageJsonContent: any ): { hasModifiedPackageJson: boolean; packageJsonContent: any } { let hasModifiedPackageJson = false; @@ -706,7 +706,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple private getPackageJsonFilePathForModule( moduleName: string, - projectDir: string, + projectDir: string ): string { const pathToJsonFile = resolvePackageJSONPath(moduleName, { paths: [projectDir], @@ -721,7 +721,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple private getNodeModuleData( module: string, - projectDir: string, + projectDir: string ): INodeModuleData { // module can be modulePath or moduleName if (!this.$fs.exists(module) || path.basename(module) !== "package.json") { @@ -741,37 +741,37 @@ 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), + this.getNodeModulesPath(projectData.projectDir) ); } private async getAllInstalledModules( - projectData: IProjectData, + projectData: IProjectData ): Promise { await this.ensure(projectData); const nodeModules = this.getDependencies(projectData.projectDir); return _.map(nodeModules, (nodeModuleName) => - this.getNodeModuleData(nodeModuleName, projectData.projectDir), + this.getNodeModuleData(nodeModuleName, projectData.projectDir) ); } private async executeNpmCommand( npmCommandName: string, npmCommandArguments: string, - projectData: IProjectData, + projectData: IProjectData ): Promise { if (npmCommandName === PluginsService.INSTALL_COMMAND_NAME) { await this.$packageManager.install( npmCommandArguments, projectData.projectDir, - this.npmInstallOptions, + this.npmInstallOptions ); } else if (npmCommandName === PluginsService.UNINSTALL_COMMAND_NAME) { await this.$packageManager.uninstall( npmCommandArguments, PluginsService.NPM_CONFIG, - projectData.projectDir, + projectData.projectDir ); } @@ -786,31 +786,31 @@ This framework comes from ${dependencyName} plugin, which is installed multiple action: ( _pluginDestinationPath: string, pl: string, - _platformData: IPlatformData, + _platformData: IPlatformData ) => Promise, - projectData: IProjectData, + projectData: IProjectData ): Promise { const availablePlatforms = this.$mobileHelper.platformNames.map((p) => - p.toLowerCase(), + p.toLowerCase() ); for (const platform of availablePlatforms) { const isPlatformInstalled = this.$fs.exists( - path.join(projectData.platformsDir, platform.toLowerCase()), + path.join(projectData.platformsDir, platform.toLowerCase()) ); if (isPlatformInstalled) { const platformData = this.$platformsDataService.getPlatformData( platform.toLowerCase(), - projectData, + projectData ); const pluginDestinationPath = path.join( platformData.appDestinationDirectoryPath, this.$options.hostProjectModuleName, - "tns_modules", + "tns_modules" ); await action( pluginDestinationPath, platform.toLowerCase(), - platformData, + platformData ); } } @@ -818,11 +818,11 @@ This framework comes from ${dependencyName} plugin, which is installed multiple private getInstalledFrameworkVersion( platform: constants.PlatformTypes, - projectData: IProjectData, + projectData: IProjectData ): string { const runtimePackage = this.$projectDataService.getRuntimePackage( projectData.projectDir, - platform, + platform ); // const platformData = this.$platformsDataService.getPlatformData(platform, projectData); // const frameworkData = this.$projectDataService.getNSValue(projectData.projectDir, platformData.frameworkPackageName); @@ -832,27 +832,27 @@ This framework comes from ${dependencyName} plugin, which is installed multiple private isPluginDataValidForPlatform( pluginData: IPluginData, platform: constants.PlatformTypes, - projectData: IProjectData, + projectData: IProjectData ): boolean { let isValid = true; const installedFrameworkVersion = this.getInstalledFrameworkVersion( platform, - projectData, + projectData ); const pluginPlatformsData = pluginData.platformsData; if (pluginPlatformsData) { const versionRequiredByPlugin = (pluginPlatformsData)[platform]; if (!versionRequiredByPlugin) { this.$logger.warn( - `${pluginData.name} is not supported for ${platform}.`, + `${pluginData.name} is not supported for ${platform}.` ); isValid = false; } else if ( semver.gt(versionRequiredByPlugin, installedFrameworkVersion) ) { this.$logger.warn( - `${pluginData.name} requires at least version ${versionRequiredByPlugin} of platform ${platform}. Currently installed version is ${installedFrameworkVersion}.`, + `${pluginData.name} requires at least version ${versionRequiredByPlugin} of platform ${platform}. Currently installed version is ${installedFrameworkVersion}.` ); isValid = false; } @@ -862,7 +862,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple } private async getPluginNativeHashes( - pluginPlatformsDir: string, + pluginPlatformsDir: string ): Promise { let data: IStringDictionary = {}; if (this.$fs.exists(pluginPlatformsDir)) { @@ -875,7 +875,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple } private getAllPluginsNativeHashes( - pathToPluginsBuildFile: string, + pathToPluginsBuildFile: string ): IDictionary { if (this.$options.hostProjectPath) { // TODO: force rebuild plugins for now until we decide where to put .ns-plugins-build-data.json when embedding @@ -904,7 +904,7 @@ This framework comes from ${dependencyName} plugin, which is installed multiple opts.currentPluginNativeHashes; this.$fs.writeJson( opts.pathToPluginsBuildFile, - opts.allPluginsNativeHashes, + opts.allPluginsNativeHashes ); } }