diff --git a/package.json b/package.json index 0de07eb44..262384ee6 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "clean-stack": "^3.0.1", "cli-progress": "^3.12.0", "debug": "^4.3.4", - "ejs": "^3.1.8", + "ejs": "^3.1.9", "get-package-type": "^0.1.0", "globby": "^11.1.0", "hyperlinker": "^1.0.0", @@ -39,11 +39,11 @@ "@oclif/test": "^3.0.1", "@types/ansi-styles": "^3.2.1", "@types/benchmark": "^2.1.2", - "@types/chai": "^4.3.4", + "@types/chai": "^4.3.8", "@types/chai-as-promised": "^7.1.5", "@types/clean-stack": "^2.1.1", "@types/cli-progress": "^3.11.0", - "@types/ejs": "^3.1.2", + "@types/ejs": "^3.1.3", "@types/indent-string": "^4.0.1", "@types/js-yaml": "^3.12.7", "@types/mocha": "^10.0.2", @@ -55,7 +55,7 @@ "@types/wordwrap": "^1.0.1", "@types/wrap-ansi": "^3.0.0", "benchmark": "^2.1.4", - "chai": "^4.3.7", + "chai": "^4.3.10", "chai-as-promised": "^7.1.1", "commitlint": "^17.7.2", "cross-env": "^7.0.3", diff --git a/src/cli-ux/prompt.ts b/src/cli-ux/prompt.ts index 1e21bd99b..b72d232b6 100644 --- a/src/cli-ux/prompt.ts +++ b/src/cli-ux/prompt.ts @@ -28,7 +28,7 @@ interface IPromptConfig { function normal(options: IPromptConfig, retries = 100): Promise { if (retries < 0) throw new Error('no input') return new Promise((resolve, reject) => { - let timer: NodeJS.Timer + let timer: NodeJS.Timeout if (options.timeout) { timer = setTimeout(() => { process.stdin.pause() diff --git a/src/config/cache.ts b/src/config/cache.ts new file mode 100644 index 000000000..c91bc57d4 --- /dev/null +++ b/src/config/cache.ts @@ -0,0 +1,26 @@ +import {Plugin} from '../interfaces' + +type CacheContents = { + rootPlugin: Plugin +} + +type ValueOf = T[keyof T] + +/** + * A simple cache for storing values that need to be accessed globally. + */ +export default class Cache extends Map> { + static instance: Cache + static getInstance(): Cache { + if (!Cache.instance) { + Cache.instance = new Cache() + } + + return Cache.instance + } + + public get(key: 'rootPlugin'): Plugin | undefined + public get(key: keyof CacheContents): ValueOf | undefined { + return super.get(key) + } +} diff --git a/src/config/config.ts b/src/config/config.ts index c1409e273..8a00033a5 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -18,6 +18,7 @@ import {settings} from '../settings' import {requireJson} from '../util/fs' import {getHomeDir, getPlatform} from '../util/os' import {compact, isProd} from '../util/util' +import Cache from './cache' import PluginLoader from './plugin-loader' import {Debug, collectUsableIds, getCommandIdPermutations} from './util' @@ -104,13 +105,14 @@ export class Config implements IConfig { private _commands = new Map() - private static _rootPlugin: IPlugin - private _topics = new Map() private commandPermutations = new Permutations() + private pluginLoader!: PluginLoader + private rootPlugin!: IPlugin + private topicPermutations = new Permutations() constructor(public options: Options) {} @@ -149,7 +151,7 @@ export class Config implements IConfig { } static get rootPlugin(): IPlugin | undefined { - return Config._rootPlugin + return this.rootPlugin } public get commandIDs(): string[] { @@ -281,18 +283,22 @@ export class Config implements IConfig { (settings.performanceEnabled === undefined ? this.options.enablePerf : settings.performanceEnabled) ?? false const marker = Performance.mark(OCLIF_MARKER_OWNER, 'config.load') this.pluginLoader = new PluginLoader({plugins: this.options.plugins, root: this.options.root}) - Config._rootPlugin = await this.pluginLoader.loadRoot() + this.rootPlugin = await this.pluginLoader.loadRoot() + + // Cache the root plugin so that we can reference it later when determining if + // we should skip ts-node registration for an ESM plugin. + Cache.getInstance().set('rootPlugin', this.rootPlugin) - this.root = Config._rootPlugin.root - this.pjson = Config._rootPlugin.pjson + this.root = this.rootPlugin.root + this.pjson = this.rootPlugin.pjson - this.plugins.set(Config._rootPlugin.name, Config._rootPlugin) - this.root = Config._rootPlugin.root - this.pjson = Config._rootPlugin.pjson + this.plugins.set(this.rootPlugin.name, this.rootPlugin) + this.root = this.rootPlugin.root + this.pjson = this.rootPlugin.pjson this.name = this.pjson.name this.version = this.options.version || this.pjson.version || '0.0.0' this.channel = this.options.channel || channelFromVersion(this.version) - this.valid = Config._rootPlugin.valid + this.valid = this.rootPlugin.valid this.arch = arch() === 'ia32' ? 'x86' : (arch() as any) this.platform = WSL ? 'wsl' : getPlatform() @@ -364,7 +370,7 @@ export class Config implements IConfig { dataDir: this.dataDir, devPlugins: this.options.devPlugins, force: opts?.force ?? false, - rootPlugin: Config._rootPlugin, + rootPlugin: this.rootPlugin, userPlugins: this.options.userPlugins, }) diff --git a/src/config/plugin.ts b/src/config/plugin.ts index 340f42d44..12be13015 100644 --- a/src/config/plugin.ts +++ b/src/config/plugin.ts @@ -171,7 +171,10 @@ export class Plugin implements IPlugin { const marker = Performance.mark(OCLIF_MARKER_OWNER, `plugin.commandIDs#${this.name}`, {plugin: this.name}) this._debug(`loading IDs from ${this.commandsDir}`) - const patterns = ['**/*.+(js|cjs|mjs|ts|tsx)', '!**/*.+(d.ts|test.ts|test.js|spec.ts|spec.js)?(x)'] + const patterns = [ + '**/*.+(js|cjs|mjs|ts|tsx|mts|cts)', + '!**/*.+(d.ts|test.ts|test.js|spec.ts|spec.js|d.mts|d.cts)?(x)', + ] const ids = sync(patterns, {cwd: this.commandsDir}).map((file) => { const p = parse(file) const topics = p.dir.split('/') diff --git a/src/config/ts-node.ts b/src/config/ts-node.ts index e78546a6d..7a0328350 100644 --- a/src/config/ts-node.ts +++ b/src/config/ts-node.ts @@ -7,6 +7,7 @@ import {Plugin, TSConfig} from '../interfaces' import {settings} from '../settings' import {readJsonSync} from '../util/fs' import {isProd} from '../util/util' +import Cache from './cache' import {Debug} from './util' // eslint-disable-next-line new-cap @@ -14,11 +15,6 @@ const debug = Debug('ts-node') export const TS_CONFIGS: Record = {} const REGISTERED = new Set() -/** - * Cache the root plugin so that we can reference it later when determining if - * we should skip ts-node registration for an ESM plugin. - */ -let ROOT_PLUGIN: Plugin | undefined function loadTSConfig(root: string): TSConfig | undefined { if (TS_CONFIGS[root]) return TS_CONFIGS[root] @@ -111,7 +107,7 @@ function registerTSNode(root: string): TSConfig | undefined { tsNode.register(conf) REGISTERED.add(root) - + debug('%O', tsconfig) return tsconfig } @@ -127,8 +123,12 @@ function registerTSNode(root: string): TSConfig | undefined { * We still register ts-node for ESM plugins when NODE_ENV is "test" or "development" and root plugin is also ESM * since that allows plugins to be auto-transpiled when developing locally using `bin/dev.js`. */ -function cannotTranspileEsm(root: string, plugin: Plugin | undefined, isProduction: boolean): boolean { - return (isProduction || ROOT_PLUGIN?.moduleType === 'commonjs') && plugin?.moduleType === 'module' +function cannotTranspileEsm( + rootPlugin: Plugin | undefined, + plugin: Plugin | undefined, + isProduction: boolean, +): boolean { + return (isProduction || rootPlugin?.moduleType === 'commonjs') && plugin?.moduleType === 'module' } /** @@ -154,9 +154,19 @@ function cannotUseTsNode(root: string, plugin: Plugin | undefined, isProduction: function determinePath(root: string, orig: string): string { const tsconfig = registerTSNode(root) if (!tsconfig) return orig - const {outDir, rootDir, rootDirs} = tsconfig.compilerOptions - const rootDirPath = rootDir || (rootDirs || [])[0] - if (!rootDirPath || !outDir) return orig + debug(`determining path for ${orig}`) + const {baseUrl, outDir, rootDir, rootDirs} = tsconfig.compilerOptions + const rootDirPath = rootDir ?? (rootDirs ?? [])[0] ?? baseUrl + if (!rootDirPath) { + debug(`no rootDir, rootDirs, or baseUrl specified in tsconfig.json. Returning default path ${orig}`) + return orig + } + + if (!outDir) { + debug(`no outDir specified in tsconfig.json. Returning default path ${orig}`) + return orig + } + // rewrite path from ./lib/foo to ./src/foo const lib = join(root, outDir) // ./lib const src = join(root, rootDirPath) // ./src @@ -168,7 +178,18 @@ function determinePath(root: string, orig: string): string { // For hooks, it might point to a module, not a file. Something like "./hooks/myhook" // That file doesn't exist, and the real file is "./hooks/myhook.ts" // In that case we attempt to resolve to the filename. If it fails it will revert back to the lib path - if (existsSync(out) || existsSync(out + '.ts')) return out + + debug(`lib dir: ${lib}`) + debug(`src dir: ${src}`) + debug(`src commands dir: ${out}`) + if (existsSync(out) || existsSync(out + '.ts')) { + debug(`Found source file for ${orig} at ${out}`) + return out + } + + debug(`No source file found. Returning default path ${orig}`) + if (!isProd()) memoizedWarn(`Could not find source for ${orig} based on tsconfig. Defaulting to compiled source.`) + return orig } @@ -180,7 +201,7 @@ function determinePath(root: string, orig: string): string { export function tsPath(root: string, orig: string, plugin: Plugin): string export function tsPath(root: string, orig: string | undefined, plugin?: Plugin): string | undefined export function tsPath(root: string, orig: string | undefined, plugin?: Plugin): string | undefined { - if (plugin?.isRoot) ROOT_PLUGIN = plugin + const rootPlugin = Cache.getInstance().get('rootPlugin') if (!orig) return orig orig = orig.startsWith(root) ? orig : join(root, orig) @@ -194,9 +215,9 @@ export function tsPath(root: string, orig: string | undefined, plugin?: Plugin): const isProduction = isProd() - if (cannotTranspileEsm(root, plugin, isProduction)) { + if (cannotTranspileEsm(rootPlugin, plugin, isProduction)) { debug( - `Skipping ts-node registration for ${root} because it's an ESM module (NODE_ENV: ${process.env.NODE_ENV}, root plugin module type: ${ROOT_PLUGIN?.moduleType})))`, + `Skipping ts-node registration for ${root} because it's an ESM module (NODE_ENV: ${process.env.NODE_ENV}, root plugin module type: ${rootPlugin?.moduleType})))`, ) if (plugin?.type === 'link') memoizedWarn( diff --git a/src/flags.ts b/src/flags.ts index 15183df86..9f263c3b3 100644 --- a/src/flags.ts +++ b/src/flags.ts @@ -1,4 +1,3 @@ -/* eslint-disable valid-jsdoc */ import {URL} from 'node:url' import {CLIError} from './errors' diff --git a/src/interfaces/ts-config.ts b/src/interfaces/ts-config.ts index 80ddd1692..44aea6320 100644 --- a/src/interfaces/ts-config.ts +++ b/src/interfaces/ts-config.ts @@ -1,5 +1,6 @@ export interface TSConfig { compilerOptions: { + baseUrl?: string emitDecoratorMetadata?: boolean esModuleInterop?: boolean experimentalDecorators?: boolean diff --git a/src/module-loader.ts b/src/module-loader.ts index 526719dff..7fecc887f 100644 --- a/src/module-loader.ts +++ b/src/module-loader.ts @@ -13,7 +13,7 @@ const getPackageType = require('get-package-type') * Defines file extension resolution when source files do not have an extension. */ // eslint-disable-next-line camelcase -const s_EXTENSIONS: string[] = ['.ts', '.js', '.mjs', '.cjs'] +const s_EXTENSIONS: string[] = ['.ts', '.js', '.mjs', '.cjs', '.mts', '.cts'] const isPlugin = (config: IConfig | IPlugin): config is IPlugin => (config).type !== undefined diff --git a/src/util/cache-command.ts b/src/util/cache-command.ts index a49f92d5c..81a5b9e5b 100644 --- a/src/util/cache-command.ts +++ b/src/util/cache-command.ts @@ -88,11 +88,10 @@ export async function cacheCommand( // @ts-expect-error because v2 commands have base flags stored in _baseFlags const uncachedBaseFlags = cmd.baseFlags ?? cmd._baseFlags - const flags = await cacheFlags( - aggregateFlags(uncachedFlags, uncachedBaseFlags, cmd.enableJsonFlag), - respectNoCacheDefault, - ) - const args = await cacheArgs(ensureArgObject(cmd.args), respectNoCacheDefault) + const [flags, args] = await Promise.all([ + await cacheFlags(aggregateFlags(uncachedFlags, uncachedBaseFlags, cmd.enableJsonFlag), respectNoCacheDefault), + await cacheArgs(ensureArgObject(cmd.args), respectNoCacheDefault), + ]) const stdProperties = { aliases: cmd.aliases || [], diff --git a/test/command/main.test.ts b/test/command/main.test.ts index 9176adabc..581d95f3a 100644 --- a/test/command/main.test.ts +++ b/test/command/main.test.ts @@ -1,13 +1,12 @@ import {expect} from 'chai' import {resolve} from 'node:path' import {SinonSandbox, SinonStub, createSandbox} from 'sinon' +import stripAnsi from 'strip-ansi' import {Interfaces, stdout} from '../../src/index' import {run} from '../../src/main' import {requireJson} from '../../src/util/fs' -import stripAnsi = require('strip-ansi') - const pjson = requireJson(__dirname, '..', '..', 'package.json') const version = `@oclif/core/${pjson.version} ${process.platform}-${process.arch} node-${process.version}` diff --git a/test/integration/util.ts b/test/integration/util.ts index 005f92cee..fc2c3066e 100644 --- a/test/integration/util.ts +++ b/test/integration/util.ts @@ -113,7 +113,6 @@ export class Executor { } } -// eslint-disable-next-line valid-jsdoc /** * Setup for integration tests. * diff --git a/yarn.lock b/yarn.lock index 2b900ea2b..e61750403 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1017,11 +1017,16 @@ dependencies: "@types/chai" "*" -"@types/chai@*", "@types/chai@^4.3.4": +"@types/chai@*": version "4.3.4" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.4.tgz#e913e8175db8307d78b4e8fa690408ba6b65dee4" integrity sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw== +"@types/chai@^4.3.8": + version "4.3.8" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.8.tgz#aa200a264a3bc78ccfc1718eedbd3b9d5a591270" + integrity sha512-yW/qTM4mRBBcsA9Xw9FbcImYtFPY7sgr+G/O5RDYVmxiy9a+pE5FyoFUi8JYCZY5nicj8atrr1pcfPiYpeNGOA== + "@types/clean-stack@^2.1.1": version "2.1.1" resolved "https://registry.yarnpkg.com/@types/clean-stack/-/clean-stack-2.1.1.tgz#7dd5430b733d088263b01653ea0ec39af9fe67f1" @@ -1041,10 +1046,10 @@ resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== -"@types/ejs@^3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@types/ejs/-/ejs-3.1.2.tgz#75d277b030bc11b3be38c807e10071f45ebc78d9" - integrity sha512-ZmiaE3wglXVWBM9fyVC17aGPkLo/UgaOjEiI2FXQfyczrCefORPxIe+2dVmnmk3zkVIbizjrlQzmPGhSYGXG5g== +"@types/ejs@^3.1.3": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@types/ejs/-/ejs-3.1.3.tgz#ad91d1dd6e24fb60bbf96c534bce58b95eef9b57" + integrity sha512-mv5T/JI/bu+pbfz1o+TLl1NF0NIBbjS0Vl6Ppz1YY9DkXfzZT0lelXpfS5i3ZS3U/p90it7uERQpBvLYoK8e4A== "@types/eslint@^7.2.13": version "7.29.0" @@ -1892,19 +1897,6 @@ chai@^4.3.10: pathval "^1.1.1" type-detect "^4.0.8" -chai@^4.3.7: - version "4.3.7" - resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.7.tgz#ec63f6df01829088e8bf55fca839bcd464a8ec51" - integrity sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A== - dependencies: - assertion-error "^1.1.0" - check-error "^1.0.2" - deep-eql "^4.1.2" - get-func-name "^2.0.0" - loupe "^2.3.1" - pathval "^1.1.1" - type-detect "^4.0.5" - chalk@5.3.0, chalk@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" @@ -2316,13 +2308,6 @@ decamelize@^4.0.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== -deep-eql@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.2.tgz#270ceb902f87724077e6f6449aed81463f42fc1c" - integrity sha512-gT18+YW4CcW/DBNTwAmqTtkJh7f9qqScu2qFVlx7kCoeY9tlBu9cUcr7+I+Z/noG8INehS3xQgLpTtd/QUTn4w== - dependencies: - type-detect "^4.0.0" - deep-eql@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.3.tgz#7c7775513092f7df98d8df9996dd085eb668cc6d" @@ -2593,6 +2578,13 @@ ejs@^3.1.8: dependencies: jake "^10.8.5" +ejs@^3.1.9: + version "3.1.9" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361" + integrity sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ== + dependencies: + jake "^10.8.5" + electron-to-chromium@^1.4.530: version "1.4.531" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.531.tgz#22966d894c4680726c17cf2908ee82ff5d26ac25" @@ -4615,13 +4607,6 @@ log-update@^5.0.1: strip-ansi "^7.0.1" wrap-ansi "^8.0.1" -loupe@^2.3.1: - version "2.3.4" - resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.4.tgz#7e0b9bffc76f148f9be769cb1321d3dcf3cb25f3" - integrity sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ== - dependencies: - get-func-name "^2.0.0" - loupe@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.6.tgz#76e4af498103c532d1ecc9be102036a21f787b53" @@ -6824,7 +6809,7 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5, type-detect@^4.0.8: +type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==