diff --git a/package.json b/package.json index 5d8c5ad9..65ebf2e5 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,10 @@ "author": "Salesforce", "bugs": "https://github.com/oclif/plugin-command-snapshot/issues", "dependencies": { - "@oclif/core": "^3.20.0", + "@oclif/core": "3.21.0", "@types/lodash.difference": "^4.5.9", "chalk": "^5.3.0", + "globby": "^14.0.1", "just-diff": "^5.2.0", "lodash.difference": "^4.5.0", "lodash.get": "^4.4.2", diff --git a/src/commands/schema/compare.ts b/src/commands/schema/compare.ts index d3cc0a5a..7f77647e 100644 --- a/src/commands/schema/compare.ts +++ b/src/commands/schema/compare.ts @@ -8,8 +8,8 @@ import * as semver from 'semver' import {Schema} from 'ts-json-schema-generator' import SnapshotCommand from '../../snapshot-command.js' -import {getKeyNameFromFilename} from '../../util.js' -import {SchemaGenerator, Schemas, getAllFiles} from './generate.js' +import {GLOB_PATTERNS, getAllFiles, getKeyNameFromFilename} from '../../util.js' +import {SchemaGenerator, Schemas} from './generate.js' export type SchemaComparison = Array<{op: Operation; path: (number | string)[]; value: unknown}> @@ -32,6 +32,25 @@ export default class SchemaCompare extends SnapshotCommand { } public async run(): Promise { + const strategy = + typeof this.config.pjson.oclif?.commands === 'string' ? 'pattern' : this.config.pjson.oclif?.commands?.strategy + const commandsDir = + typeof this.config.pjson.oclif?.commands === 'string' + ? this.config.pjson.oclif?.commands + : this.config.pjson.oclif?.commands?.target + const commandGlobs = + typeof this.config.pjson.oclif?.commands === 'string' + ? GLOB_PATTERNS + : this.config.pjson.oclif?.commands?.globPatterns + + if (strategy === 'single') { + this.error('This command is not supported for single command CLIs') + } + + if (strategy === 'explicit') { + this.error('This command is not supported for explicit command discovery') + } + const {flags} = await this.parse(SchemaCompare) try { @@ -42,7 +61,8 @@ export default class SchemaCompare extends SnapshotCommand { } const existingSchema = this.readExistingSchema(flags.filepath) - const latestSchema = await this.generateLatestSchema() + const generator = new SchemaGenerator({base: this, commandGlobs, commandsDir}) + const latestSchema = await generator.generate() this.debug('existingSchema', existingSchema) this.debug('latestSchema', latestSchema) const changes = diff(latestSchema, existingSchema) @@ -113,7 +133,7 @@ export default class SchemaCompare extends SnapshotCommand { } this.log() - const bin = process.platform === 'win32' ? 'bin\\dev.cmd' : 'bin/dev' + const bin = process.platform === 'win32' ? 'bin\\dev.cmd' : 'bin/dev.js' this.log( 'If intended, please update the schema file(s) and run again:', chalk.bold(`${bin} ${toConfiguredId('schema:generate', this.config)}`), @@ -122,11 +142,6 @@ export default class SchemaCompare extends SnapshotCommand { return changes } - private async generateLatestSchema(): Promise { - const generator = new SchemaGenerator(this) - return generator.generate() - } - private readExistingSchema(filePath: string): Schemas { const contents = fs.readdirSync(filePath) const folderIsVersioned = contents.every((c) => semver.valid(c)) diff --git a/src/commands/schema/generate.ts b/src/commands/schema/generate.ts index 6887b557..14ad88a0 100644 --- a/src/commands/schema/generate.ts +++ b/src/commands/schema/generate.ts @@ -1,11 +1,12 @@ import {Flags} from '@oclif/core' import chalk from 'chalk' +import {globbySync} from 'globby' import * as fs from 'node:fs' import * as path from 'node:path' import {Schema, createGenerator} from 'ts-json-schema-generator' import SnapshotCommand from '../../snapshot-command.js' -import {getSchemaFileName} from '../../util.js' +import {GLOB_PATTERNS, getAllFiles, getSchemaFileName} from '../../util.js' export type SchemasMap = { [key: string]: Schema @@ -15,32 +16,26 @@ export type Schemas = {commands: SchemasMap; hooks: SchemasMap} export type GenerateResponse = string[] -export function getAllFiles(dirPath: string, ext: string, allFiles: string[] = []): string[] { - let files: string[] = [] - try { - files = fs.readdirSync(dirPath) - } catch {} - - for (const file of files) { - const fPath = path.join(dirPath, file) - if (fs.statSync(fPath).isDirectory()) { - allFiles = getAllFiles(fPath, ext, allFiles) - } else if (file.endsWith(ext)) { - allFiles.push(fPath) - } - } - - return allFiles +type SchemaGenerateOptions = { + base: SnapshotCommand + commandGlobs?: string[] + commandsDir?: string + ignoreVoid?: boolean } export class SchemaGenerator { + private base: SnapshotCommand private classToId: Record = {} - - // eslint-disable-next-line no-useless-constructor - constructor( - private base: SnapshotCommand, - private ignoreVoid = true, - ) {} + private commandGlobs: string[] + private commandsDir: string | undefined + private ignoreVoid: boolean + + constructor(options: SchemaGenerateOptions) { + this.base = options.base + this.ignoreVoid = options.ignoreVoid ?? true + this.commandsDir = options.commandsDir + this.commandGlobs = options.commandGlobs ?? GLOB_PATTERNS + } public async generate(): Promise { for (const cmd of this.base.commands) { @@ -102,7 +97,12 @@ export class SchemaGenerator { } private getAllCmdFiles(): string[] { - const {rootDir} = this.getDirs() + const {outDir, rootDir} = this.getDirs() + if (this.commandsDir) { + const commandsSrcDir = path.resolve(this.base.config.root, this.commandsDir).replace(outDir, rootDir) + return globbySync(this.commandGlobs, {absolute: true, cwd: commandsSrcDir}) + } + return getAllFiles(path.join(rootDir, 'commands'), '.ts') } @@ -217,8 +217,27 @@ export default class SchemaGenerate extends SnapshotCommand { } public async run(): Promise { + const strategy = + typeof this.config.pjson.oclif?.commands === 'string' ? 'pattern' : this.config.pjson.oclif?.commands?.strategy + const commandsDir = + typeof this.config.pjson.oclif?.commands === 'string' + ? this.config.pjson.oclif?.commands + : this.config.pjson.oclif?.commands?.target + const commandGlobs = + typeof this.config.pjson.oclif?.commands === 'string' + ? GLOB_PATTERNS + : this.config.pjson.oclif?.commands?.globPatterns + + if (strategy === 'single') { + this.error('This command is not supported for single command CLIs') + } + + if (strategy === 'explicit') { + this.error('This command is not supported for explicit command discovery') + } + const {flags} = await this.parse(SchemaGenerate) - const generator = new SchemaGenerator(this, flags.ignorevoid) + const generator = new SchemaGenerator({base: this, commandGlobs, commandsDir, ignoreVoid: flags.ignorevoid}) const schemas = await generator.generate() diff --git a/src/util.ts b/src/util.ts index 792da68d..f16dfe79 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,3 +1,6 @@ +import fs from 'node:fs' +import path from 'node:path' + /** * Get the file name for a given command ID replacing "-" with "__" and ":" with "-" * @param cmdId - command ID @@ -15,3 +18,24 @@ export const getSchemaFileName = (cmdId: string): string => { */ export const getKeyNameFromFilename = (file: string): string => file.replaceAll('-', ':').replaceAll('__', '-').replace('.json', '') + +export const getAllFiles = (dirPath: string, ext: string, allFiles: string[] = []): string[] => + safeReadDirSync(dirPath) + .flatMap((f) => + f.isDirectory() ? getAllFiles(path.join(dirPath, f.name), ext, allFiles) : path.join(dirPath, f.name), + ) + .filter((f) => f.endsWith(ext)) + +const safeReadDirSync = (dirPath: string): fs.Dirent[] => { + try { + // TODO: use recursive option when available in Node 20 + return fs.readdirSync(dirPath, {withFileTypes: true}) + } catch { + return [] + } +} + +export const GLOB_PATTERNS = [ + '**/*.+(js|cjs|mjs|ts|tsx|mts|cts)', + '!**/*.+(d.ts|test.ts|test.js|spec.ts|spec.js|d.mts|d.cts)?(x)', +] diff --git a/yarn.lock b/yarn.lock index ea9d011f..c86b58d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1311,6 +1311,39 @@ node-gyp "^8.2.0" read-package-json-fast "^2.0.1" +"@oclif/core@3.21.0": + version "3.21.0" + resolved "https://registry.yarnpkg.com/@oclif/core/-/core-3.21.0.tgz#a235704e66589c8c104ccd616d0a8f1f36cf693e" + integrity sha512-xR7qGPOWtOnYmdYocSn6oEh2oTQLsPOXoj3HYGpb26V3WulwF8Cm33WPnMsSISv4ben3Rtc5i59u9O5NnuG42g== + dependencies: + "@types/cli-progress" "^3.11.5" + ansi-escapes "^4.3.2" + ansi-styles "^4.3.0" + cardinal "^2.1.1" + chalk "^4.1.2" + clean-stack "^3.0.1" + cli-progress "^3.12.0" + color "^4.2.3" + debug "^4.3.4" + ejs "^3.1.9" + get-package-type "^0.1.0" + globby "^11.1.0" + hyperlinker "^1.0.0" + indent-string "^4.0.0" + is-wsl "^2.2.0" + js-yaml "^3.14.1" + natural-orderby "^2.0.3" + object-treeify "^1.1.33" + password-prompt "^1.1.3" + slice-ansi "^4.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + supports-color "^8.1.1" + supports-hyperlinks "^2.2.0" + widest-line "^3.1.0" + wordwrap "^1.0.0" + wrap-ansi "^7.0.0" + "@oclif/core@^2.15.0": version "2.15.0" resolved "https://registry.yarnpkg.com/@oclif/core/-/core-2.15.0.tgz#f27797b30a77d13279fba88c1698fc34a0bd0d2a" @@ -1345,7 +1378,7 @@ wordwrap "^1.0.0" wrap-ansi "^7.0.0" -"@oclif/core@^3.18.1", "@oclif/core@^3.19.2", "@oclif/core@^3.19.3", "@oclif/core@^3.20.0": +"@oclif/core@^3.18.1", "@oclif/core@^3.19.2", "@oclif/core@^3.19.3": version "3.20.0" resolved "https://registry.yarnpkg.com/@oclif/core/-/core-3.20.0.tgz#534458dc6e8c46d8f03906aaadaca079e16a6554" integrity sha512-8BajhglY8frYGAS1whAukeouFZUN9MgQoLfNXtScPVEAjPlaD2BbSIAYQH2yF2qb/iVvbj/1DwYS3gqicYOq1A== @@ -1532,6 +1565,11 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== +"@sindresorhus/merge-streams@^2.1.0": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@sindresorhus/merge-streams/-/merge-streams-2.2.1.tgz#82b5e1e135ef62ef8b522d6e7f43ad360a69f294" + integrity sha512-255V7MMIKw6aQ43Wbqp9HZ+VHn6acddERTLiiLnlcPLU9PdTq9Aijl12oklAgUEblLWye+vHLzmqBx6f2TGcZw== + "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" @@ -4105,7 +4143,7 @@ fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" -fast-glob@^3.3.0: +fast-glob@^3.3.0, fast-glob@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== @@ -4552,6 +4590,18 @@ globby@^13.1.2: merge2 "^1.4.1" slash "^4.0.0" +globby@^14.0.1: + version "14.0.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-14.0.1.tgz#a1b44841aa7f4c6d8af2bc39951109d77301959b" + integrity sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ== + dependencies: + "@sindresorhus/merge-streams" "^2.1.0" + fast-glob "^3.3.2" + ignore "^5.2.4" + path-type "^5.0.0" + slash "^5.1.0" + unicorn-magic "^0.1.0" + gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -6575,6 +6625,11 @@ path-type@^4.0.0: resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +path-type@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-5.0.0.tgz#14b01ed7aea7ddf9c7c3f46181d4d04f9c785bb8" + integrity sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg== + pathval@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" @@ -7179,6 +7234,11 @@ slash@^4.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== +slash@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-5.1.0.tgz#be3adddcdf09ac38eebe8dcdc7b1a57a75b095ce" + integrity sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg== + slice-ansi@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" @@ -7764,6 +7824,11 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +unicorn-magic@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/unicorn-magic/-/unicorn-magic-0.1.0.tgz#1bb9a51c823aaf9d73a8bfcd3d1a23dde94b0ce4" + integrity sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ== + unique-filename@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230"