@oclif/core 3.19.7 → 3.20.1-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cache.d.ts CHANGED
@@ -2,15 +2,23 @@ import { PJSON, Plugin } from './interfaces';
2
2
  type CacheContents = {
3
3
  rootPlugin: Plugin;
4
4
  exitCodes: PJSON.Plugin['oclif']['exitCodes'];
5
+ '@oclif/core': OclifCoreInfo;
5
6
  };
6
7
  type ValueOf<T> = T[keyof T];
8
+ type OclifCoreInfo = {
9
+ name: string;
10
+ version: string;
11
+ };
7
12
  /**
8
13
  * A simple cache for storing values that need to be accessed globally.
9
14
  */
10
15
  export default class Cache extends Map<keyof CacheContents, ValueOf<CacheContents>> {
11
16
  static instance: Cache;
17
+ constructor();
12
18
  static getInstance(): Cache;
19
+ get(key: '@oclif/core'): OclifCoreInfo;
13
20
  get(key: 'rootPlugin'): Plugin | undefined;
14
21
  get(key: 'exitCodes'): PJSON.Plugin['oclif']['exitCodes'] | undefined;
22
+ private getOclifCoreMeta;
15
23
  }
16
24
  export {};
package/lib/cache.js CHANGED
@@ -1,10 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const node_fs_1 = require("node:fs");
4
+ const node_path_1 = require("node:path");
3
5
  /**
4
6
  * A simple cache for storing values that need to be accessed globally.
5
7
  */
6
8
  class Cache extends Map {
7
9
  static instance;
10
+ constructor() {
11
+ super();
12
+ this.set('@oclif/core', this.getOclifCoreMeta());
13
+ }
8
14
  static getInstance() {
9
15
  if (!Cache.instance) {
10
16
  Cache.instance = new Cache();
@@ -14,5 +20,22 @@ class Cache extends Map {
14
20
  get(key) {
15
21
  return super.get(key);
16
22
  }
23
+ getOclifCoreMeta() {
24
+ try {
25
+ // eslint-disable-next-line node/no-extraneous-require
26
+ return { name: '@oclif/core', version: require('@oclif/core/package.json').version };
27
+ }
28
+ catch {
29
+ try {
30
+ return {
31
+ name: '@oclif/core',
32
+ version: JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, '..', 'package.json'), 'utf8')),
33
+ };
34
+ }
35
+ catch {
36
+ return { name: '@oclif/core', version: 'unknown' };
37
+ }
38
+ }
39
+ }
17
40
  }
18
41
  exports.default = Cache;
@@ -4,7 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.config = exports.Config = void 0;
7
- const fs_1 = require("../util/fs");
7
+ const cache_1 = __importDefault(require("../cache"));
8
8
  const simple_1 = __importDefault(require("./action/simple"));
9
9
  const spinner_1 = __importDefault(require("./action/spinner"));
10
10
  const g = global;
@@ -35,7 +35,8 @@ class Config {
35
35
  }
36
36
  exports.Config = Config;
