From 59145ee40ec33f70aec3fb79c1431de19e7fb041 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Thu, 2 Nov 2023 07:24:45 -0700 Subject: [PATCH] feat: read extended tsconfigs (#845) * feat: add swc option if possible * chore: better debugs * feat: use tsconfck for resolving extended tsconfigs * test: plugins e2e * feat: improve implementation * test: remove tsPathSync test * feat: pass entire tsconfig to ts-node * fix: handle invalid json * chore: clean up * fix: return tsconfig for previously registered * test: code coverage * test: handle line breaks * test: handle line breaks * test: use different text for warning assertion * chore: code review * chore: code review * fix: find-root debug logs * fix: merge ts-node opts in reverse --- package.json | 2 + src/config/plugin.ts | 87 ++++++++++++++++------------ src/config/ts-node.ts | 88 ++++++++++++++--------------- src/errors/index.ts | 5 +- src/interfaces/plugin.ts | 1 + src/interfaces/ts-config.ts | 2 + src/module-loader.ts | 9 +-- src/util/find-root.ts | 32 +++++++++-- src/util/fs.ts | 3 - test/config/config.flexible.test.ts | 2 + test/config/config.test.ts | 2 + test/config/ts-node.test.ts | 68 +++++++++++++++------- test/config/typescript.test.ts | 3 +- test/integration/esm-cjs.ts | 5 +- test/integration/plugins.e2e.ts | 1 + test/integration/util.ts | 10 +++- yarn.lock | 17 ++++++ 17 files changed, 217 insertions(+), 120 deletions(-) diff --git a/package.json b/package.json index 8207c908b..0b8f13bd2 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "strip-ansi": "^6.0.1", "supports-color": "^8.1.1", "supports-hyperlinks": "^2.2.0", + "tsconfck": "^3.0.0", "widest-line": "^3.1.0", "wordwrap": "^1.0.0", "wrap-ansi": "^7.0.0" @@ -43,6 +44,7 @@ "@types/chai-as-promised": "^7.1.5", "@types/clean-stack": "^2.1.1", "@types/cli-progress": "^3.11.0", + "@types/debug": "^4.1.10", "@types/ejs": "^3.1.3", "@types/indent-string": "^4.0.1", "@types/js-yaml": "^3.12.7", diff --git a/src/config/plugin.ts b/src/config/plugin.ts index fbf7d9009..46c1a2052 100644 --- a/src/config/plugin.ts +++ b/src/config/plugin.ts @@ -1,4 +1,4 @@ -import {sync} from 'globby' +import globby from 'globby' import {join, parse, relative, sep} from 'node:path' import {inspect} from 'node:util' @@ -13,7 +13,7 @@ import {OCLIF_MARKER_OWNER, Performance} from '../performance' import {cacheCommand} from '../util/cache-command' import {findRoot} from '../util/find-root' import {readJson, requireJson} from '../util/fs' -import {castArray, compact, isProd, mapValues} from '../util/util' +import {castArray, compact, isProd} from '../util/util' import {tsPath} from './ts-node' import {Debug, getCommandIdPermutations} from './util' @@ -41,6 +41,21 @@ const search = (cmd: any) => { return Object.values(cmd).find((cmd: any) => typeof cmd.run === 'function') } +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)', +] + +function processCommandIds(files: string[]): string[] { + return files.map((file) => { + const p = parse(file) + const topics = p.dir.split('/') + const command = p.name !== 'index' && p.name + const id = [...topics, command].filter(Boolean).join(':') + return id === '' ? '.' : id + }) +} + export class Plugin implements IPlugin { alias!: string @@ -48,8 +63,12 @@ export class Plugin implements IPlugin { children: Plugin[] = [] + commandIDs: string[] = [] + commands!: Command.Loadable[] + commandsDir: string | undefined + hasManifest = false hooks!: {[k: string]: string[]} @@ -80,8 +99,6 @@ export class Plugin implements IPlugin { _base = `${_pjson.name}@${_pjson.version}` - private _commandsDir!: string | undefined - // eslint-disable-next-line new-cap protected _debug = Debug() @@ -89,35 +106,6 @@ export class Plugin implements IPlugin { constructor(public options: PluginOptions) {} - public get commandIDs(): string[] { - if (!this.commandsDir) return [] - - 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|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('/') - const command = p.name !== 'index' && p.name - const id = [...topics, command].filter(Boolean).join(':') - return id === '' ? '.' : id - }) - this._debug('found commands', ids) - marker?.addDetails({count: ids.length}) - marker?.stop() - return ids - } - - public get commandsDir(): string | undefined { - if (this._commandsDir) return this._commandsDir - - this._commandsDir = tsPath(this.root, this.pjson.oclif.commands, this) - return this._commandsDir - } - public get topics(): Topic[] { return topicsToArray(this.pjson.oclif.topics || {}) } @@ -163,7 +151,7 @@ export class Plugin implements IPlugin { } public async load(): Promise { - this.type = this.options.type || 'core' + this.type = this.options.type ?? 'core' this.tag = this.options.tag this.isRoot = this.options.isRoot ?? false if (this.options.parent) this.parent = this.options.parent as Plugin @@ -174,7 +162,7 @@ export class Plugin implements IPlugin { this.type === 'link' && !this.parent ? this.options.root : await findRoot(this.options.name, this.options.root) if (!root) throw new CLIError(`could not find package.json with ${inspect(this.options)}`) this.root = root - this._debug('reading %s plugin %s', this.type, root) + this._debug(`loading ${this.type} plugin from ${root}`) this.pjson = await readJson(join(root, 'package.json')) this.flexibleTaxonomy = this.options?.flexibleTaxonomy || this.pjson.oclif?.flexibleTaxonomy || false this.moduleType = this.pjson.type === 'module' ? 'module' : 'commonjs' @@ -192,7 +180,17 @@ export class Plugin implements IPlugin { this.pjson.oclif = this.pjson['cli-engine'] || {} } - this.hooks = mapValues(this.pjson.oclif.hooks ?? {}, (i) => castArray(i).map((i) => tsPath(this.root, i, this))) + this.commandsDir = await this.getCommandsDir() + this.commandIDs = await this.getCommandIDs() + + this.hooks = Object.fromEntries( + await Promise.all( + Object.entries(this.pjson.oclif.hooks ?? {}).map(async ([k, v]) => [ + k, + await Promise.all(castArray(v).map(async (i) => tsPath(this.root, i, this))), + ]), + ), + ) this.manifest = await this._manifest() this.commands = Object.entries(this.manifest.commands) @@ -292,6 +290,23 @@ export class Plugin implements IPlugin { return err } + private async getCommandIDs(): Promise { + if (!this.commandsDir) return [] + + const marker = Performance.mark(OCLIF_MARKER_OWNER, `plugin.getCommandIDs#${this.name}`, {plugin: this.name}) + this._debug(`loading IDs from ${this.commandsDir}`) + const files = await globby(GLOB_PATTERNS, {cwd: this.commandsDir}) + const ids = processCommandIds(files) + this._debug('found commands', ids) + marker?.addDetails({count: ids.length}) + marker?.stop() + return ids + } + + private async getCommandsDir(): Promise { + return tsPath(this.root, this.pjson.oclif.commands, this) + } + private warn(err: CLIError | Error | string, scope?: string): void { if (this.warned) return if (typeof err === 'string') err = new Error(err) diff --git a/src/config/ts-node.ts b/src/config/ts-node.ts index a9b36ea2c..28d2ed7a0 100644 --- a/src/config/ts-node.ts +++ b/src/config/ts-node.ts @@ -4,7 +4,7 @@ import * as TSNode from 'ts-node' import {memoizedWarn} from '../errors' import {Plugin, TSConfig} from '../interfaces' import {settings} from '../settings' -import {existsSync, readJsonSync} from '../util/fs' +import {existsSync, readJson} from '../util/fs' import {isProd} from '../util/util' import Cache from './cache' import {Debug} from './util' @@ -15,42 +15,40 @@ const debug = Debug('ts-node') export const TS_CONFIGS: Record = {} const REGISTERED = new Set() -function loadTSConfig(root: string): TSConfig | undefined { - if (TS_CONFIGS[root]) return TS_CONFIGS[root] - const tsconfigPath = join(root, 'tsconfig.json') - let typescript: typeof import('typescript') | undefined +async function loadTSConfig(root: string): Promise { try { - typescript = require('typescript') - } catch { - try { - typescript = require(require.resolve('typescript', {paths: [root, __dirname]})) - } catch { - debug(`Could not find typescript dependency. Skipping ts-node registration for ${root}.`) - memoizedWarn( - 'Could not find typescript. Please ensure that typescript is a devDependency. Falling back to compiled source.', - ) - return - } - } + if (TS_CONFIGS[root]) return TS_CONFIGS[root] + const tsconfigPath = join(root, 'tsconfig.json') + const tsconfig = await readJson(tsconfigPath) + + if (!tsconfig || Object.keys(tsconfig.compilerOptions).length === 0) return + + TS_CONFIGS[root] = tsconfig - if (existsSync(tsconfigPath) && typescript) { - const tsconfig = typescript.parseConfigFileTextToJson(tsconfigPath, readJsonSync(tsconfigPath, false)).config - if (!tsconfig || !tsconfig.compilerOptions) { - throw new Error( - `Could not read and parse tsconfig.json at ${tsconfigPath}, or it ` + - 'did not contain a "compilerOptions" section.', + if (tsconfig.extends) { + const {parse} = await import('tsconfck') + const result = await parse(tsconfigPath) + const tsNodeOpts = Object.fromEntries( + (result.extended ?? []).flatMap((e) => Object.entries(e.tsconfig['ts-node'] ?? {})).reverse(), ) + + TS_CONFIGS[root] = {...result.tsconfig, 'ts-node': tsNodeOpts} } - TS_CONFIGS[root] = tsconfig - return tsconfig + return TS_CONFIGS[root] + } catch (error) { + if (error instanceof SyntaxError) { + debug(`Could not parse tsconfig.json. Skipping ts-node registration for ${root}.`) + memoizedWarn(`Could not parse tsconfig.json for ${root}. Falling back to compiled source.`) + } } } -function registerTSNode(root: string): TSConfig | undefined { - const tsconfig = loadTSConfig(root) +async function registerTSNode(root: string): Promise { + const tsconfig = await loadTSConfig(root) if (!tsconfig) return if (REGISTERED.has(root)) return tsconfig + debug('registering ts-node at', root) const tsNodePath = require.resolve('ts-node', {paths: [root, __dirname]}) debug('ts-node path:', tsNodePath) @@ -76,25 +74,24 @@ function registerTSNode(root: string): TSConfig | undefined { } } else if (tsconfig.compilerOptions.rootDir) { rootDirs.push(join(root, tsconfig.compilerOptions.rootDir)) + } else if (tsconfig.compilerOptions.baseUrl) { + rootDirs.push(join(root, tsconfig.compilerOptions.baseUrl)) } else { rootDirs.push(join(root, 'src')) } + // Because we need to provide a modified `rootDirs` to ts-node, we need to + // remove `baseUrl` and `rootDir` from `compilerOptions` so that they + // don't conflict. + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const {baseUrl, rootDir, ...rest} = tsconfig.compilerOptions const conf: TSNode.RegisterOptions = { compilerOptions: { - emitDecoratorMetadata: tsconfig.compilerOptions.emitDecoratorMetadata ?? false, - esModuleInterop: tsconfig.compilerOptions.esModuleInterop, - experimentalDecorators: tsconfig.compilerOptions.experimentalDecorators ?? false, - module: tsconfig.compilerOptions.module ?? 'commonjs', + ...rest, rootDirs, - sourceMap: tsconfig.compilerOptions.sourceMap ?? true, - target: tsconfig.compilerOptions.target ?? 'es2019', typeRoots, - ...(tsconfig.compilerOptions.moduleResolution - ? {moduleResolution: tsconfig.compilerOptions.moduleResolution} - : {}), - ...(tsconfig.compilerOptions.jsx ? {jsx: tsconfig.compilerOptions.jsx} : {}), }, + ...tsconfig['ts-node'], cwd: root, esm: tsconfig['ts-node']?.esm ?? true, experimentalSpecifierResolution: tsconfig['ts-node']?.experimentalSpecifierResolution ?? 'explicit', @@ -106,7 +103,9 @@ function registerTSNode(root: string): TSConfig | undefined { tsNode.register(conf) REGISTERED.add(root) - debug('%O', tsconfig) + debug('tsconfig: %O', tsconfig) + debug('ts-node options: %O', conf) + return tsconfig } @@ -150,9 +149,10 @@ function cannotUseTsNode(root: string, plugin: Plugin | undefined, isProduction: /** * Determine the path to the source file from the compiled ./lib files */ -function determinePath(root: string, orig: string): string { - const tsconfig = registerTSNode(root) +async function determinePath(root: string, orig: string): Promise { + const tsconfig = await registerTSNode(root) if (!tsconfig) return orig + debug(`determining path for ${orig}`) const {baseUrl, outDir, rootDir, rootDirs} = tsconfig.compilerOptions const rootDirPath = rootDir ?? (rootDirs ?? [])[0] ?? baseUrl @@ -197,9 +197,9 @@ function determinePath(root: string, orig: string): string { * this is for developing typescript plugins/CLIs * if there is a tsconfig and the original sources exist, it attempts to require ts-node */ -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 { +export async function tsPath(root: string, orig: string, plugin: Plugin): Promise +export async function tsPath(root: string, orig: string | undefined, plugin?: Plugin): Promise +export async function tsPath(root: string, orig: string | undefined, plugin?: Plugin): Promise { const rootPlugin = plugin?.options.isRoot ? plugin : Cache.getInstance().get('rootPlugin') if (!orig) return orig @@ -245,7 +245,7 @@ export function tsPath(root: string, orig: string | undefined, plugin?: Plugin): } try { - return determinePath(root, orig) + return await determinePath(root, orig) } catch (error: any) { debug(error) return orig diff --git a/src/errors/index.ts b/src/errors/index.ts index ce120a502..cacaa1fd1 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -1,3 +1,4 @@ +import write from '../cli-ux/write' import {OclifError, PrettyPrintableError} from '../interfaces' import {config} from './config' import {CLIError, addOclifExitCode} from './errors/cli' @@ -32,7 +33,7 @@ export function error(input: Error | string, options: {exit?: false | number} & if (options.exit === false) { const message = prettyPrint(err) - console.error(message) + if (message) write.stderr(message + '\n') if (config.errorLogger) config.errorLogger.log(err?.stack ?? '') } else throw err } @@ -49,7 +50,7 @@ export function warn(input: Error | string): void { } const message = prettyPrint(err) - console.error(message) + if (message) write.stderr(message + '\n') if (config.errorLogger) config.errorLogger.log(err?.stack ?? '') } diff --git a/src/interfaces/plugin.ts b/src/interfaces/plugin.ts index 994a42216..28945e1a6 100644 --- a/src/interfaces/plugin.ts +++ b/src/interfaces/plugin.ts @@ -38,6 +38,7 @@ export interface Plugin { alias: string readonly commandIDs: string[] commands: Command.Loadable[] + readonly commandsDir: string | undefined findCommand(id: string, opts: {must: true}): Promise findCommand(id: string, opts?: {must: boolean}): Promise | undefined readonly hasManifest: boolean diff --git a/src/interfaces/ts-config.ts b/src/interfaces/ts-config.ts index 44aea6320..33db32a4b 100644 --- a/src/interfaces/ts-config.ts +++ b/src/interfaces/ts-config.ts @@ -13,9 +13,11 @@ export interface TSConfig { sourceMap?: boolean target?: string } + extends?: string 'ts-node'?: { esm?: boolean experimentalSpecifierResolution?: 'explicit' | 'node' scope?: boolean + swc?: boolean } } diff --git a/src/module-loader.ts b/src/module-loader.ts index 588a4d915..15a2981d5 100644 --- a/src/module-loader.ts +++ b/src/module-loader.ts @@ -38,7 +38,7 @@ export async function load(config: IConfig | IPlugin, modulePath: strin let filePath: string | undefined let isESM: boolean | undefined try { - ;({filePath, isESM} = resolvePath(config, modulePath)) + ;({filePath, isESM} = await resolvePath(config, modulePath)) return (isESM ? await import(pathToFileURL(filePath).href) : require(filePath)) as T } catch (error: any) { if (error.code === 'MODULE_NOT_FOUND' || error.code === 'ERR_MODULE_NOT_FOUND') { @@ -73,7 +73,7 @@ export async function loadWithData( let filePath: string | undefined let isESM: boolean | undefined try { - ;({filePath, isESM} = resolvePath(config, modulePath)) + ;({filePath, isESM} = await resolvePath(config, modulePath)) const module = isESM ? await import(pathToFileURL(filePath).href) : require(filePath) return {filePath, isESM, module} } catch (error: any) { @@ -172,7 +172,7 @@ export function isPathModule(filePath: string): boolean { * * @returns {{isESM: boolean, filePath: string}} An object including file path and whether the module is ESM. */ -function resolvePath(config: IConfig | IPlugin, modulePath: string): {filePath: string; isESM: boolean} { +async function resolvePath(config: IConfig | IPlugin, modulePath: string): Promise<{filePath: string; isESM: boolean}> { let isESM: boolean let filePath: string | undefined @@ -181,7 +181,8 @@ function resolvePath(config: IConfig | IPlugin, modulePath: string): {filePath: isESM = isPathModule(filePath) } catch { filePath = - (isPlugin(config) ? tsPath(config.root, modulePath, config) : tsPath(config.root, modulePath)) ?? modulePath + (isPlugin(config) ? await tsPath(config.root, modulePath, config) : await tsPath(config.root, modulePath)) ?? + modulePath let fileExists = false let isDirectory = false diff --git a/src/util/find-root.ts b/src/util/find-root.ts index 9ee8f3c6c..82362b93c 100644 --- a/src/util/find-root.ts +++ b/src/util/find-root.ts @@ -1,11 +1,14 @@ /* eslint-disable no-await-in-loop */ import type {PackageInformation, PackageLocator, getPackageInformation} from 'pnpapi' +import makeDebug from 'debug' import {basename, dirname, join} from 'node:path' import {PJSON} from '../interfaces' import {safeReadJson} from './fs' +const debug = makeDebug('find-root') + // essentially just "cd .." function* up(from: string) { while (dirname(from) !== from) { @@ -26,7 +29,9 @@ async function findPluginRoot(root: string, name?: string) { // If we know the plugin name then we just need to traverse the file // system until we find the directory that matches the plugin name. if (name) { + debug.extend(name)(`Finding root starting at ${root}`) for (const next of up(root)) { + debug.extend(name)(`Checking ${next}`) if (next.endsWith(basename(name))) return next } } @@ -44,6 +49,7 @@ async function findPluginRoot(root: string, name?: string) { try { const cur = join(next, 'package.json') + debug.extend('root-plugin')(`Checking ${cur}`) if (await safeReadJson(cur)) return dirname(cur) } catch {} } @@ -56,6 +62,7 @@ async function findPluginRoot(root: string, name?: string) { * See https://github.com/oclif/config/pull/289#issuecomment-983904051 */ async function findRootLegacy(name: string | undefined, root: string): Promise { + debug.extend(name ?? 'root-plugin')('Finding root using legacy method') for (const next of up(root)) { let cur if (name) { @@ -106,9 +113,9 @@ const isPeerDependency = ( */ function findPnpRoot(name: string, root: string): string | undefined { maybeRequirePnpApi(root) - if (!pnp) return + debug.extend(name)('Finding root for using pnp method') const seen = new Set() const traverseDependencyTree = (locator: PackageLocator, parentPkg?: PackageInformation): string | undefined => { @@ -165,15 +172,30 @@ function findPnpRoot(name: string, root: string): string | undefined { */ export async function findRoot(name: string | undefined, root: string) { if (name) { + debug.extend(name)(`Finding root using ${root}`) let pkgPath try { pkgPath = require.resolve(name, {paths: [root]}) - } catch {} + debug.extend(name)(`Found starting point with require.resolve`) + } catch { + debug.extend(name)(`require.resolve could not find plugin starting point`) + } - if (pkgPath) return findPluginRoot(dirname(pkgPath), name) + if (pkgPath) { + const found = await findPluginRoot(dirname(pkgPath), name) + if (found) { + debug.extend(name)(`Found root at ${found}`) + return found + } + } - return process.versions.pnp ? findPnpRoot(name, root) : findRootLegacy(name, root) + const found = process.versions.pnp ? findPnpRoot(name, root) : await findRootLegacy(name, root) + debug.extend(name)(found ? `Found root at ${found}` : 'No root found!') + return found } - return findPluginRoot(root) + debug.extend('root-plugin')(`Finding root plugin using ${root}`) + const found = await findPluginRoot(root) + debug.extend('root-plugin')(found ? `Found root at ${found}` : 'No root found!') + return found } diff --git a/src/util/fs.ts b/src/util/fs.ts index ff78464cc..50e04a99f 100644 --- a/src/util/fs.ts +++ b/src/util/fs.ts @@ -2,8 +2,6 @@ import {Stats, existsSync as fsExistsSync, readFileSync} from 'node:fs' import {readFile, stat} from 'node:fs/promises' import {join} from 'node:path' -const debug = require('debug') - export function requireJson(...pathParts: string[]): T { return JSON.parse(readFileSync(join(...pathParts), 'utf8')) } @@ -51,7 +49,6 @@ export const fileExists = async (input: string): Promise => { } export async function readJson(path: string): Promise { - debug('config')('readJson %s', path) const contents = await readFile(path, 'utf8') return JSON.parse(contents) as T } diff --git a/test/config/config.flexible.test.ts b/test/config/config.flexible.test.ts index b68162c2c..a2b0b7486 100644 --- a/test/config/config.flexible.test.ts +++ b/test/config/config.flexible.test.ts @@ -105,6 +105,7 @@ describe('Config with flexible taxonomy', () => { hasManifest: false, isRoot: false, options: {root: ''}, + commandsDir: './lib/commands', } const pluginB: IPlugin = { @@ -127,6 +128,7 @@ describe('Config with flexible taxonomy', () => { hasManifest: false, isRoot: false, options: {root: ''}, + commandsDir: './lib/commands', } const plugins = new Map().set(pluginA.name, pluginA).set(pluginB.name, pluginB) diff --git a/test/config/config.test.ts b/test/config/config.test.ts index 1fd875216..76cf0cb9f 100644 --- a/test/config/config.test.ts +++ b/test/config/config.test.ts @@ -294,6 +294,7 @@ describe('Config', () => { hasManifest: false, isRoot: false, options: {root: ''}, + commandsDir: './lib/commands', } const pluginB: IPlugin = { @@ -316,6 +317,7 @@ describe('Config', () => { hasManifest: false, isRoot: false, options: {root: ''}, + commandsDir: './lib/commands', } const plugins = new Map().set(pluginA.name, pluginA).set(pluginB.name, pluginB) let test = fancy diff --git a/test/config/ts-node.test.ts b/test/config/ts-node.test.ts index 2ab8e44a2..83ba9861b 100644 --- a/test/config/ts-node.test.ts +++ b/test/config/ts-node.test.ts @@ -1,10 +1,12 @@ import {expect} from 'chai' import {join, resolve} from 'node:path' import {SinonSandbox, createSandbox} from 'sinon' +import stripAnsi from 'strip-ansi' import * as tsNode from 'ts-node' -import {Interfaces, settings} from '../../src' +import write from '../../src/cli-ux/write' import * as configTsNode from '../../src/config/ts-node' +import {Interfaces, settings} from '../../src/index' import * as util from '../../src/util/fs' const root = resolve(__dirname, 'fixtures/typescript') @@ -27,7 +29,6 @@ describe('tsPath', () => { beforeEach(() => { sandbox = createSandbox() - sandbox.stub(util, 'existsSync').returns(true) sandbox.stub(tsNode, 'register') }) @@ -42,49 +43,76 @@ describe('tsPath', () => { configTsNode.REGISTERED = new Set() }) - it('should resolve a .js file to ts src', () => { - sandbox.stub(util, 'readJsonSync').returns(JSON.stringify(DEFAULT_TS_CONFIG)) - const result = configTsNode.tsPath(root, jsCompiled) + it('should resolve a .js file to ts src', async () => { + sandbox.stub(util, 'readJson').resolves(DEFAULT_TS_CONFIG) + const result = await configTsNode.tsPath(root, jsCompiled) expect(result).to.equal(join(root, tsModule)) }) - it('should resolve a module file to ts src', () => { - sandbox.stub(util, 'readJsonSync').returns(JSON.stringify(DEFAULT_TS_CONFIG)) - const result = configTsNode.tsPath(root, jsCompiledModule) + it('should resolve a module file to ts src', async () => { + sandbox.stub(util, 'readJson').resolves(DEFAULT_TS_CONFIG) + const result = await configTsNode.tsPath(root, jsCompiledModule) expect(result).to.equal(join(root, tsModule)) }) - it('should resolve a .ts file', () => { - sandbox.stub(util, 'readJsonSync').returns(JSON.stringify(DEFAULT_TS_CONFIG)) - const result = configTsNode.tsPath(root, tsSource) + it('should resolve a .ts file', async () => { + sandbox.stub(util, 'readJson').resolves(DEFAULT_TS_CONFIG) + const result = await configTsNode.tsPath(root, tsSource) expect(result).to.equal(join(root, tsSource)) }) - it('should resolve .js with no rootDir or outDir', () => { - sandbox.stub(util, 'readJsonSync').returns({compilerOptions: {}}) - const result = configTsNode.tsPath(root, jsCompiled) + it('should resolve a .ts file using baseUrl', async () => { + sandbox.stub(util, 'readJson').resolves({ + compilerOptions: { + baseUrl: '.src/', + outDir: 'lib', + }, + }) + const result = await configTsNode.tsPath(root, tsSource) + expect(result).to.equal(join(root, tsSource)) + }) + + it('should resolve .ts with no outDir', async () => { + sandbox.stub(util, 'readJson').resolves({compilerOptions: {rootDir: 'src'}}) + const result = await configTsNode.tsPath(root, tsSource) + expect(result).to.equal(join(root, tsSource)) + }) + + it('should resolve .js with no rootDir and outDir', async () => { + sandbox.stub(util, 'readJson').resolves({compilerOptions: {strict: true}}) + const result = await configTsNode.tsPath(root, jsCompiled) expect(result).to.equal(join(root, jsCompiled)) }) - it('should resolve to .ts file if enabled and prod', () => { - sandbox.stub(util, 'readJsonSync').returns(JSON.stringify(DEFAULT_TS_CONFIG)) + it('should resolve to .ts file if enabled and prod', async () => { + sandbox.stub(util, 'readJson').resolves(DEFAULT_TS_CONFIG) settings.tsnodeEnabled = true const originalNodeEnv = process.env.NODE_ENV delete process.env.NODE_ENV - const result = configTsNode.tsPath(root, jsCompiled) + const result = await configTsNode.tsPath(root, jsCompiled) expect(result).to.equal(join(root, tsModule)) process.env.NODE_ENV = originalNodeEnv delete settings.tsnodeEnabled }) - it('should resolve to js if disabled', () => { - sandbox.stub(util, 'readJsonSync').returns(JSON.stringify(DEFAULT_TS_CONFIG)) + it('should resolve to js if disabled', async () => { + sandbox.stub(util, 'readJson').resolves(DEFAULT_TS_CONFIG) settings.tsnodeEnabled = false - const result = configTsNode.tsPath(root, jsCompiled) + const result = await configTsNode.tsPath(root, jsCompiled) expect(result).to.equal(join(root, jsCompiled)) delete settings.tsnodeEnabled }) + + it('should handle SyntaxError', async () => { + sandbox.stub(util, 'readJson').throws(new SyntaxError('Unexpected token } in JSON at position 0')) + const stderrStub = sandbox.stub(write, 'stderr') + const result = await configTsNode.tsPath(root, tsSource) + expect(result).to.equal(join(root, tsSource)) + expect(stripAnsi(stderrStub.firstCall.firstArg).split('\n').join(' ')).to.include( + 'Warning: Could not parse tsconfig.json', + ) + }) }) diff --git a/test/config/typescript.test.ts b/test/config/typescript.test.ts index ea06c28cc..441e9fb75 100644 --- a/test/config/typescript.test.ts +++ b/test/config/typescript.test.ts @@ -36,8 +36,7 @@ describe('typescript', () => { }) withConfig.stdout().it('runs init hook', async (ctx) => { - // to-do: fix union types - await (ctx.config.runHook as any)('init', {id: 'myid', argv: ['foo']}) + await ctx.config.runHook('init', {id: 'myid', argv: ['foo']}) expect(ctx.stdout).to.equal('running ts init hook\n') }) }) diff --git a/test/integration/esm-cjs.ts b/test/integration/esm-cjs.ts index f642c6e48..7dc33f94d 100644 --- a/test/integration/esm-cjs.ts +++ b/test/integration/esm-cjs.ts @@ -236,7 +236,10 @@ type PluginConfig = { noLinkCore: options.noLinkCore ?? false, }) - const result = await options.executor.executeCommand(`plugins:link ${pluginExecutor.pluginDir}`, options.script) + const result = await options.executor.executeCommand( + `plugins:link ${pluginExecutor.pluginDir} --no-install`, + options.script, + ) expect(result.code).to.equal(0) const pluginsResult = await options.executor.executeCommand('plugins', options.script) diff --git a/test/integration/plugins.e2e.ts b/test/integration/plugins.e2e.ts index 6bf15f8a9..0a88c8c1e 100644 --- a/test/integration/plugins.e2e.ts +++ b/test/integration/plugins.e2e.ts @@ -20,6 +20,7 @@ describe('oclif plugins', () => { '@oclif/plugin-version', '@oclif/plugin-which', ], + yarnInstallArgs: ['--no-lockfile'], }) }) diff --git a/test/integration/util.ts b/test/integration/util.ts index fc2c3066e..1ae0a80f0 100644 --- a/test/integration/util.ts +++ b/test/integration/util.ts @@ -24,6 +24,7 @@ export type SetupOptions = { plugins?: string[] subDir?: string noLinkCore?: boolean + yarnInstallArgs?: string[] } export type ExecutorOptions = { @@ -186,9 +187,12 @@ export async function setup(testFile: string, options: SetupOptions): Promise