From 12f1458a332c36e889a2b4c228a15dba86154b77 Mon Sep 17 00:00:00 2001 From: Shazron Abdullah <36107+shazron@users.noreply.github.com> Date: Thu, 26 Mar 2026 06:54:58 +0800 Subject: [PATCH] feat: update @oclif/core to v4 and migrate ESLint to v9 flat config - Upgrade @oclif/core from ^2.8.12 to ^4.0.0 (fixes #391) - Replace removed ux.table with custom src/ux-table.js (ESM-only @oclif/table is incompatible with CJS) - Upgrade @adobe/eslint-config-aio-lib-config to 5.0.0, eslint to ^9.0.0 - Migrate ESLint config from .eslintrc to eslint.config.js (flat config format) - Patch Command.prototype.parse in test setup for oclif v4 config.runHook requirement - Update createTestFlagsFunction to check 'flags' instead of '_flags' (oclif v4 change) - Simplify api/list.js --json flag handling (remove oclif v2 workaround) - Maintain 100% branch/line/statement coverage Co-Authored-By: Claude Sonnet 4.6 --- .eslintrc | 22 ------- .gitignore | 1 + eslint.config.js | 41 +++++++++++++ package.json | 17 ++---- src/commands/runtime/action/list.js | 5 +- src/commands/runtime/activation/list.js | 5 +- src/commands/runtime/api/list.js | 12 ++-- src/commands/runtime/namespace/get.js | 11 ++-- src/commands/runtime/namespace/list.js | 5 +- src/commands/runtime/package/list.js | 5 +- src/commands/runtime/property/get.js | 5 +- src/commands/runtime/rule/list.js | 5 +- src/commands/runtime/trigger/list.js | 5 +- src/ux-table.js | 79 +++++++++++++++++++++++++ test/commands/runtime/api/list.test.js | 11 +--- test/jest.setup.js | 16 ++++- 16 files changed, 174 insertions(+), 71 deletions(-) delete mode 100644 .eslintrc create mode 100644 eslint.config.js create mode 100644 src/ux-table.js diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 8d3ddfc5..00000000 --- a/.eslintrc +++ /dev/null @@ -1,22 +0,0 @@ -{ - "extends": "@adobe/eslint-config-aio-lib-config", - "globals": { - "fixtureFile": true, - "fixtureFileWithTimeZoneAdjustment": true, - "fixtureJson": true, - "fixtureZip": true, - "fakeFileSystem": true, - "createTestBaseFlagsFunction": true, - "createTestFlagsFunction": true - }, - "rules": { - "jsdoc/tag-lines": [ - // The Error level should be `error`, `warn`, or `off` (or 2, 1, or 0) - "error", - "never", - { - "startLines": null - } - ] - } -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 039af9ff..b53c3908 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ junit.xml oclif.manifest.json .vscode .idea +.claude diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..0443a0a5 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,41 @@ +/* +Copyright 2019 Adobe Inc. All rights reserved. +This file is licensed 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 REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +const aioConfig = require('@adobe/eslint-config-aio-lib-config') +const jestPlugin = require('eslint-plugin-jest') + +const testGlobals = { + fixtureFile: 'readonly', + fixtureFileWithTimeZoneAdjustment: 'readonly', + fixtureJson: 'readonly', + fixtureZip: 'readonly', + fakeFileSystem: 'readonly', + createTestBaseFlagsFunction: 'readonly', + createTestFlagsFunction: 'readonly' +} + +module.exports = [ + ...aioConfig, + { + ignores: ['node_modules/**', 'coverage/**'] + }, + { + files: ['test/**/*.js', 'e2e/**/*.js'], + ...jestPlugin.configs['flat/recommended'], + languageOptions: { + globals: { + ...jestPlugin.configs['flat/recommended'].languageOptions.globals, + ...testGlobals + } + } + } +] diff --git a/package.json b/package.json index fdccd49c..8ed24c51 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "@adobe/aio-lib-env": "^3.0.1", "@adobe/aio-lib-ims": "^8.0.1", "@adobe/aio-lib-runtime": "^7.1.0", - "@oclif/core": "^2.8.12", + "@oclif/core": "^4.0.0", "@types/jest": "^29.5.3", "chalk": "^4.1.2", "dayjs": "^1.10.4", @@ -27,26 +27,21 @@ "sha1": "^1.1.1" }, "devDependencies": { - "@adobe/eslint-config-aio-lib-config": "^4.0.0", + "@adobe/eslint-config-aio-lib-config": "5.0.0", "@babel/core": "^7.16.12", "@babel/preset-env": "^7.16.11", "babel-jest": "^29.5.0", "babel-runtime": "^6.26.0", "dedent-js": "^1.0.1", "eol": "^0.10.0", - "eslint": "^8.57.1", - "eslint-config-oclif": "^5.2.2", - "eslint-config-standard": "^17.1.0", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jest": "^27.9.0", + "eslint": "^9.0.0", + "eslint-plugin-jest": "^29.0.0", "eslint-plugin-jsdoc": "^48.11.0", - "eslint-plugin-n": "^15.7.0", - "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^6.6.0", "execa": "^4.0.0", "jest": "^29.6.2", "jest-junit": "^16.0.0", - "oclif": "^3.2.0", + "neostandard": "^0", + "oclif": "^4.0.0", "stdout-stderr": "^0.1.9" }, "engines": { diff --git a/src/commands/runtime/action/list.js b/src/commands/runtime/action/list.js index 6c2b90e3..47430b63 100644 --- a/src/commands/runtime/action/list.js +++ b/src/commands/runtime/action/list.js @@ -13,7 +13,8 @@ governing permissions and limitations under the License. const moment = require('dayjs') const RuntimeBaseCommand = require('../../../RuntimeBaseCommand') const { parsePackageName } = require('@adobe/aio-lib-runtime').utils -const { Args, Flags, ux } = require('@oclif/core') +const { Args, Flags } = require('@oclif/core') +const { table } = require('../../../ux-table') const decorators = require('../../../decorators').decorators() class ActionList extends RuntimeBaseCommand { @@ -86,7 +87,7 @@ class ActionList extends RuntimeBaseCommand { } } } - ux.table(result, columns) + table(result, columns) } } catch (err) { await this.handleError('failed to list the actions', err) diff --git a/src/commands/runtime/activation/list.js b/src/commands/runtime/activation/list.js index 1777f981..bce0860a 100644 --- a/src/commands/runtime/activation/list.js +++ b/src/commands/runtime/activation/list.js @@ -12,7 +12,8 @@ governing permissions and limitations under the License. const moment = require('dayjs') const RuntimeBaseCommand = require('../../../RuntimeBaseCommand') -const { Args, Flags, ux } = require('@oclif/core') +const { Args, Flags } = require('@oclif/core') +const { table } = require('../../../ux-table') const decorators = require('../../../decorators').decorators() const statusStrings = ['success', 'app error', 'dev error', 'sys error'] @@ -187,7 +188,7 @@ class ActivationList extends RuntimeBaseCommand { } } if (listActivation) { - ux.table(listActivation, columns, { + table(listActivation, columns, { 'no-truncate': true }) } diff --git a/src/commands/runtime/api/list.js b/src/commands/runtime/api/list.js index 3907e254..646c9f78 100644 --- a/src/commands/runtime/api/list.js +++ b/src/commands/runtime/api/list.js @@ -10,7 +10,8 @@ governing permissions and limitations under the License. */ const RuntimeBaseCommand = require('../../../RuntimeBaseCommand') -const { Args, Flags, ux } = require('@oclif/core') +const { Args, Flags } = require('@oclif/core') +const { table } = require('../../../ux-table') /** @private */ function processApi (api) { @@ -40,13 +41,8 @@ function processApi (api) { class ApiList extends RuntimeBaseCommand { async run () { - // Workaround for oclif v2 parsing issue: capture argv before parse() when multiple optional args are present - // oclif v2 doesn't properly parse --json flag when command has 3+ optional positional arguments - // Related: https://github.com/oclif/core/issues/854 (workaround: search argv directly) - const argvBeforeParse = [...(this.argv ?? [])] const { args, flags } = await this.parse(ApiList) - const hasJsonInArgv = argvBeforeParse.includes('--json') - const shouldOutputJson = flags.json || hasJsonInArgv + const shouldOutputJson = flags.json try { const ow = await this.wsk() @@ -74,7 +70,7 @@ class ApiList extends RuntimeBaseCommand { }, data) }) - ux.table(data, { + table(data, { Action: { minWidth: 10 }, Verb: { minWidth: 10 }, APIName: { header: 'API Name', minWidth: 10 }, diff --git a/src/commands/runtime/namespace/get.js b/src/commands/runtime/namespace/get.js index 613bf475..1fd79334 100644 --- a/src/commands/runtime/namespace/get.js +++ b/src/commands/runtime/namespace/get.js @@ -11,7 +11,8 @@ governing permissions and limitations under the License. */ const RuntimeBaseCommand = require('../../../RuntimeBaseCommand') -const { Flags, ux } = require('@oclif/core') +const { Flags } = require('@oclif/core') +const { table } = require('../../../ux-table') /** @private */ function createColumns (columnName) { @@ -79,10 +80,10 @@ class NamespaceGet extends RuntimeBaseCommand { } else { this.log('Entities in namespace:') - ux.table(data.packages, createColumns('packages')) - ux.table(data.actions, createColumns('actions')) - ux.table(data.triggers, createColumns('triggers')) - ux.table(data.rules, createColumns('rules')) + table(data.packages, createColumns('packages')) + table(data.actions, createColumns('actions')) + table(data.triggers, createColumns('triggers')) + table(data.rules, createColumns('rules')) } } catch (err) { await this.handleError('failed to get the data for a namespace', err) diff --git a/src/commands/runtime/namespace/list.js b/src/commands/runtime/namespace/list.js index 1cd804a2..c224987a 100644 --- a/src/commands/runtime/namespace/list.js +++ b/src/commands/runtime/namespace/list.js @@ -11,7 +11,8 @@ governing permissions and limitations under the License. */ const RuntimeBaseCommand = require('../../../RuntimeBaseCommand') -const { Flags, ux } = require('@oclif/core') +const { Flags } = require('@oclif/core') +const { table } = require('../../../ux-table') class NamespaceList extends RuntimeBaseCommand { async run () { @@ -29,7 +30,7 @@ class NamespaceList extends RuntimeBaseCommand { get: row => row } } - ux.table(result, columns) + table(result, columns) } } catch (err) { await this.handleError('failed to list namespaces', err) diff --git a/src/commands/runtime/package/list.js b/src/commands/runtime/package/list.js index d9449c88..0bacf4b1 100644 --- a/src/commands/runtime/package/list.js +++ b/src/commands/runtime/package/list.js @@ -12,7 +12,8 @@ governing permissions and limitations under the License. const moment = require('dayjs') const RuntimeBaseCommand = require('../../../RuntimeBaseCommand') -const { Args, Flags, ux } = require('@oclif/core') +const { Args, Flags } = require('@oclif/core') +const { table } = require('../../../ux-table') class PackageList extends RuntimeBaseCommand { async run () { @@ -78,7 +79,7 @@ class PackageList extends RuntimeBaseCommand { get: row => row.name } } - ux.table(result, columns) + table(result, columns) } } catch (err) { await this.handleError('failed to list the packages', err) diff --git a/src/commands/runtime/property/get.js b/src/commands/runtime/property/get.js index a041de77..8887c939 100644 --- a/src/commands/runtime/property/get.js +++ b/src/commands/runtime/property/get.js @@ -10,7 +10,8 @@ governing permissions and limitations under the License. */ const RuntimeBaseCommand = require('../../../RuntimeBaseCommand') -const { Flags, ux } = require('@oclif/core') +const { Flags } = require('@oclif/core') +const { table } = require('../../../ux-table') const { createFetch } = require('@adobe/aio-lib-core-networking') const { PropertyKey, PropertyDefault, propertiesFile, PropertyEnv } = require('../../../properties') const debug = require('debug')('aio-cli-plugin-runtime/property') @@ -90,7 +91,7 @@ class PropertyGet extends RuntimeBaseCommand { data.push({ Property: PropertyGet.flags.apibuildno.description, Value: result.buildno }) } } - ux.table(data, + table(data, { Property: { minWidth: 10 }, Value: { minWidth: 20 } diff --git a/src/commands/runtime/rule/list.js b/src/commands/runtime/rule/list.js index cbcf4580..d24cb852 100644 --- a/src/commands/runtime/rule/list.js +++ b/src/commands/runtime/rule/list.js @@ -11,7 +11,8 @@ governing permissions and limitations under the License. const moment = require('dayjs') const RuntimeBaseCommand = require('../../../RuntimeBaseCommand') -const { Flags, ux } = require('@oclif/core') +const { Flags } = require('@oclif/core') +const { table } = require('../../../ux-table') class RuleList extends RuntimeBaseCommand { async run () { @@ -68,7 +69,7 @@ class RuleList extends RuntimeBaseCommand { get: row => row.name } } - ux.table(resultsWithStatus, columns) + table(resultsWithStatus, columns) } }) return p diff --git a/src/commands/runtime/trigger/list.js b/src/commands/runtime/trigger/list.js index 81179072..aef3b03a 100644 --- a/src/commands/runtime/trigger/list.js +++ b/src/commands/runtime/trigger/list.js @@ -12,7 +12,8 @@ governing permissions and limitations under the License. const moment = require('dayjs') const RuntimeBaseCommand = require('../../../RuntimeBaseCommand') -const { Flags, ux } = require('@oclif/core') +const { Flags } = require('@oclif/core') +const { table } = require('../../../ux-table') class TriggerList extends RuntimeBaseCommand { async run () { @@ -84,7 +85,7 @@ class TriggerList extends RuntimeBaseCommand { get: row => row.name } } - ux.table(resultsWithStatus, columns) + table(resultsWithStatus, columns) } }) } catch (err) { diff --git a/src/ux-table.js b/src/ux-table.js new file mode 100644 index 00000000..6ce5c2db --- /dev/null +++ b/src/ux-table.js @@ -0,0 +1,79 @@ +/* +Copyright 2019 Adobe Inc. All rights reserved. +This file is licensed 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 REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +/** + * Print a simple text table to stdout. + * + * Replaces ux.table from @oclif/core v1-v3 (removed in v4). + * + * @param {Array} data - Array of data objects + * @param {object} columns - Column definitions. Each key maps to a column. Supports: + * - header {string}: column header (defaults to key, capitalized) + * - get {function}: accessor function (defaults to row[key]) + * - minWidth {number}: minimum column width + * @param {object} [options] - Options: + * - printLine {function}: function to print a line (defaults to process.stdout.write) + * - 'no-header' {boolean}: skip printing the header row + * - 'no-truncate' {boolean}: ignored (included for API compatibility) + */ +function table (data, columns, options = {}) { + const printLine = options.printLine || ((s) => process.stdout.write(s + '\n')) + + const cols = Object.keys(columns).map(key => { + const col = columns[key] + const header = typeof col.header === 'string' ? col.header : key.charAt(0).toUpperCase() + key.slice(1) + const getValue = col.get || ((row) => row[key]) + const minWidth = Math.max(col.minWidth || 0, col.maxWidth || 0, header.length + 1) + return { key, header, getValue, minWidth } + }) + + // Compute string values for all rows + const rows = data.map(row => { + const result = {} + for (const col of cols) { + const val = col.getValue(row) + result[col.key] = val != null ? String(val) : '' + } + return result + }) + + // Compute column widths: max of minWidth and widest value + 1 + const widths = {} + for (const col of cols) { + const maxDataWidth = rows.length > 0 ? Math.max(...rows.map(r => r[col.key].length)) : 0 + widths[col.key] = Math.max(col.minWidth, maxDataWidth + 1) + } + + const rowStart = ' ' + + // Print header and divider + let header = rowStart + let divider = rowStart + for (const col of cols) { + const w = widths[col.key] + header += col.header.padEnd(w) + divider += ''.padEnd(w - 1, '─') + ' ' + } + printLine(header) + printLine(divider) + + // Print rows + for (const row of rows) { + let line = rowStart + for (const col of cols) { + line += row[col.key].padEnd(widths[col.key]) + } + printLine(line) + } +} + +module.exports = { table } diff --git a/test/commands/runtime/api/list.test.js b/test/commands/runtime/api/list.test.js index 52ba263f..baf469d0 100644 --- a/test/commands/runtime/api/list.test.js +++ b/test/commands/runtime/api/list.test.js @@ -114,18 +114,9 @@ describe('instance methods', () => { test('handles falsy argv gracefully', async () => { rtLib.mockResolvedFixture(rtAction, 'api/list.json') const cmd = new TheCommand([]) - const originalArgv = cmd.argv - let argvAccessCount = 0 - Object.defineProperty(cmd, 'argv', { - get: function () { - argvAccessCount++ - return argvAccessCount === 1 ? undefined : originalArgv - }, - configurable: true - }) return cmd.run() .then(() => { - expect(argvAccessCount).toBeGreaterThan(0) + expect(cmd.argv).toBeDefined() }) }) diff --git a/test/jest.setup.js b/test/jest.setup.js index 4664a302..b23383a1 100644 --- a/test/jest.setup.js +++ b/test/jest.setup.js @@ -16,6 +16,19 @@ const eol = require('eol') jest.setTimeout(30000) +// oclif v4 requires this.config.runHook in Command.parse(). +// Patch the prototype so unit tests that instantiate commands directly still work. +const { Command } = require('@oclif/core') +const _originalParse = Command.prototype.parse +Command.prototype.parse = async function (options, argv) { + if (!this.config) { + this.config = { runHook: async () => ({ successes: [], failures: [] }), bin: 'aio', userAgent: 'test/0.0.0', findCommand: () => null, pjson: { oclif: {} } } + } else if (!this.config.runHook) { + this.config.runHook = async () => ({ successes: [], failures: [] }) + } + return _originalParse.call(this, options, argv) +} + global.__mockFs = {} jest.mock('fs', () => { const actualFs = jest.requireActual('fs') @@ -240,7 +253,8 @@ global.createTestFlagsFunction = (TheCommand, Flags) => { return () => { // every command needs to override .flags (for global flags) // eslint: see https://eslint.org/docs/rules/no-prototype-builtins - expect(Object.prototype.hasOwnProperty.call(TheCommand, '_flags')).toBeTruthy() + // In oclif v4, flags are stored as 'flags' (not '_flags' as in v1) + expect(Object.prototype.hasOwnProperty.call(TheCommand, 'flags')).toBeTruthy() const flagsKeys = Object.keys(Flags) const theCommandFlagKeys = Object.keys(TheCommand.flags)