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: binary execution #14

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ Pass in a file to run:
tsx ./file.ts
```

You can also run a local binary (binaries in `./node_modules/.bin/`):
```sh
tsx binary-name
```

### Watch mode
Run file and automatically re-run on changes.

Expand Down
39 changes: 30 additions & 9 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { cli } from 'cleye';
import { version } from '../package.json';
import { run } from './run';
import { isBinaryPath, runBinary } from './run-binary';
import { watchCommand } from './watch';

cli({
Expand All @@ -25,7 +25,10 @@ cli({
},
},
help: false,
}, (argv) => {
}, async (argv) => {
let binaryPath = process.execPath;
let args = process.argv.slice(2);

if (argv._.length === 0) {
if (argv.flags.version) {
console.log(version);
Expand All @@ -40,16 +43,34 @@ cli({
}

// Load REPL
process.argv.push(require.resolve('./repl'));
args.push(require.resolve('./repl'));
}

const args = process.argv.slice(2).filter(
argument => (argument !== '--no-cache' && argument !== '--noCache'),
);
const [scriptPath] = argv._;
const foundBinary = await isBinaryPath(scriptPath);

if (foundBinary) {
binaryPath = foundBinary;

run(args, {
noCache: Boolean(argv.flags.noCache),
}).on(
const scriptIndex = args.indexOf(scriptPath);
if (scriptIndex > -1) {
args.splice(scriptIndex, 1);
}
}

if (argv.flags.noCache) {
args = args.filter(
argument => (argument !== '--no-cache' && argument !== '--noCache'),
);
}

runBinary(
binaryPath,
args,
{
noCache: Boolean(argv.flags.noCache),
},
).on(
'close',
code => process.exit(code!),
);
Expand Down
11 changes: 5 additions & 6 deletions src/ignore-node-warnings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { Transform } from 'stream';

const warningTraceTip = '(Use `node --trace-warnings ...` to show where the warning was created)';
const nodeWarningPattern = /^\(node:\d+\) (.+)\n/m;
const warningsToIgnore = [
'ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time',
'ExperimentalWarning: Custom ESM Loaders is an experimental feature. This feature could change at any time',
'ExperimentalWarning: Importing JSON modules is an experimental feature. This feature could change at any time',
];

export const ignoreNodeWarnings = () => {
const warningsToIgnore = [
'ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time',
'ExperimentalWarning: Custom ESM Loaders is an experimental feature. This feature could change at any time',
'ExperimentalWarning: Importing JSON modules is an experimental feature. This feature could change at any time',
];
let filterStderr = true;
let counter = 0;

Expand Down Expand Up @@ -37,7 +37,6 @@ export const ignoreNodeWarnings = () => {
return callback(null, chunk);
}

warningsToIgnore.splice(ignoreWarning, 1);
if (warningsToIgnore.length === 0) {
filterStderr = false;
}
Expand Down
39 changes: 30 additions & 9 deletions src/run.ts → src/run-binary.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,43 @@
import fs from 'fs';
import type { StdioOptions } from 'child_process';
import { pathToFileURL } from 'url';
import spawn from 'cross-spawn';
import { ignoreNodeWarnings } from './ignore-node-warnings';

export function run(
const pathExists = (filePath: string) => fs.promises.access(filePath).then(() => true, () => false);

const isPathPattern = /^\.|\//;

export const isBinaryPath = async (filePath: string) => {
if (isPathPattern.test(filePath)) {
return false;
}

const fileExists = await pathExists(filePath);

if (fileExists) {
return false;
}

const binaryPath = `./node_modules/.bin/${filePath}`;
if (await pathExists(binaryPath)) {
return binaryPath;
}

return false;
};

export function runBinary(
binaryPath: string,
argv: string[],
options?: {
noCache?: boolean;
ipc?: boolean;
},
) {
const environment = {
const environment: Record<string, string> = {
...process.env,
NODE_OPTIONS: `--loader ${pathToFileURL(require.resolve('./loader.js')).toString()}`,
};

if (options?.noCache) {
Expand All @@ -30,13 +56,8 @@ export function run(
}

const childProcess = spawn(
process.execPath,
[
'--loader',
pathToFileURL(require.resolve('./loader.js')).toString(),

...argv,
],
binaryPath,
argv,
{
stdio,
env: environment,
Expand Down
6 changes: 4 additions & 2 deletions src/watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ChildProcess } from 'child_process';
import { fileURLToPath } from 'url';
import { command } from 'cleye';
import { watch } from 'chokidar';
import { run } from './run';
import { runBinary } from './run-binary';

// From ansi-escapes
// https://github.com/sindresorhus/ansi-escapes/blob/2b3b59c56ff77a/index.js#L80
Expand All @@ -18,6 +18,8 @@ function isDependencyPath(
);
}

const nodeBinary = process.execPath;

export const watchCommand = command({
name: 'watch',
parameters: ['<script path>'],
Expand All @@ -44,7 +46,7 @@ export const watchCommand = command({

process.stdout.write(clearScreen);

runProcess = run(argv._, options);
runProcess = runBinary(nodeBinary, argv._, options);

runProcess.on('message', (data) => {
// Collect run-time dependencies to watch
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/node_modules/.bin/binary

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions tests/fixtures/node_modules/package-module-binary/binary

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ const nodeVersions = [
import('./specs/json'),
node,
);
runTestSuite(
import('./specs/binary'),
node,
);
});
}

Expand Down
42 changes: 42 additions & 0 deletions tests/specs/binary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import path from 'path';
import { testSuite, expect } from 'manten';
import { tsx } from '../utils/tsx';
import type { NodeApis } from '../utils/tsx';

export default testSuite(async ({ describe }, node: NodeApis) => {
describe('Binary', ({ test }) => {
test('missing binary falls back to file-system', async () => {
const tsxProcess = await tsx({
args: ['missing-binary'],
cwd: path.resolve('./tests/fixtures/'),
nodePath: node.path,
});

expect(tsxProcess.exitCode).toBe(1);
expect(tsxProcess.stdout).toBe('');
expect(tsxProcess.stderr).toMatch('ERR_MODULE_NOT_FOUND');
});

test('file path to be ignored as binary', async () => {
const tsxProcess = await tsx({
args: ['./binary'],
cwd: path.resolve('./tests/fixtures'),
});

expect(tsxProcess.exitCode).toBe(1);
expect(tsxProcess.stdout).toBe('');
expect(tsxProcess.stderr).toMatch('ERR_MODULE_NOT_FOUND');
});

test('binary to run', async () => {
const tsxProcess = await tsx({
args: ['binary', 'hello', 'world', 'binary'],
cwd: path.resolve('./tests/fixtures'),
});

expect(tsxProcess.exitCode).toBe(0);
expect(tsxProcess.stdout).toBe('["hello","world","binary"]');
expect(tsxProcess.stderr).toBe('');
});
});
});
2 changes: 2 additions & 0 deletions tests/utils/tsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const tsx = (
options.args,
{
env: {
NODE_OPTIONS: '',
ESBK_DISABLE_CACHE: '1',
},
nodePath: options.nodePath,
Expand All @@ -36,6 +37,7 @@ export async function createNode(

return {
version: node.version,
path: node.path,
packageType: '',
get isCJS() {
return this.packageType === 'commonjs';
Expand Down