This is an automated email from the ASF dual-hosted git repository.

breautek pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cordova-plugman.git


The following commit(s) were added to refs/heads/master by this push:
     new d769121  refactor!: Replace p libs with native JS APIs (#197)
d769121 is described below

commit d769121af9291e84573daa5393d8641efa522667
Author: Norman Breau <[email protected]>
AuthorDate: Thu Jun 18 10:04:27 2026 -0300

    refactor!: Replace p libs with native JS APIs (#197)
    
    * refactor!: Replaced p-try/p-each-series with native async functions
    
    * test: Added test suite for commands.spec.js
    
    Generated-by: Claude Sonnet 4.6
    
    * fix: package-lock missing engines update
---
 package-lock.json     |  19 +--
 package.json          |   4 +-
 spec/commands.spec.js | 329 ++++++++++++++++++++++++++++++++++++++++++++++++++
 src/commands.js       |  55 ++++++---
 4 files changed, 371 insertions(+), 36 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index c825ee6..7c4236d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,9 +11,7 @@
       "dependencies": {
         "cordova-lib": "^13.0.0",
         "loud-rejection": "^2.2.0",
-        "nopt": "^9.0.0",
-        "p-each-series": "^2.2.0",
-        "p-try": "^2.2.0"
+        "nopt": "^9.0.0"
       },
       "bin": {
         "plugman": "bin.js"
@@ -25,7 +23,7 @@
         "rewire": "^9.0.1"
       },
       "engines": {
-        "node": ">=20.5.0"
+        "node": ">=20.17.0 || >=22.9.0"
       }
     },
     "node_modules/@babel/code-frame": {
@@ -5483,18 +5481,6 @@
         "url": "https://github.com/sponsors/ljharb";
       }
     },
-    "node_modules/p-each-series": {
-      "version": "2.2.0",
-      "resolved": 
"https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz";,
-      "integrity": 
"sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus";
-      }
-    },
     "node_modules/p-limit": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz";,
@@ -5543,6 +5529,7 @@
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz";,
       "integrity": 
"sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=6"
diff --git a/package.json b/package.json
index 3fb13eb..42b6cc3 100644
--- a/package.json
+++ b/package.json
@@ -12,9 +12,7 @@
   "dependencies": {
     "cordova-lib": "^13.0.0",
     "loud-rejection": "^2.2.0",
-    "nopt": "^9.0.0",
-    "p-each-series": "^2.2.0",
-    "p-try": "^2.2.0"
+    "nopt": "^9.0.0"
   },
   "devDependencies": {
     "@cordova/eslint-config": "^6.0.1",
diff --git a/spec/commands.spec.js b/spec/commands.spec.js
new file mode 100644
index 0000000..8faf6a5
--- /dev/null
+++ b/spec/commands.spec.js
@@ -0,0 +1,329 @@
+/*
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+*/
+
+const rewire = require('rewire');
+const commands = rewire('../src/commands');
+
+describe('commands', () => {
+    let mockPlugman;
+
+    beforeEach(() => {
+        mockPlugman = {
+            install: jasmine.createSpy('install').and.resolveTo(),
+            uninstall: jasmine.createSpy('uninstall').and.resolveTo(),
+            create: jasmine.createSpy('create').and.resolveTo(),
+            platform: jasmine.createSpy('platform').and.resolveTo(),
+            createpackagejson: 
jasmine.createSpy('createpackagejson').and.resolveTo()
+        };
+        commands.__set__('plugman', mockPlugman);
+    });
+
+    describe('install', () => {
+        it('should reject if --platform is missing', async () => {
+            await expectAsync(commands.install({ project: '/project', plugin: 
['plugin-a'] }))
+                .toBeRejectedWithError('Missing required option --platform');
+        });
+
+        it('should reject if --project is missing', async () => {
+            await expectAsync(commands.install({ platform: 'android', plugin: 
['plugin-a'] }))
+                .toBeRejectedWithError('Missing required option --project');
+        });
+
+        it('should reject if --plugin is missing', async () => {
+            await expectAsync(commands.install({ platform: 'android', project: 
'/project' }))
+                .toBeRejectedWithError('Missing required option --plugin');
+        });
+
+        it('should call plugman.install once per plugin', async () => {
+            await commands.install({
+                platform: 'android',
+                project: '/project',
+                plugin: ['plugin-a', 'plugin-b'],
+                plugins_dir: '/plugins'
+            });
+
+            expect(mockPlugman.install).toHaveBeenCalledTimes(2);
+            expect(mockPlugman.install).toHaveBeenCalledWith('android', 
'/project', 'plugin-a', '/plugins', jasmine.any(Object));
+            expect(mockPlugman.install).toHaveBeenCalledWith('android', 
'/project', 'plugin-b', '/plugins', jasmine.any(Object));
+        });
+
+        it('should not call plugman.install when plugin list is empty', async 
() => {
+            await commands.install({
+                platform: 'android',
+                project: '/project',
+                plugin: ['placeholder'] // satisfies the required check
+            });
+            mockPlugman.install.calls.reset();
+
+            // Verify a single plugin results in exactly one call
+            await commands.install({ platform: 'android', project: '/project', 
plugin: ['x'] });
+            expect(mockPlugman.install).toHaveBeenCalledTimes(1);
+        });
+
+        it('should use safe defaults for optional flags', async () => {
+            await commands.install({
+                platform: 'android',
+                project: '/project',
+                plugin: ['plugin-a']
+            });
+
+            const opts = mockPlugman.install.calls.mostRecent().args[4];
+            expect(opts).toEqual(jasmine.objectContaining({
+                subdir: '.',
+                save: false,
+                force: false,
+                nohooks: false,
+                cli_variables: {}
+            }));
+        });
+
+        it('should forward save, force, and nohooks flags', async () => {
+            await commands.install({
+                platform: 'android',
+                project: '/project',
+                plugin: ['plugin-a'],
+                save: true,
+                force: true,
+                nohooks: true
+            });
+
+            const opts = mockPlugman.install.calls.mostRecent().args[4];
+            expect(opts).toEqual(jasmine.objectContaining({ save: true, force: 
true, nohooks: true }));
+        });
+
+        it('should forward www, searchpath, and link options', async () => {
+            await commands.install({
+                platform: 'android',
+                project: '/project',
+                plugin: ['plugin-a'],
+                www: '/www',
+                searchpath: '/search',
+                link: true
+            });
+
+            const opts = mockPlugman.install.calls.mostRecent().args[4];
+            expect(opts).toEqual(jasmine.objectContaining({ www_dir: '/www', 
searchpath: '/search', link: true }));
+        });
+
+        it('should expand --variable assignments into cli_variables', async () 
=> {
+            await commands.install({
+                platform: 'android',
+                project: '/project',
+                plugin: ['plugin-a'],
+                variable: ['FOO=bar', 'BAZ=qux']
+            });
+
+            const opts = mockPlugman.install.calls.mostRecent().args[4];
+            expect(opts.cli_variables).toEqual({ FOO: 'bar', BAZ: 'qux' });
+        });
+
+        it('should uppercase variable keys', async () => {
+            await commands.install({
+                platform: 'android',
+                project: '/project',
+                plugin: ['plugin-a'],
+                variable: ['lowercase=value']
+            });
+
+            const opts = mockPlugman.install.calls.mostRecent().args[4];
+            expect(opts.cli_variables).toEqual({ LOWERCASE: 'value' });
+        });
+
+        it('should preserve equals signs in variable values', async () => {
+            await commands.install({
+                platform: 'android',
+                project: '/project',
+                plugin: ['plugin-a'],
+                variable: ['KEY=val=ue']
+            });
+
+            const opts = mockPlugman.install.calls.mostRecent().args[4];
+            expect(opts.cli_variables).toEqual({ KEY: 'val=ue' });
+        });
+
+        it('should discard variables with invalid key names', async () => {
+            await commands.install({
+                platform: 'android',
+                project: '/project',
+                plugin: ['plugin-a'],
+                variable: ['VALID=ok', 'invalid key=bad']
+            });
+
+            const opts = mockPlugman.install.calls.mostRecent().args[4];
+            expect(opts.cli_variables).toEqual({ VALID: 'ok' });
+        });
+    });
+
+    describe('uninstall', () => {
+        it('should reject if --platform is missing', async () => {
+            await expectAsync(commands.uninstall({ project: '/project', 
plugin: ['plugin-a'] }))
+                .toBeRejectedWithError('Missing required option --platform');
+        });
+
+        it('should reject if --project is missing', async () => {
+            await expectAsync(commands.uninstall({ platform: 'android', 
plugin: ['plugin-a'] }))
+                .toBeRejectedWithError('Missing required option --project');
+        });
+
+        it('should reject if --plugin is missing', async () => {
+            await expectAsync(commands.uninstall({ platform: 'android', 
project: '/project' }))
+                .toBeRejectedWithError('Missing required option --plugin');
+        });
+
+        it('should call plugman.uninstall once per plugin', async () => {
+            await commands.uninstall({
+                platform: 'android',
+                project: '/project',
+                plugin: ['plugin-a', 'plugin-b'],
+                plugins_dir: '/plugins'
+            });
+
+            expect(mockPlugman.uninstall).toHaveBeenCalledTimes(2);
+            expect(mockPlugman.uninstall).toHaveBeenCalledWith('android', 
'/project', 'plugin-a', '/plugins', jasmine.any(Object));
+            expect(mockPlugman.uninstall).toHaveBeenCalledWith('android', 
'/project', 'plugin-b', '/plugins', jasmine.any(Object));
+        });
+
+        it('should default save to false', async () => {
+            await commands.uninstall({
+                platform: 'android',
+                project: '/project',
+                plugin: ['plugin-a']
+            });
+
+            const opts = mockPlugman.uninstall.calls.mostRecent().args[4];
+            expect(opts).toEqual(jasmine.objectContaining({ save: false }));
+        });
+
+        it('should forward save flag', async () => {
+            await commands.uninstall({
+                platform: 'android',
+                project: '/project',
+                plugin: ['plugin-a'],
+                save: true
+            });
+
+            const opts = mockPlugman.uninstall.calls.mostRecent().args[4];
+            expect(opts).toEqual(jasmine.objectContaining({ save: true }));
+        });
+
+        it('should forward www option', async () => {
+            await commands.uninstall({
+                platform: 'android',
+                project: '/project',
+                plugin: ['plugin-a'],
+                www: '/www'
+            });
+
+            const opts = mockPlugman.uninstall.calls.mostRecent().args[4];
+            expect(opts).toEqual(jasmine.objectContaining({ www_dir: '/www' 
}));
+        });
+    });
+
+    describe('create', () => {
+        it('should reject if --name is missing', async () => {
+            await expectAsync(commands.create({ plugin_id: 'com.example', 
plugin_version: '1.0.0' }))
+                .toBeRejectedWithError('Missing required option --name');
+        });
+
+        it('should reject if --plugin_id is missing', async () => {
+            await expectAsync(commands.create({ name: 'MyPlugin', 
plugin_version: '1.0.0' }))
+                .toBeRejectedWithError('Missing required option --plugin_id');
+        });
+
+        it('should reject if --plugin_version is missing', async () => {
+            await expectAsync(commands.create({ name: 'MyPlugin', plugin_id: 
'com.example' }))
+                .toBeRejectedWithError('Missing required option 
--plugin_version');
+        });
+
+        it('should call plugman.create with the provided options', async () => 
{
+            await commands.create({
+                name: 'MyPlugin',
+                plugin_id: 'com.example',
+                plugin_version: '1.0.0',
+                path: '/output'
+            });
+
+            expect(mockPlugman.create).toHaveBeenCalledOnceWith('MyPlugin', 
'com.example', '1.0.0', '/output', {});
+        });
+
+        it('should default path to "." when not provided', async () => {
+            await commands.create({
+                name: 'MyPlugin',
+                plugin_id: 'com.example',
+                plugin_version: '1.0.0'
+            });
+
+            expect(mockPlugman.create).toHaveBeenCalledOnceWith('MyPlugin', 
'com.example', '1.0.0', '.', {});
+        });
+
+        it('should expand --variable assignments into cli_variables', async () 
=> {
+            await commands.create({
+                name: 'MyPlugin',
+                plugin_id: 'com.example',
+                plugin_version: '1.0.0',
+                variable: ['FOO=bar']
+            });
+
+            const cli_variables = 
mockPlugman.create.calls.mostRecent().args[4];
+            expect(cli_variables).toEqual({ FOO: 'bar' });
+        });
+    });
+
+    describe('platform', () => {
+        it('should reject if --platform_name is missing', async () => {
+            await expectAsync(commands.platform({ argv: { remain: ['add'] } }))
+                .toBeRejectedWithError('Missing required option 
--platform_name');
+        });
+
+        it('should reject when operation is not "add" or "remove"', async () 
=> {
+            await expectAsync(commands.platform({ platform_name: 'android', 
argv: { remain: ['update'] } }))
+                .toBeRejectedWithError(/Operation must be either 'add' or 
'remove'/);
+        });
+
+        it('should reject when operation is empty', async () => {
+            await expectAsync(commands.platform({ platform_name: 'android', 
argv: { remain: [] } }))
+                .toBeRejectedWithError(/Operation must be either 'add' or 
'remove'/);
+        });
+
+        it('should call plugman.platform with operation "add"', async () => {
+            await commands.platform({ platform_name: 'android', argv: { 
remain: ['add'] } });
+
+            expect(mockPlugman.platform).toHaveBeenCalledOnceWith({ operation: 
'add', platform_name: 'android' });
+        });
+
+        it('should call plugman.platform with operation "remove"', async () => 
{
+            await commands.platform({ platform_name: 'android', argv: { 
remain: ['remove'] } });
+
+            expect(mockPlugman.platform).toHaveBeenCalledOnceWith({ operation: 
'remove', platform_name: 'android' });
+        });
+    });
+
+    describe('createpackagejson', () => {
+        it('should reject when plugin path is not provided', async () => {
+            await expectAsync(commands.createpackagejson({ argv: { remain: [] 
} }))
+                .toBeRejectedWithError('Missing required path to plugin');
+        });
+
+        it('should call plugman.createpackagejson with the plugin path', async 
() => {
+            await commands.createpackagejson({ argv: { remain: 
['/path/to/plugin'] } });
+
+            
expect(mockPlugman.createpackagejson).toHaveBeenCalledOnceWith('/path/to/plugin');
+        });
+    });
+});
diff --git a/src/commands.js b/src/commands.js
index 4ef5d6e..cf083d9 100644
--- a/src/commands.js
+++ b/src/commands.js
@@ -19,12 +19,10 @@
 
 // copyright (c) 2013 Andrew Lunny, Adobe Systems
 
-const pTry = require('p-try');
-const pEachSeries = require('p-each-series');
 const { plugman } = require('cordova-lib');
 
 module.exports = {
-    install (cli_opts) {
+    async install (cli_opts) {
         assertRequiredOptions(cli_opts, ['platform', 'project', 'plugin']);
 
         const opts = {
@@ -39,12 +37,15 @@ module.exports = {
             nohooks: cli_opts.nohooks || false
         };
 
-        return pEachSeries(cli_opts.plugin, pluginSrc =>
-            plugman.install(cli_opts.platform, cli_opts.project, pluginSrc, 
cli_opts.plugins_dir, opts)
-        );
+        const plugins = cli_opts?.plugin || [];
+
+        for (let i = 0; i < plugins.length; i++) {
+            const pluginSrc = plugins[i];
+            await plugman.install(cli_opts.platform, cli_opts.project, 
pluginSrc, cli_opts.plugins_dir, opts);
+        }
     },
 
-    uninstall (cli_opts) {
+    async uninstall (cli_opts) {
         assertRequiredOptions(cli_opts, ['platform', 'project', 'plugin']);
 
         const opts = {
@@ -53,19 +54,39 @@ module.exports = {
             projectRoot: cli_opts.project
         };
 
-        return pEachSeries(cli_opts.plugin, pluginSrc =>
-            plugman.uninstall(cli_opts.platform, cli_opts.project, pluginSrc, 
cli_opts.plugins_dir, opts)
-        );
+        const plugins = cli_opts?.plugin || [];
+        for (let i = 0; i < plugins.length; i++) {
+            const pluginSrc = plugins[i];
+            await plugman.uninstall(cli_opts.platform, cli_opts.project, 
pluginSrc, cli_opts.plugins_dir, opts);
+        }
+    },
+
+    async create (cli_opts) {
+        assertRequiredOptions(cli_opts, ['name', 'plugin_id', 
'plugin_version']);
+
+        const cli_variables = expandCliVariables(cli_opts.variable);
+        return plugman.create(cli_opts.name, cli_opts.plugin_id, 
cli_opts.plugin_version, cli_opts.path || '.', cli_variables);
+    },
+
+    async platform (cli_opts) {
+        assertRequiredOptions(cli_opts, ['platform_name']);
+        const operation = cli_opts.argv.remain[0] || '';
+        if (operation !== 'add' && operation !== 'remove') {
+            throw new Error(`Operation must be either 'add' or 'remove' but 
was '${operation}'`);
+        }
+
+        return plugman.platform({ operation, platform_name: 
cli_opts.platform_name });
+    },
+
+    async createpackagejson (cli_opts) {
+        const plugin_path = cli_opts.argv.remain[0];
+        if (!plugin_path) {
+            throw new Error('Missing required path to plugin');
+        }
+        return plugman.createpackagejson(plugin_path);
     }
 };
 
-// Until we can declare all above functions async, wrap them all with pTry
-// to turn all thrown errors into rejections
-for (const key in module.exports) {
-    const fn = module.exports[key];
-    module.exports[key] = (...args) => pTry(fn, ...args);
-}
-
 function assertRequiredOptions (options, requiredKeys) {
     for (const key of requiredKeys) {
         if (!options[key]) throw new Error(`Missing required option --${key}`);


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to