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]