37
37
  function fetch() {
38
- const major = (0, fs_1.requireJson)(__dirname, '..', '..', 'package.json').version.split('.')[0];
38
+ const core = cache_1.default.getInstance().get('@oclif/core');
39
+ const major = core?.version.split('.')[0] || 'unknown';
39
40
  if (globals[major])
40
41
  return globals[major];
41
42
  globals[major] = new Config();
package/lib/command.js CHANGED
@@ -30,16 +30,16 @@ exports.Command = void 0;
30
30
  const chalk_1 = __importDefault(require("chalk"));
31
31
  const node_url_1 = require("node:url");
32
32
  const node_util_1 = require("node:util");
33
+ const cache_1 = __importDefault(require("./cache"));
33
34
  const cli_ux_1 = require("./cli-ux");
34
35
  const config_1 = require("./config");
35
36
  const Errors = __importStar(require("./errors"));
36
37
  const util_1 = require("./help/util");
37
38
  const Parser = __importStar(require("./parser"));
38
39
  const aggregate_flags_1 = require("./util/aggregate-flags");
39
- const fs_1 = require("./util/fs");
40
40
  const ids_1 = require("./util/ids");
41
41
  const util_2 = require("./util/util");
42
- const pjson = (0, fs_1.requireJson)(__dirname, '..', 'package.json');
42
+ const pjson = cache_1.default.getInstance().get('@oclif/core');
43
43
  /**
44
44
  * swallows stdout epipe errors
45
45
  * this occurs when stdout closes such as when piping to head
@@ -18,6 +18,7 @@ export declare class Config implements IConfig {
18
18
  errlog: string;
19
19
  flexibleTaxonomy: boolean;
20
20
  home: string;
21
+ isSingleCommandCLI: boolean;
21
22
  name: string;
22
23
  npmRegistry?: string;
23
24
  nsisCustomization?: string;
@@ -44,11 +44,11 @@ const fs_1 = require("../util/fs");
44
44
  const os_1 = require("../util/os");
45
45
  const util_2 = require("../util/util");
46
46
  const plugin_loader_1 = __importDefault(require("./plugin-loader"));
47
- const ts_node_1 = require("./ts-node");
47
+ const ts_path_1 = require("./ts-path");
48
48
  const util_3 = require("./util");
49
49
  // eslint-disable-next-line new-cap
50
50
  const debug = (0, util_3.Debug)();
51
- const _pjson = (0, fs_1.requireJson)(__dirname, '..', '..', 'package.json');
51
+ const _pjson = cache_1.default.getInstance().get('@oclif/core');
52
52
  const BASE = `${_pjson.name}@${_pjson.version}`;
53
53
  function channelFromVersion(version) {
54
54
  const m = version.match(/[^-]+(?:-([^.]+))?/);
@@ -98,6 +98,7 @@ class Config {
98
98
  errlog;
99
99
  flexibleTaxonomy;
100
100
  home;
101
+ isSingleCommandCLI = false;
101
102
  name;
102
103
  npmRegistry;
103
104
  nsisCustomization;
@@ -328,6 +329,10 @@ class Config {
328
329
  ...(s3.templates && s3.templates.vanilla),
329
330
  },
330
331
  };
332
+ this.isSingleCommandCLI = Boolean(this.pjson.oclif.default ||
333
+ (typeof this.pjson.oclif.commands !== 'string' &&
334
+ this.pjson.oclif.commands?.strategy === 'single' &&
335
+ this.pjson.oclif.commands?.target));
331
336
  await this.loadPluginsAndCommands();
332
337
  debug('config done');
333
338
  marker?.addDetails({
@@ -482,14 +487,21 @@ class Config {
482
487
  };
483
488
  const hooks = p.hooks[event] || [];
484
489
  for (const hook of hooks) {
485
- const marker = performance_1.Performance.mark(performance_1.OCLIF_MARKER_OWNER, `config.runHook#${p.name}(${hook})`);
490
+ const marker = performance_1.Performance.mark(performance_1.OCLIF_MARKER_OWNER, `config.runHook#${p.name}(${hook.target})`);
486
491
  try {
487
492
  /* eslint-disable no-await-in-loop */
488
- const { filePath, isESM, module } = await (0, module_loader_1.loadWithData)(p, await (0, ts_node_1.tsPath)(p.root, hook, p));
493
+ const { filePath, isESM, module } = await (0, module_loader_1.loadWithData)(p, await (0, ts_path_1.tsPath)(p.root, hook.target, p));
489
494
  debug('start', isESM ? '(import)' : '(require)', filePath);
495
+ // If no hook is found using the identifier, then we should `search` for the hook but only if the hook identifier is 'default'
496
+ // A named identifier (e.g. MY_HOOK) that isn't found indicates that the hook isn't implemented in the plugin.
497
+ const hookFn = module[hook.identifier] ?? (hook.identifier === 'default' ? search(module) : undefined);
498
+ if (!hookFn) {
499
+ debug('No hook found for hook definition:', hook);
500
+ continue;
501
+ }
490
502
  const result = timeout
491
- ? await withTimeout(timeout, search(module).call(context, { ...opts, config: this, context }))
492
- : await search(module).call(context, { ...opts, config: this, context });
503
+ ? await withTimeout(timeout, hookFn.call(context, { ...opts, config: this, context }))
504
+ : await hookFn.call(context, { ...opts, config: this, context });
493
505
  final.successes.push({ plugin: p, result });
494
506
  if (p.name === '@oclif/plugin-legacy' && event === 'init') {
495
507
  this.insertLegacyPlugins(result);
@@ -511,7 +523,7 @@ class Config {
511
523
  }
512
524
  marker?.addDetails({
513
525
  event,
514
- hook,
526
+ hook: hook.target,
515
527
  plugin: p.name,
516
528
  });
517
529
  marker?.stop();
@@ -1,3 +1,3 @@
1
1
  export { Config } from './config';
2
2
  export { Plugin } from './plugin';
3
- export { tsPath } from './ts-node';
3
+ export { tsPath } from './ts-path';
@@ -5,5 +5,5 @@ var config_1 = require("./config");
5
5
  Object.defineProperty(exports, "Config", { enumerable: true, get: function () { return config_1.Config; } });
6
6
  var plugin_1 = require("./plugin");
7
7
  Object.defineProperty(exports, "Plugin", { enumerable: true, get: function () { return plugin_1.Plugin; } });
8
- var ts_node_1 = require("./ts-node");
9
- Object.defineProperty(exports, "tsPath", { enumerable: true, get: function () { return ts_node_1.tsPath; } });
8
+ var ts_path_1 = require("./ts-path");
9
+ Object.defineProperty(exports, "tsPath", { enumerable: true, get: function () { return ts_path_1.tsPath; } });
@@ -1,6 +1,6 @@
1
1
  import { Command } from '../command';
2
2
  import { Manifest } from '../interfaces/manifest';
3
- import { PJSON } from '../interfaces/pjson';
3
+ import { HookOptions, PJSON } from '../interfaces/pjson';
4
4
  import { Plugin as IPlugin, PluginOptions } from '../interfaces/plugin';
5
5
  import { Topic } from '../interfaces/topic';
6
6
  export declare class Plugin implements IPlugin {
@@ -13,7 +13,7 @@ export declare class Plugin implements IPlugin {
13
13
  commandsDir: string | undefined;
14
14
  hasManifest: boolean;
15
15
  hooks: {
16
- [k: string]: string[];
16
+ [key: string]: HookOptions[];
17
17
  };
18
18
  isRoot: boolean;
19
19
  manifest: Manifest;
@@ -29,6 +29,8 @@ export declare class Plugin implements IPlugin {
29
29
  protected warned: boolean;
30
30
  _base: string;
31
31
  protected _debug: (..._: any) => void;
32
+ private commandCache;
33
+ private commandDiscoveryOpts;
32
34
  private flexibleTaxonomy;
33
35
  constructor(options: PluginOptions);
34
36
  get topics(): Topic[];
@@ -42,6 +44,9 @@ export declare class Plugin implements IPlugin {
42
44
  private _manifest;
43
45
  private addErrorScope;
44
46
  private getCommandIDs;
47
+ private getCommandIdsFromPattern;
48
+ private getCommandIdsFromTarget;
45
49
  private getCommandsDir;
50
+ private loadCommandsFromTarget;
46
51
  private warn;
47
52
  }
@@ -7,16 +7,18 @@ exports.Plugin = void 0;
7
7
  const globby_1 = __importDefault(require("globby"));
8
8
  const node_path_1 = require("node:path");
9
9
  const node_util_1 = require("node:util");
10
+ const cache_1 = __importDefault(require("../cache"));
10
11
  const errors_1 = require("../errors");
11
12
  const module_loader_1 = require("../module-loader");
12
13
  const performance_1 = require("../performance");
14
+ const symbols_1 = require("../symbols");
13
15
  const cache_command_1 = require("../util/cache-command");
14
16
  const find_root_1 = require("../util/find-root");
15
17
  const fs_1 = require("../util/fs");
16
18
  const util_1 = require("../util/util");
17
- const ts_node_1 = require("./ts-node");
19
+ const ts_path_1 = require("./ts-path");
18
20
  const util_2 = require("./util");
19
- const _pjson = (0, fs_1.requireJson)(__dirname, '..', '..', 'package.json');
21
+ const _pjson = cache_1.default.getInstance().get('@oclif/core');
20
22
  function topicsToArray(input, base) {
21
23
  if (!input)
22
24
  return [];
@@ -30,13 +32,17 @@ function topicsToArray(input, base) {
30
32
  });
31
33
  }
32
34
  const cachedCommandCanBeUsed = (manifest, id) => Boolean(manifest?.commands[id] && 'isESM' in manifest.commands[id] && 'relativePath' in manifest.commands[id]);
33
- const search = (cmd) => {
35
+ const searchForCommandClass = (cmd) => {
34
36
  if (typeof cmd.run === 'function')
35
37
  return cmd;
36
38
  if (cmd.default && cmd.default.run)
37
39
  return cmd.default;
38
40
  return Object.values(cmd).find((cmd) => typeof cmd.run === 'function');
39
41
  };
42
+ const ensureCommandClass = (cmd) => {
43
+ if (cmd && typeof cmd.run === 'function')
44
+ return cmd;
45
+ };
40
46
  const GLOB_PATTERNS = [
41
47
  '**/*.+(js|cjs|mjs|ts|tsx|mts|cts)',
42
48
  '!**/*.+(d.ts|test.ts|test.js|spec.ts|spec.js|d.mts|d.cts)?(x)',
@@ -47,9 +53,34 @@ function processCommandIds(files) {
47
53
  const topics = p.dir.split('/');
48
54
  const command = p.name !== 'index' && p.name;
49
55
  const id = [...topics, command].filter(Boolean).join(':');
50
- return id === '' ? '.' : id;
56
+ return id === '' ? symbols_1.SINGLE_COMMAND_CLI_SYMBOL : id;
51
57
  });
52
58
  }
59
+ function determineCommandDiscoveryOptions(commandDiscovery, defaultCmdId) {
60
+ if (!commandDiscovery)
61
+ return;
62
+ if (typeof commandDiscovery === 'string' && defaultCmdId) {
63
+ return { strategy: 'single', target: commandDiscovery };
64
+ }
65
+ if (typeof commandDiscovery === 'string') {
66
+ return { globPatterns: GLOB_PATTERNS, strategy: 'pattern', target: commandDiscovery };
67
+ }
68
+ if (!commandDiscovery.target)
69
+ throw new errors_1.CLIError('`oclif.commandDiscovery.target` is required.');
70
+ if (!commandDiscovery.strategy)
71
+ throw new errors_1.CLIError('`oclif.commandDiscovery.strategy` is required.');
72
+ if (commandDiscovery.strategy === 'explicit' && !commandDiscovery.identifier) {
73
+ commandDiscovery.identifier = 'default';
74
+ }
75
+ return commandDiscovery;
76
+ }
77
+ function determineHookOptions(hook) {
78
+ if (typeof hook === 'string')
79
+ return { identifier: 'default', target: hook };
80
+ if (!hook.identifier)
81
+ return { ...hook, identifier: 'default' };
82
+ return hook;
83
+ }
53
84
  class Plugin {
54
85
  options;
55
86
  alias;
@@ -76,6 +107,8 @@ class Plugin {
76
107
  _base = `${_pjson.name}@${_pjson.version}`;
77
108
  // eslint-disable-next-line new-cap
78
109
  _debug = (0, util_2.Debug)();
110
+ commandCache;
111
+ commandDiscoveryOpts;
79
112
  flexibleTaxonomy;
80
113
  constructor(options) {
81
114
  this.options = options;
@@ -89,32 +122,43 @@ class Plugin {
89
122
  plugin: this.name,
90
123
  });
91
124
  const fetch = async () => {
92
- const commandsDir = await this.getCommandsDir();
93
- if (!commandsDir)
94
- return;
95
- let module;
96
- let isESM;
97
- let filePath;
98
- try {
99
- ;
100
- ({ filePath, isESM, module } = cachedCommandCanBeUsed(this.manifest, id)
101
- ? await (0, module_loader_1.loadWithDataFromManifest)(this.manifest.commands[id], this.root)
102
- : await (0, module_loader_1.loadWithData)(this, (0, node_path_1.join)(commandsDir ?? this.pjson.oclif.commands, ...id.split(':'))));
103
- this._debug(isESM ? '(import)' : '(require)', filePath);
125
+ if (this.commandDiscoveryOpts?.strategy === 'pattern') {
126
+ const commandsDir = await this.getCommandsDir();
127
+ if (!commandsDir)
128
+ return;
129
+ let module;
130
+ let isESM;
131
+ let filePath;
132
+ try {
133
+ ;
134
+ ({ filePath, isESM, module } = cachedCommandCanBeUsed(this.manifest, id)
135
+ ? await (0, module_loader_1.loadWithDataFromManifest)(this.manifest.commands[id], this.root)
136
+ : await (0, module_loader_1.loadWithData)(this, (0, node_path_1.join)(commandsDir ?? this.pjson.oclif.commands, ...id.split(':'))));
137
+ this._debug(isESM ? '(import)' : '(require)', filePath);
138
+ }
139
+ catch (error) {
140
+ if (!opts.must && error.code === 'MODULE_NOT_FOUND')
141
+ return;
142
+ throw error;
143
+ }
144
+ const cmd = searchForCommandClass(module);
145
+ if (!cmd)
146
+ return;
147
+ cmd.id = id;
148
+ cmd.plugin = this;
149
+ cmd.isESM = isESM;
150
+ cmd.relativePath = (0, node_path_1.relative)(this.root, filePath || '').split(node_path_1.sep);
151
+ return cmd;
104
152
  }
105
- catch (error) {
106
- if (!opts.must && error.code === 'MODULE_NOT_FOUND')
153
+ if (this.commandDiscoveryOpts?.strategy === 'single' || this.commandDiscoveryOpts?.strategy === 'explicit') {
154
+ const commandCache = await this.loadCommandsFromTarget();
155
+ const cmd = ensureCommandClass(commandCache?.[id]);
156
+ if (!cmd)
107
157
  return;
108
- throw error;
158
+ cmd.id = id;
159
+ cmd.plugin = this;
160
+ return cmd;
109
161
  }
110
- const cmd = search(module);
111
- if (!cmd)
112
- return;
113
- cmd.id = id;
114
- cmd.plugin = this;
115
- cmd.isESM = isESM;
116
- cmd.relativePath = (0, node_path_1.relative)(this.root, filePath || '').split(node_path_1.sep);
117
- return cmd;
118
162
  };
119
163
  const cmd = await fetch();
120
164
  if (!cmd && opts.must)
@@ -153,7 +197,12 @@ class Plugin {
153
197
  else {
154
198
  this.pjson.oclif = this.pjson['cli-engine'] || {};
155
199
  }
156
- this.hooks = Object.fromEntries(Object.entries(this.pjson.oclif.hooks ?? {}).map(([k, v]) => [k, (0, util_1.castArray)(v)]));
200
+ this.hooks = Object.fromEntries(Object.entries(this.pjson.oclif.hooks ?? {}).map(([k, v]) => [
201
+ k,
202
+ (0, util_1.castArray)(v).map((v) => determineHookOptions(v)),
203
+ ]));
204
+ this.commandDiscoveryOpts = determineCommandDiscoveryOptions(this.pjson.oclif?.commands, this.pjson.oclif?.default);
205
+ this._debug('command discovery options', this.commandDiscoveryOpts);
157
206
  this.manifest = await this._manifest();
158
207
  this.commands = Object.entries(this.manifest.commands)
159
208
  .map(([id, c]) => ({
@@ -205,7 +254,13 @@ class Plugin {
205
254
  const manifest = {
206
255
  commands: (await Promise.all(this.commandIDs.map(async (id) => {
207
256
  try {
208
- const cached = await (0, cache_command_1.cacheCommand)(await this.findCommand(id, { must: true }), this, respectNoCacheDefault);
257
+ const found = await this.findCommand(id, { must: true });
258
+ const cached = await (0, cache_command_1.cacheCommand)(found, this, respectNoCacheDefault);
259
+ // Ensure that id is set to the id being processed
260
+ // This is necessary because the id is set by findCommand but if there
261
+ // are multiple instances of a Command, then the id will be set to the
262
+ // last one found.
263
+ cached.id = id;
209
264
  if (this.flexibleTaxonomy) {
210
265
  const permutations = (0, util_2.getCommandIdPermutations)(id);
211
266
  const aliasPermutations = cached.aliases.flatMap((a) => (0, util_2.getCommandIdPermutations)(a));
@@ -246,25 +301,70 @@ class Plugin {
246
301
  return err;
247
302
  }
248
303
  async getCommandIDs() {
249
- const commandsDir = await this.getCommandsDir();
250
- if (!commandsDir)
251
- return [];
252
304
  const marker = performance_1.Performance.mark(performance_1.OCLIF_MARKER_OWNER, `plugin.getCommandIDs#${this.name}`, { plugin: this.name });
253
- this._debug(`loading IDs from ${commandsDir}`);
254
- const files = await (0, globby_1.default)(GLOB_PATTERNS, { cwd: commandsDir });
255
- const ids = processCommandIds(files);
305
+ let ids;
306
+ switch (this.commandDiscoveryOpts?.strategy) {
307
+ case 'explicit': {
308
+ ids = (await this.getCommandIdsFromTarget()) ?? [];
309
+ break;
310
+ }
311
+ case 'pattern': {
312
+ ids = await this.getCommandIdsFromPattern();
313
+ break;
314
+ }
315
+ case 'single': {
316
+ ids = (await this.getCommandIdsFromTarget()) ?? [];
317
+ break;
318
+ }
319
+ default: {
320
+ ids = [];
321
+ }
322
+ }
256
323
  this._debug('found commands', ids);
257
324
  marker?.addDetails({ count: ids.length });
258
325
  marker?.stop();
259
326
  return ids;
260
327
  }
328
+ async getCommandIdsFromPattern() {
329
+ const commandsDir = await this.getCommandsDir();
330
+ if (!commandsDir)
331
+ return [];
332
+ this._debug(`loading IDs from ${commandsDir}`);
333
+ const files = await (0, globby_1.default)(this.commandDiscoveryOpts?.globPatterns ?? GLOB_PATTERNS, { cwd: commandsDir });
334
+ return processCommandIds(files);
335
+ }
336
+ async getCommandIdsFromTarget() {
337
+ const commandsFromExport = await this.loadCommandsFromTarget();
338
+ if (commandsFromExport) {
339
+ return Object.entries((await this.loadCommandsFromTarget()) ?? [])
340
+ .filter(([, cmd]) => ensureCommandClass(cmd))
341
+ .map(([id]) => id);
342
+ }
343
+ }
261
344
  async getCommandsDir() {
262
345
  if (this.commandsDir)
263
346
  return this.commandsDir;
264
- this.commandsDir = await (0, ts_node_1.tsPath)(this.root, this.pjson.oclif.commands, this);
347
+ this.commandsDir = await (0, ts_path_1.tsPath)(this.root, this.commandDiscoveryOpts?.target, this);
265
348
  return this.commandsDir;
266
349
  }
350
+ async loadCommandsFromTarget() {
351
+ if (this.commandCache)
352
+ return this.commandCache;
353
+ if (this.commandDiscoveryOpts?.strategy === 'explicit' && this.commandDiscoveryOpts.target) {
354
+ const filePath = await (0, ts_path_1.tsPath)(this.root, this.commandDiscoveryOpts.target, this);
355
+ const module = await (0, module_loader_1.load)(this, filePath);
356
+ this.commandCache = module[this.commandDiscoveryOpts?.identifier ?? 'default'] ?? {};
357
+ return this.commandCache;
358
+ }
359
+ if (this.commandDiscoveryOpts?.strategy === 'single' && this.commandDiscoveryOpts.target) {
360
+ const filePath = await (0, ts_path_1.tsPath)(this.root, this.commandDiscoveryOpts?.target ?? this.root, this);
361
+ const module = await (0, module_loader_1.load)(this, filePath);
362
+ this.commandCache = { [symbols_1.SINGLE_COMMAND_CLI_SYMBOL]: searchForCommandClass(module) };
363
+ return this.commandCache;
364
+ }
365
+ }
267
366
  warn(err, scope) {
367
+ console.trace();
268
368
  if (this.warned)
269
369
  return;
270
370
  if (typeof err === 'string')
@@ -13,9 +13,50 @@ const read_tsconfig_1 = require("../util/read-tsconfig");
13
13
  const util_1 = require("../util/util");
14
14
  const util_2 = require("./util");
15
15
  // eslint-disable-next-line new-cap
16
- const debug = (0, util_2.Debug)('ts-node');
16
+ const debug = (0, util_2.Debug)('ts-path');
17
17
  exports.TS_CONFIGS = {};
18
18
  const REGISTERED = new Set();
19
+ function determineRuntime() {
20
+ /**
21
+ * Examples:
22
+ * #!/usr/bin/env bun
23
+ * bun bin/run.js
24
+ * bun bin/dev.js
25
+ */
26
+ if (process.execPath.split(node_path_1.sep).includes('bun'))
27
+ return 'bun';
28
+ /**
29
+ * Examples:
30
+ * #!/usr/bin/env node
31
+ * #!/usr/bin/env node --loader ts-node/esm --experimental-specifier-resolution=node --no-warnings
32
+ * node bin/run.js
33
+ * node bin/dev.js
34
+ */
35
+ if (process.execArgv.length === 0)
36
+ return 'node';
37
+ /**
38
+ * Examples:
39
+ * #!/usr/bin/env ts-node
40
+ * #!/usr/bin/env node_modules/.bin/ts-node
41
+ * ts-node bin/run.js
42
+ * ts-node bin/dev.js
43
+ */
44
+ if (process.execArgv[0] === '--require' && process.execArgv[1].split(node_path_1.sep).includes('ts-node'))
45
+ return 'ts-node';
46
+ if (process.execArgv[0].split(node_path_1.sep).includes('ts-node'))
47
+ return 'ts-node';
48
+ /**
49
+ * Examples:
50
+ * #!/usr/bin/env tsx
51
+ * #!/usr/bin/env node_modules/.bin/tsx
52
+ * tsx bin/run.js
53
+ * tsx bin/dev.js
54
+ */
55
+ if (process.execArgv[0] === '--require' && process.execArgv[1].split(node_path_1.sep).includes('tsx'))
56
+ return 'tsx';
57
+ return 'node';
58
+ }
59
+ const RUN_TIME = determineRuntime();
19
60
  function isErrno(error) {
20
61
  return 'code' in error && error.code === 'ENOENT';
21
62
  }
@@ -23,22 +64,23 @@ async function loadTSConfig(root) {
23
64
  try {
24
65
  if (exports.TS_CONFIGS[root])
25
66
  return exports.TS_CONFIGS[root];
26
- exports.TS_CONFIGS[root] = await (0, read_tsconfig_1.readTSConfig)(root);
67
+ const tsconfig = await (0, read_tsconfig_1.readTSConfig)(root);
68
+ if (!tsconfig)
69
+ return;
70
+ debug('tsconfig: %O', tsconfig);
71
+ exports.TS_CONFIGS[root] = tsconfig;
27
72
  return exports.TS_CONFIGS[root];
28
73
  }
29
74
  catch (error) {
30
75
  if (isErrno(error))
31
76
  return;
32
- debug(`Could not parse tsconfig.json. Skipping ts-node registration for ${root}.`);
77
+ debug(`Could not parse tsconfig.json. Skipping typescript path lookup for ${root}.`);
33
78
  (0, errors_1.memoizedWarn)(`Could not parse tsconfig.json for ${root}. Falling back to compiled source.`);
34
79
  }
35
80
  }
36
- async function registerTSNode(root) {
37
- const tsconfig = await loadTSConfig(root);
38
- if (!tsconfig)
39
- return;
81
+ async function registerTSNode(root, tsconfig) {
40
82
  if (REGISTERED.has(root))
41
- return tsconfig;
83
+ return;
42
84
  debug('registering ts-node at', root);
43
85
  const tsNodePath = require.resolve('ts-node', { paths: [root, __dirname] });
44
86
  debug('ts-node path:', tsNodePath);
@@ -87,11 +129,9 @@ async function registerTSNode(root) {
87
129
  skipProject: true,
88
130
  transpileOnly: true,
89
131
  };
132
+ debug('ts-node options: %O', conf);
90
133
  tsNode.register(conf);
91
134
  REGISTERED.add(root);
92
- debug('tsconfig: %O', tsconfig);
93
- debug('ts-node options: %O', conf);
94
- return tsconfig;
95
135
  }
96
136
  /**
97
137
  * Skip ts-node registration for ESM plugins in production.
@@ -121,17 +161,22 @@ function cannotUseTsNode(root, plugin, isProduction) {
121
161
  if (plugin?.moduleType !== 'module' || isProduction)
122
162
  return false;
123
163
  const nodeMajor = Number.parseInt(process.version.replace('v', '').split('.')[0], 10);
124
- const tsNodeExecIsUsed = process.execArgv[0] === '--require' && process.execArgv[1].split(node_path_1.sep).includes(`ts-node`);
125
- return tsNodeExecIsUsed && nodeMajor >= 20;
164
+ return RUN_TIME === 'ts-node' && nodeMajor >= 20;
126
165
  }
127
166
  /**
128
167
  * Determine the path to the source file from the compiled ./lib files
129
168
  */
130
169
  async function determinePath(root, orig) {
131
- const tsconfig = await registerTSNode(root);
170
+ const tsconfig = await loadTSConfig(root);
132
171
  if (!tsconfig)
133
172
  return orig;
134
- debug(`determining path for ${orig}`);
173
+ debug(`Determining path for ${orig}`);
174
+ if (RUN_TIME === 'tsx' || RUN_TIME === 'bun') {
175
+ debug(`Skipping ts-node registration for ${root} because the runtime is: ${RUN_TIME}`);
176
+ }
177
+ else {
178
+ await registerTSNode(root, tsconfig);
179
+ }
135
180
  const { baseUrl, outDir, rootDir, rootDirs } = tsconfig.compilerOptions;
136
181
  const rootDirPath = rootDir ?? (rootDirs ?? [])[0] ?? baseUrl;
137
182
  if (!rootDirPath) {
@@ -171,24 +216,25 @@ async function tsPath(root, orig, plugin) {
171
216
  return orig;
172
217
  orig = orig.startsWith(root) ? orig : (0, node_path_1.join)(root, orig);
173
218
  // NOTE: The order of these checks matter!
174
- if (settings_1.settings.tsnodeEnabled === false) {
175
- debug(`Skipping ts-node registration for ${root} because tsNodeEnabled is explicitly set to false`);
219
+ const enableAutoTranspile = settings_1.settings.enableAutoTranspile ?? settings_1.settings.tsnodeEnabled;
220
+ if (enableAutoTranspile === false) {
221
+ debug(`Skipping typescript path lookup for ${root} because enableAutoTranspile is explicitly set to false`);
176
222
  return orig;
177
223
  }
178
224
  const isProduction = (0, util_1.isProd)();
179
225
  // Do not skip ts-node registration if the plugin is linked
180
- if (settings_1.settings.tsnodeEnabled === undefined && isProduction && plugin?.type !== 'link') {
181
- debug(`Skipping ts-node registration for ${root} because NODE_ENV is NOT "test" or "development"`);
226
+ if (enableAutoTranspile === undefined && isProduction && plugin?.type !== 'link') {
227
+ debug(`Skipping typescript path lookup for ${root} because NODE_ENV is NOT "test" or "development"`);
182
228
  return orig;
183
229
  }
184
230
  if (cannotTranspileEsm(rootPlugin, plugin, isProduction)) {
185
- debug(`Skipping ts-node registration for ${root} because it's an ESM module (NODE_ENV: ${process.env.NODE_ENV}, root plugin module type: ${rootPlugin?.moduleType})`);
231
+ debug(`Skipping typescript path lookup for ${root} because it's an ESM module (NODE_ENV: ${process.env.NODE_ENV}, root plugin module type: ${rootPlugin?.moduleType})`);
186
232
  if (plugin?.type === 'link')
187
233
  (0, errors_1.memoizedWarn)(`${plugin?.name} is a linked ESM module and cannot be auto-transpiled. Existing compiled source will be used instead.`);
188
234
  return orig;
189
235
  }
190
236
  if (cannotUseTsNode(root, plugin, isProduction)) {
191
- debug(`Skipping ts-node registration for ${root} because ts-node is run in node version ${process.version}"`);
237
+ debug(`Skipping typescript path lookup for ${root} because ts-node is run in node version ${process.version}"`);
192
238
  (0, errors_1.memoizedWarn)(`ts-node executable cannot transpile ESM in Node 20. Existing compiled source will be used instead. See https://github.com/oclif/core/issues/817.`);
193
239
  return orig;
194
240
  }
@@ -77,7 +77,7 @@ class DocOpts {
77
77
  return new DocOpts(cmd).toString();
78
78
  }
79
79
  toString() {
80
- const opts = this.cmd.id === '.' || this.cmd.id === '' ? [] : ['<%= command.id %>'];
80
+ const opts = ['<%= command.id %>'];
81
81
  if (this.cmd.args) {
82
82
  const a = Object.values((0, ensure_arg_object_1.ensureArgObject)(this.cmd.args)).map((arg) => arg.required ? arg.name.toUpperCase() : `[${arg.name.toUpperCase()}]`) || [];
83
83
  opts.push(...a);
package/lib/help/index.js CHANGED
@@ -10,6 +10,7 @@ const theme_1 = require("../cli-ux/theme");
10
10
  const write_1 = __importDefault(require("../cli-ux/write"));
11
11
  const errors_1 = require("../errors");
12
12
  const module_loader_1 = require("../module-loader");
13
+ const symbols_1 = require("../symbols");
13
14
  const cache_default_value_1 = require("../util/cache-default-value");
14
15
  const ids_1 = require("../util/ids");
15
16
  const util_1 = require("../util/util");
@@ -189,8 +190,8 @@ class Help extends HelpBase {
189
190
  argv = (0, util_2.standardizeIDFromArgv)(argv, this.config);
190
191
  const subject = getHelpSubject(argv, this.config);
191
192
  if (!subject) {
192
- if (this.config.pjson.oclif.default) {
193
- const rootCmd = this.config.findCommand(this.config.pjson.oclif.default);
193
+ if (this.config.isSingleCommandCLI) {
194
+ const rootCmd = this.config.findCommand(symbols_1.SINGLE_COMMAND_CLI_SYMBOL);
194
195
  if (rootCmd) {
195
196
  await this.showCommandHelp(rootCmd);
196
197
  return;
@@ -201,6 +202,12 @@ class Help extends HelpBase {
201
202
  }
202
203
  const command = this.config.findCommand(subject);
203
204
  if (command) {
205
+ if (command.id === symbols_1.SINGLE_COMMAND_CLI_SYMBOL) {
206
+ // If the command is the root command of a single command CLI,
207
+ // then set the command id to an empty string to prevent the
208
+ // the SINGLE_COMMAND_CLI_SYMBOL from being displayed in the help output.
209
+ command.id = '';
210
+ }
204
211
  if (command.hasDynamicHelp && command.pluginType !== 'jit') {
205
212
  const loaded = await command.load();
206
213
  for (const [name, flag] of Object.entries(loaded.flags ?? {})) {
@@ -96,6 +96,7 @@ export interface Config {
96
96
  * example: /home/myuser
97
97
  */
98
98
  readonly home: string;
99
+ readonly isSingleCommandCLI: boolean;
99
100
  readonly name: string;
100
101
  /**
101
102
  * npm registry to use for installing plugins
@@ -16,6 +16,86 @@ export interface PJSON {
16
16
  };
17
17
  version: string;
18
18
  }
19
+ export type CommandDiscovery = {
20
+ /**
21
+ * The strategy to use for loading commands.
22
+ *
23
+ * - `pattern` will use glob patterns to find command files in the specified `target`.
24
+ * - `explicit` will use `import` (or `require` for CJS) to load the commands from the
25
+ * specified `target`.
26
+ * - `single` will use the `target` which should export a command class. This is for CLIs that
27
+ * only have a single command.
28
+ *
29
+ * In both cases, the `oclif.manifest.json` file will be used to find the commands if it exists.
30
+ */
31
+ strategy: 'pattern' | 'explicit' | 'single';
32
+ /**
33
+ * If the `strategy` is `pattern`, this is the **directory** to use to find command files.
34
+ *
35
+ * If the `strategy` is `explicit`, this is the **file** that exports the commands.
36
+ * - This export must be an object with keys that are the command names and values that are the command classes.
37
+ * - Unless `identifier` is specified, the default export will be used.
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * // in src/commands.ts
42
+ * import {Command} from '@oclif/core'
43
+ * import Hello from './commands/hello/index.js'
44
+ * import HelloWorld from './commands/hello/world.js'
45
+ *
46
+ * export default {
47
+ * hello: Hello,
48
+ * 'hello:world': HelloWorld,
49
+ * } satisfies Record<string, Command.Class>
50
+ * ```
51
+ */
52
+ target: string;
53
+ /**
54
+ * The glob patterns to use to find command files when no `oclif.manifest.json` is present.
55
+ * This is only used when `strategy` is `pattern`.
56
+ */
57
+ globPatterns?: string[];
58
+ /**
59
+ * The name of the export to used when loading the command object from the `target` file. Only
60
+ * used when `strategy` is `explicit`. Defaults to `default`.
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * // in src/commands.ts
65
+ * import {Command} from '@oclif/core'
66
+ * import Hello from './commands/hello/index.js'
67
+ * import HelloWorld from './commands/hello/world.js'
68
+ *
69
+ * export const MY_COMMANDS = {
70
+ * hello: Hello,
71
+ * 'hello:world': HelloWorld,
72
+ * } satisfies Record<string, Command.Class>
73
+ * ```
74
+ *
75
+ * In the package.json:
76
+ * ```json
77
+ * {
78
+ * "oclif": {
79
+ * "commands": {
80
+ * "strategy": "explicit",
81
+ * "target": "./dist/index.js",
82
+ * "identifier": "MY_COMMANDS"
83
+ * }
84
+ * }
85
+ * ```
86
+ */
87
+ identifier?: string;
88
+ };
89
+ export type HookOptions = {
90
+ /**
91
+ * The file path containing hook.
92
+ */
93
+ target: string;
94
+ /**
95
+ * The name of the export to use when loading the hook function from the `target` file. Defaults to `default`.
96
+ */
97
+ identifier: string;
98
+ };
19
99
  export declare namespace PJSON {
20
100
  interface Plugin extends PJSON {
21
101
  name: string;
@@ -25,7 +105,13 @@ export declare namespace PJSON {
25
105
  aliases?: {
26
106
  [name: string]: null | string;
27
107
  };
28
- commands?: string;
108
+ commands?: string | CommandDiscovery;
109
+ /**
110
+ * Default command id when no command is found. This is used to support single command CLIs.
111
+ * Only supported value is "."
112
+ *
113
+ * @deprecated Use `commands.strategy: 'single'` instead.
114
+ */
29
115
  default?: string;
30
116
  description?: string;
31
117
  devPlugins?: string[];
@@ -42,7 +128,7 @@ export declare namespace PJSON {
42
128
  helpClass?: string;
43
129
  helpOptions?: HelpOptions;
44
130
  hooks?: {
45
- [name: string]: string | string[];
131
+ [name: string]: string | string[] | HookOptions | HookOptions[];
46
132
  };
47
133
  jitPlugins?: Record<string, string>;
48
134
  macos?: {
@@ -1,5 +1,5 @@
1
1
  import { Command } from '../command';
2
- import { PJSON } from './pjson';
2
+ import { HookOptions, PJSON } from './pjson';
3
3
  import { Topic } from './topic';
4
4
  export interface PluginOptions {
5
5
  children?: Plugin[];
@@ -44,7 +44,7 @@ export interface Plugin {
44
44
  }): Promise<Command.Class> | undefined;
45
45
  readonly hasManifest: boolean;
46
46
  hooks: {
47
- [k: string]: string[];
47
+ [key: string]: HookOptions[];
48
48
  };
49
49
  /**
50
50
  * True if the plugin is the root plugin.
package/lib/main.js CHANGED
@@ -6,9 +6,10 @@ const cli_ux_1 = require("./cli-ux");
6
6
  const config_1 = require("./config");
7
7
  const help_1 = require("./help");
8
8
  const performance_1 = require("./performance");
9
+ const symbols_1 = require("./symbols");
9
10
  const debug = require('debug')('oclif:main');
10
11
  const helpAddition = (argv, config) => {
11
- if (argv.length === 0 && !config.pjson.oclif.default)
12
+ if (argv.length === 0 && !config.isSingleCommandCLI)
12
13
  return true;
13
14
  const mergedHelpFlags = (0, help_1.getHelpFlagAdditions)(config);
14
15
  for (const arg of argv) {
@@ -52,7 +53,11 @@ async function run(argv, options) {
52
53
  options = (0, node_url_1.fileURLToPath)(options);
53
54
  }
54
55
  const config = await config_1.Config.load(options ?? require.main?.filename ?? __dirname);
55
- let [id, ...argvSlice] = (0, help_1.normalizeArgv)(config, argv);
56
+ // If this is a single command CLI, then insert the SINGLE_COMMAND_CLI_SYMBOL into the argv array to serve as the command id.
57
+ if (config.isSingleCommandCLI) {
58
+ argv = [symbols_1.SINGLE_COMMAND_CLI_SYMBOL, ...argv];
59
+ }
60
+ const [id, ...argvSlice] = (0, help_1.normalizeArgv)(config, argv);
56
61
  // run init hook
57
62
  await config.runHook('init', { argv: argvSlice, id });
58
63
  // display version if applicable
@@ -76,17 +81,8 @@ async function run(argv, options) {
76
81
  await collectPerf();
77
82
  return;
78
83
  }
79
- if (config.pjson.oclif.default) {
80
- id = config.pjson.oclif.default;
81
- argvSlice = argv;
82
- }
83
84
  }
84
85
  initMarker?.stop();
85
- // If the the default command is '.' (signifying that the CLI is a single command CLI) and '.' is provided
86
- // as an argument, we need to add back the '.' to argv since it was stripped out earlier as part of the
87
- // command id.
88
- if (config.pjson.oclif.default === '.' && id === '.' && argv[0] === '.')
89
- argvSlice = ['.', ...argvSlice];
90
86
  try {
91
87
  return await config.runCommand(id, argvSlice, cmd);
92
88
  }
@@ -4,7 +4,7 @@ exports.isPathModule = exports.loadWithDataFromManifest = exports.loadWithData =
4
4
  const node_fs_1 = require("node:fs");
5
5
  const node_path_1 = require("node:path");
6
6
  const node_url_1 = require("node:url");
7
- const ts_node_1 = require("./config/ts-node");
7
+ const ts_path_1 = require("./config/ts-path");
8
8
  const errors_1 = require("./errors");
9
9
  const fs_1 = require("./util/fs");
10
10
  const getPackageType = require('get-package-type');
@@ -164,7 +164,7 @@ async function resolvePath(config, modulePath) {
164
164
  }
165
165
  catch {
166
166
  filePath =
167
- (isPlugin(config) ? await (0, ts_node_1.tsPath)(config.root, modulePath, config) : await (0, ts_node_1.tsPath)(config.root, modulePath)) ??
167
+ (isPlugin(config) ? await (0, ts_path_1.tsPath)(config.root, modulePath, config) : await (0, ts_path_1.tsPath)(config.root, modulePath)) ??
168
168
  modulePath;
169
169
  let fileExists = false;
170
170
  let isDirectory = false;
package/lib/settings.d.ts CHANGED
@@ -9,7 +9,7 @@ export type Settings = {
9
9
  /**
10
10
  * Show additional debug output without DEBUG. Mainly shows stackstraces.
11
11
  *
12
- * Useful to set in the ./bin/dev script.
12
+ * Useful to set in the ./bin/dev.js script.
13
13
  * oclif.settings.debug = true;
14
14
  */
15
15
  debug?: boolean;
@@ -25,15 +25,18 @@ export type Settings = {
25
25
  */
26
26
  performanceEnabled?: boolean;
27
27
  /**
28
- * Try to use ts-node to load typescript source files instead of
29
- * javascript files.
28
+ * Try to use ts-node to load typescript source files instead of javascript files.
29
+ * Defaults to true in development and test environments (e.g. using bin/dev.js or
30
+ * NODE_ENV=development or NODE_ENV=test).
30
31
  *
31
- * NOTE: This requires registering ts-node first.
32
- * require('ts-node').register();
33
- *
34
- * Environment Variable:
35
- * NODE_ENV=development
32
+ * @deprecated use enableAutoTranspile instead.
36
33
  */
37
34
  tsnodeEnabled?: boolean;
35
+ /**
36
+ * Enable automatic transpilation of TypeScript files to JavaScript.
37
+ *
38
+ * Defaults to true in development and test environments (e.g. using bin/dev.js or NODE_ENV=development or NODE_ENV=test).
39
+ */
40
+ enableAutoTranspile?: boolean;
38
41
  };
39
42
  export declare const settings: Settings;
@@ -0,0 +1 @@
1
+ export declare const SINGLE_COMMAND_CLI_SYMBOL: string;
package/lib/symbols.js ADDED
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SINGLE_COMMAND_CLI_SYMBOL = void 0;
4
+ exports.SINGLE_COMMAND_CLI_SYMBOL = Symbol('SINGLE_COMMAND_CLI').toString();
package/lib/util/fs.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- export declare function requireJson<T>(...pathParts: string[]): T;
2
1
  /**
3
2
  * Parser for Args.directory and Flags.directory. Checks that the provided path
4
3
  * exists and is a directory.
package/lib/util/fs.js CHANGED
@@ -1,13 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.existsSync = exports.safeReadJson = exports.readJsonSync = exports.readJson = exports.fileExists = exports.dirExists = exports.requireJson = void 0;
3
+ exports.existsSync = exports.safeReadJson = exports.readJsonSync = exports.readJson = exports.fileExists = exports.dirExists = void 0;
4
4
  const node_fs_1 = require("node:fs");
5
5
  const promises_1 = require("node:fs/promises");
6
- const node_path_1 = require("node:path");
7
- function requireJson(...pathParts) {
8
- return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(...pathParts), 'utf8'));
9
- }
10
- exports.requireJson = requireJson;
11
6
  /**
12
7
  * Parser for Args.directory and Flags.directory. Checks that the provided path
13
8
  * exists and is a directory.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oclif/core",
3
3
  "description": "base library for oclif CLIs",
4
- "version": "3.19.7",
4
+ "version": "3.20.1-dev.0",
5
5
  "author": "Salesforce",
6
6
  "bugs": "https://github.com/oclif/core/issues",
7
7
  "dependencies": {
@@ -114,7 +114,6 @@
114
114
  "access": "public"
115
115
  },
116
116
  "scripts": {
117
- "build:dev": "shx rm -rf lib && tsc --sourceMap",
118
117
  "build": "shx rm -rf lib && tsc",
119
118
  "commitlint": "commitlint",
120
119
  "compile": "tsc",
@@ -127,9 +126,8 @@
127
126
  "test:circular-deps": "madge lib/ -c",
128
127
  "test:debug": "nyc mocha --debug-brk --inspect \"test/**/*.test.ts\"",
129
128
  "test:integration": "mocha --forbid-only \"test/**/*.integration.ts\" --parallel --timeout 1200000",
130
- "test:esm-cjs": "cross-env DEBUG=integration:* ts-node test/integration/esm-cjs.ts",
129
+ "test:interoperability": "cross-env DEBUG=integration:* ts-node test/integration/interop.ts",
131
130
  "test:perf": "ts-node test/perf/parser.perf.ts",
132
- "test:dev": "nyc mocha \"test/**/*.test.ts\"",
133
131
  "test": "nyc mocha --forbid-only \"test/**/*.test.ts\""
134
132
  },
135
133
  "types": "lib/index.d.ts"
File without changes