Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support bun and tsx for development #966

Merged
merged 9 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
})
})