Skip to content

Commit

Permalink
Merge pull request #966 from oclif/mdonnalley/bun-tsx
Browse files Browse the repository at this point in the history
feat: support bun and tsx for development
  • Loading branch information
shetzel committed Feb 27, 2024
2 parents 3d42b99 + c045edc commit 53aaaf9
Show file tree
Hide file tree
Showing 13 changed files with 725 additions and 623 deletions.
27 changes: 24 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
- if: runner.os == 'Windows'
run: yarn mocha --forbid-only "test/**/*.integration.ts" --exclude "test/integration/sf.integration.ts" --parallel --timeout 1200000
- if: runner.os == 'Linux'
run: yarn test:integration
run: yarn test:integration --retries 3
windows-sf-integration:
# For whatever reason the windows-latest runner doesn't like it when you shell yarn commands in the sf repo
# which is an integral part of the setup for the tests. Instead, we replicate the setup here.
Expand Down Expand Up @@ -72,16 +72,33 @@ jobs:
OCLIF_CORE_INTEGRATION_SKIP_SETUP: true
OCLIF_CORE_INTEGRATION_TEST_DIR: D:\a\integration
DEBUG: integration:*
esm-cjs-interop:
interoperability:
needs: linux-unit-tests
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node_version: [lts/*, latest]
test: [esm, cjs, precore, coreV1, coreV2]
dev_runtime: [default, bun, tsx]
exclude:
- os: windows-latest
node_version: lts/*
- os: windows-latest
dev_runtime: tsx
- os: windows-latest
dev_runtime: bun
- test: precore
dev_runtime: tsx
- test: precore
dev_runtime: bun
- test: coreV1
dev_runtime: tsx
- test: coreV1
dev_runtime: bun
- test: coreV2
dev_runtime: tsx
- test: coreV2
dev_runtime: bun
fail-fast: false
runs-on: ${{ matrix.os }}
timeout-minutes: 75
Expand All @@ -91,9 +108,13 @@ jobs:
with:
node-version: ${{ matrix.node_version }}
cache: yarn
- if: matrix.dev_runtime == 'bun'
uses: oven-sh/setup-bun@v1
- if: matrix.dev_runtime == 'tsx'
run: 'npm install -g tsx'
- uses: salesforcecli/github-workflows/.github/actions/yarnInstallWithRetries@main
- run: yarn build
- run: yarn test:esm-cjs --test=${{ matrix.test }}
- run: yarn test:interoperability --test=${{ matrix.test }} --dev-run-time=${{ matrix.dev_runtime }}
nuts:
needs: linux-unit-tests
uses: salesforcecli/github-workflows/.github/workflows/externalNut.yml@main
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@
"test:circular-deps": "madge lib/ -c",
"test:debug": "nyc mocha --debug-brk --inspect \"test/**/*.test.ts\"",
"test:integration": "mocha --forbid-only \"test/**/*.integration.ts\" --parallel --timeout 1200000",
"test:esm-cjs": "cross-env DEBUG=integration:* ts-node test/integration/esm-cjs.ts",
"test:interoperability": "cross-env DEBUG=integration:* ts-node test/integration/interop.ts",
"test:perf": "ts-node test/perf/parser.perf.ts",
"test:dev": "nyc mocha \"test/**/*.test.ts\"",
"test": "nyc mocha --forbid-only \"test/**/*.test.ts\""
Expand Down
2 changes: 1 addition & 1 deletion src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {requireJson, safeReadJson} from '../util/fs'
import {getHomeDir, getPlatform} from '../util/os'
import {compact, isProd} from '../util/util'
import PluginLoader from './plugin-loader'
import {tsPath} from './ts-node'
import {tsPath} from './ts-path'
import {Debug, collectUsableIds, getCommandIdPermutations} from './util'

// eslint-disable-next-line new-cap
Expand Down
2 changes: 1 addition & 1 deletion src/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export {Config} from './config'
export {Plugin} from './plugin'
export {tsPath} from './ts-node'
export {tsPath} from './ts-path'
2 changes: 1 addition & 1 deletion src/config/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {cacheCommand} from '../util/cache-command'
import {findRoot} from '../util/find-root'
import {readJson, requireJson} from '../util/fs'
import {castArray, compact} from '../util/util'
import {tsPath} from './ts-node'
import {tsPath} from './ts-path'
import {Debug, getCommandIdPermutations} from './util'

const _pjson = requireJson<PJSON>(__dirname, '..', '..', 'package.json')
Expand Down
88 changes: 66 additions & 22 deletions src/config/ts-node.ts → src/config/ts-path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,50 @@ import {readTSConfig} from '../util/read-tsconfig'
import {isProd} from '../util/util'
import {Debug} from './util'
// eslint-disable-next-line new-cap
const debug = Debug('ts-node')
const debug = Debug('ts-path')

export const TS_CONFIGS: Record<string, TSConfig | undefined> = {}
const REGISTERED = new Set<string>()

function determineRuntime(): 'node' | 'bun' | 'ts-node' | 'tsx' {
/**
* Examples:
* #!/usr/bin/env bun
* bun bin/run.js
* bun bin/dev.js
*/
if (process.execPath.split(sep).includes('bun')) return 'bun'
/**
* Examples:
* #!/usr/bin/env node
* #!/usr/bin/env node --loader ts-node/esm --experimental-specifier-resolution=node --no-warnings
* node bin/run.js
* node bin/dev.js
*/
if (process.execArgv.length === 0) return 'node'
/**
* Examples:
* #!/usr/bin/env ts-node
* #!/usr/bin/env node_modules/.bin/ts-node
* ts-node bin/run.js
* ts-node bin/dev.js
*/
if (process.execArgv[0] === '--require' && process.execArgv[1].split(sep).includes('ts-node')) return 'ts-node'
if (process.execArgv[0].split(sep).includes('ts-node')) return 'ts-node'
/**
* Examples:
* #!/usr/bin/env tsx
* #!/usr/bin/env node_modules/.bin/tsx
* tsx bin/run.js
* tsx bin/dev.js
*/
if (process.execArgv[0] === '--require' && process.execArgv[1].split(sep).includes('tsx')) return 'tsx'

return 'node'
}

const RUN_TIME = determineRuntime()

function isErrno(error: any): error is NodeJS.ErrnoException {
return 'code' in error && error.code === 'ENOENT'
}
Expand All @@ -23,21 +62,21 @@ async function loadTSConfig(root: string): Promise<TSConfig | undefined> {
try {
if (TS_CONFIGS[root]) return TS_CONFIGS[root]

TS_CONFIGS[root] = await readTSConfig(root)

const tsconfig = await readTSConfig(root)
if (!tsconfig) return
debug('tsconfig: %O', tsconfig)
TS_CONFIGS[root] = tsconfig
return TS_CONFIGS[root]
} catch (error) {
if (isErrno(error)) return

debug(`Could not parse tsconfig.json. Skipping ts-node registration for ${root}.`)
debug(`Could not parse tsconfig.json. Skipping typescript path lookup for ${root}.`)
memoizedWarn(`Could not parse tsconfig.json for ${root}. Falling back to compiled source.`)
}
}

async function registerTSNode(root: string): Promise<TSConfig | undefined> {
const tsconfig = await loadTSConfig(root)
if (!tsconfig) return
if (REGISTERED.has(root)) return tsconfig
async function registerTSNode(root: string, tsconfig: TSConfig): Promise<void> {
if (REGISTERED.has(root)) return

debug('registering ts-node at', root)
const tsNodePath = require.resolve('ts-node', {paths: [root, __dirname]})
Expand Down Expand Up @@ -91,12 +130,9 @@ async function registerTSNode(root: string): Promise<TSConfig | undefined> {
transpileOnly: true,
}

debug('ts-node options: %O', conf)
tsNode.register(conf)
REGISTERED.add(root)
debug('tsconfig: %O', tsconfig)
debug('ts-node options: %O', conf)

return tsconfig
}

/**
Expand Down Expand Up @@ -132,18 +168,24 @@ function cannotUseTsNode(root: string, plugin: Plugin | undefined, isProduction:
if (plugin?.moduleType !== 'module' || isProduction) return false

const nodeMajor = Number.parseInt(process.version.replace('v', '').split('.')[0], 10)
const tsNodeExecIsUsed = process.execArgv[0] === '--require' && process.execArgv[1].split(sep).includes(`ts-node`)
return tsNodeExecIsUsed && nodeMajor >= 20
return RUN_TIME === 'ts-node' && nodeMajor >= 20
}

/**
* Determine the path to the source file from the compiled ./lib files
*/
async function determinePath(root: string, orig: string): Promise<string> {
const tsconfig = await registerTSNode(root)
const tsconfig = await loadTSConfig(root)
if (!tsconfig) return orig

debug(`determining path for ${orig}`)
debug(`Determining path for ${orig}`)

if (RUN_TIME === 'tsx' || RUN_TIME === 'bun') {
debug(`Skipping ts-node registration for ${root} because the runtime is: ${RUN_TIME}`)
} else {
await registerTSNode(root, tsconfig)
}

const {baseUrl, outDir, rootDir, rootDirs} = tsconfig.compilerOptions
const rootDirPath = rootDir ?? (rootDirs ?? [])[0] ?? baseUrl
if (!rootDirPath) {
Expand Down Expand Up @@ -197,22 +239,24 @@ export async function tsPath(root: string, orig: string | undefined, plugin?: Pl

// NOTE: The order of these checks matter!

if (settings.tsnodeEnabled === false) {
debug(`Skipping ts-node registration for ${root} because tsNodeEnabled is explicitly set to false`)
const enableAutoTranspile = settings.enableAutoTranspile ?? settings.tsnodeEnabled

if (enableAutoTranspile === false) {
debug(`Skipping typescript path lookup for ${root} because enableAutoTranspile is explicitly set to false`)
return orig
}

const isProduction = isProd()

// Do not skip ts-node registration if the plugin is linked
if (settings.tsnodeEnabled === undefined && isProduction && plugin?.type !== 'link') {
debug(`Skipping ts-node registration for ${root} because NODE_ENV is NOT "test" or "development"`)
if (enableAutoTranspile === undefined && isProduction && plugin?.type !== 'link') {
debug(`Skipping typescript path lookup for ${root} because NODE_ENV is NOT "test" or "development"`)
return orig
}

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: ${rootPlugin?.moduleType})`,
`Skipping typescript path lookup 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(
Expand All @@ -222,7 +266,7 @@ export async function tsPath(root: string, orig: string | undefined, plugin?: Pl
}

if (cannotUseTsNode(root, plugin, isProduction)) {
debug(`Skipping ts-node registration for ${root} because ts-node is run in node version ${process.version}"`)
debug(`Skipping typescript path lookup for ${root} because ts-node is run in node version ${process.version}"`)
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.`,
)
Expand Down
2 changes: 1 addition & 1 deletion src/module-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {extname, join, sep} from 'node:path'
import {pathToFileURL} from 'node:url'

import {Command} from './command'
import {tsPath} from './config/ts-node'
import {tsPath} from './config/ts-path'
import {ModuleLoadError} from './errors'
import {Config as IConfig, Plugin as IPlugin} from './interfaces'
import {existsSync} from './util/fs'
Expand Down
19 changes: 11 additions & 8 deletions src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export type Settings = {
/**
* Show additional debug output without DEBUG. Mainly shows stackstraces.
*
* Useful to set in the ./bin/dev script.
* Useful to set in the ./bin/dev.js script.
* oclif.settings.debug = true;
*/
debug?: boolean
Expand All @@ -25,16 +25,19 @@ export type Settings = {
*/
performanceEnabled?: boolean
/**
* Try to use ts-node to load typescript source files instead of
* javascript files.
* Try to use ts-node to load typescript source files instead of javascript files.
* Defaults to true in development and test environments (e.g. using bin/dev.js or
* NODE_ENV=development or NODE_ENV=test).
*
* NOTE: This requires registering ts-node first.
* require('ts-node').register();
*
* Environment Variable:
* NODE_ENV=development
* @deprecated use enableAutoTranspile instead.
*/
tsnodeEnabled?: boolean
/**
* Enable automatic transpilation of TypeScript files to JavaScript.
*
* Defaults to true in development and test environments (e.g. using bin/dev.js or NODE_ENV=development or NODE_ENV=test).
*/
enableAutoTranspile?: boolean
}

// Set global.oclif to the new object if it wasn't set before
Expand Down
10 changes: 5 additions & 5 deletions test/config/ts-node.test.ts → test/config/ts-path.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {join, resolve} from 'node:path'
import {SinonSandbox, createSandbox} from 'sinon'
import * as tsNode from 'ts-node'

import * as configTsNode from '../../src/config/ts-node'
import * as configTsNode from '../../src/config/ts-path'
import {Interfaces, settings} from '../../src/index'
import * as util from '../../src/util/read-tsconfig'

Expand Down Expand Up @@ -84,23 +84,23 @@ describe('tsPath', () => {

it('should resolve to .ts file if enabled and prod', async () => {
sandbox.stub(util, 'readTSConfig').resolves(DEFAULT_TS_CONFIG)
settings.tsnodeEnabled = true
settings.enableAutoTranspile = true
const originalNodeEnv = process.env.NODE_ENV
delete process.env.NODE_ENV

const result = await configTsNode.tsPath(root, jsCompiled)
expect(result).to.equal(join(root, tsModule))

process.env.NODE_ENV = originalNodeEnv
delete settings.tsnodeEnabled
delete settings.enableAutoTranspile
})

it('should resolve to js if disabled', async () => {
sandbox.stub(util, 'readTSConfig').resolves(DEFAULT_TS_CONFIG)
settings.tsnodeEnabled = false
settings.enableAutoTranspile = false
const result = await configTsNode.tsPath(root, jsCompiled)
expect(result).to.equal(join(root, jsCompiled))

delete settings.tsnodeEnabled
delete settings.enableAutoTranspile
})
})

0 comments on commit 53aaaf9

Please sign in to comment.