Repository: digital-loukoum/esrun Branch: main Commit: 10ad7f7602d3 Files: 42 Total size: 41.8 KB Directory structure: gitextract_3nngibti/ ├── .claude/ │ └── settings.json ├── .github/ │ └── workflows/ │ ├── claude.yml │ └── npm-publish.yaml ├── .gitignore ├── .vscode/ │ └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── scripts/ │ ├── build.js │ ├── deploy.js │ └── utilities/ │ └── bumpVersion.js ├── source/ │ ├── bin.ts │ ├── index.ts │ ├── plugins/ │ │ └── fileConstants.ts │ ├── runners/ │ │ ├── Runner.ts │ │ └── Watcher.ts │ ├── tools/ │ │ ├── findInputFile.ts │ │ ├── importRequire.ts │ │ ├── onExit.ts │ │ └── resolveDependency.ts │ └── types/ │ ├── CliOption.ts │ ├── Options.ts │ ├── Parameter.ts │ └── SendCodeMode.ts ├── test/ │ ├── index.ts │ ├── readline.js │ ├── readline.ts │ ├── resolvedAlias/ │ │ └── foo.ts │ ├── samples/ │ │ ├── Zabu.ts │ │ ├── backticksInComments.ts │ │ ├── coco.ts │ │ ├── dotImport/ │ │ │ ├── dotImport.ts │ │ │ ├── doubleDotImport/ │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── fileConstants.ts │ │ ├── ipc/ │ │ │ └── child.ts │ │ └── shebang.ts │ ├── server/ │ │ ├── index.ts │ │ └── message.ts │ └── tsconfig.json └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .claude/settings.json ================================================ { "permissions": { "allow": [ "WebFetch", "Bash" ], "deny": [] } } ================================================ FILE: .github/workflows/claude.yml ================================================ name: Claude PR Assistant on: issue_comment: types: [created] pull_request_review_comment: types: [created] issues: types: [opened, assigned] pull_request_review: types: [submitted] jobs: claude-code-action: if: | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || (github.event_name == 'issues' && contains(github.event.issue.body, '@claude')) runs-on: ubuntu-latest permissions: contents: read pull-requests: read issues: read id-token: write steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 1 - name: Run Claude PR Action uses: anthropics/claude-code-action@beta with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} timeout_minutes: "60" ================================================ FILE: .github/workflows/npm-publish.yaml ================================================ name: NPM Package on: push: branches: [main] pull_request: branches: [main] jobs: build: if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 registry-url: https://registry.npmjs.org/ # install dependencies - uses: actions/cache@v4 id: pnpm-cache with: path: ~/.pnpm-store key: ${{ runner.os }}-modules-${{ matrix.node-version }}-${{ hashFiles('**/package.json') }} - name: Install dependencies uses: pnpm/action-setup@v4 with: version: 9 run_install: true # build & test - name: Build run: npm run build # TODO: Change "build" to "test" when tests are ready env: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} publish: if: github.event_name == 'push' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 registry-url: https://registry.npmjs.org/ # install dependencies - uses: actions/cache@v4 id: pnpm-cache with: path: ~/.pnpm-store key: ${{ runner.os }}-modules-${{ matrix.node-version }}-${{ hashFiles('**/package.json') }} - name: Install dependencies uses: pnpm/action-setup@v4 with: version: 9 run_install: true # setup git config (for auto-version bumping) - name: Setup git config run: | git config user.name "GitHub actions" git config user.email "<>" # deploy to npm - name: Deploy run: npm run deploy env: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} ================================================ FILE: .gitignore ================================================ node_modules/ package/ ================================================ FILE: .vscode/settings.json ================================================ { "typescript.tsdk": "node_modules/typescript/lib" } ================================================ FILE: CHANGELOG.md ================================================ ## 3.2.27 - Move from `@digitak/esrun` to `esrun`. ## 3.2.25 - Fix `url` import that would conflict with user-defined url variables. ## 3.2.24 - Add `esuilbOptions` to the `esrun` function. ## 3.2.23 - Strip shebangs ## 3.2.22 - Don't use crypto anymore (not compatible for old node versions), use timestamp instead ## 3.2.21 - Use crypto.getRandomUUID() and remove deprecated cuid dependency - Generate source maps for better debugging ## 3.2.20 - Fix temporary file creation for windows and old node versions ## 3.2.19 - Add sudo mode ## 3.2.18 - Update esbuild version from `0.14` to `0.17` ## 3.2.17 - Use latest grubber version that fixes comments with backticks ## 3.2.16 - Fix Windows bug when running esrun from a npm script ## 3.2.15 - Re-add used dependency on cuid ## 3.2.14 - Fix a rare bug where esbuild would need a require function. Use `createRequire()` from node to emulate the require function - Remove unused dependency on cuid (is a dev dependency only) ## 3.2.13 - Fix a bug on temporary file mode if node_modules folder does not exist ## 3.2.12 - Fix usage with interactive CLI - On windows, the default mode creates a temporary flie that is then executed ## 3.2.11 - Improve documentation for CLI parameters - Add an error message when trying to use `--tsconfig` parameter with no value ## 3.2.10 - Fix file watching that would work only once on some OS - CLI arguments are passed using the '=' instead of ':' (the colon still work for retro compatibility) - You can now pass custom node's cli options by prefixing your option name with `--node-`. Example: `--node-max-old-space-size=4096` ## 3.2.9 - Fix an error with Windows when passing the code to node. Using stdin now instead of a cli argument. (thanks to **@vendethiel** for the fix) - Remove error swallowing that could happen when the node process itself crashes - Add a link to the changelog in the readme ## 3.2.8 - New strategy to detect external dependencies. Now check if paths are inside a parent `node_modules` directory instead of checking if the import start with ".", "/", "~"n "@/" or "$". The previous strategy used to fail for typescript aliases that didn't start with "@/", "~" or "$". ## 3.2.7 - Update EsBuild version to `0.14` ## 3.2.6 - Fix `.mts` and `.cjs` extensions - Better file watching. Do not use custom plugin anymore but EsBuild's metafile - Remove unused dependency `anymatch` - Re-watching updated dependencies is cleaner and does not need a debounce anymore (though it is sill kept as it can be useful in some cases) ## 3.2.5 - Add `--tsconfig:path` cli option ## 3.2.4 - Add support for file constants `__dirname` and `__filename` ## 3.2.3 - Add a message when calling cli with no arguments - Cli now catches esrun errors and log them instead of throwing ## 3.2.2 - Add `beforeRun` and `afterRun` events ## 3.2.1 - Add `preserveConsole` option to prevent console clear on watch mode - Add CI/CD with version auto-bumping - Make CLI options more extensible - Starting this changelog 🎉 ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 Digital Loukoums Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # esrun > **⚠️ DEPRECATION NOTICE** > > This package is being deprecated. We recommend using modern alternatives: > > - **For new projects:** Use [Bun](https://bun.sh/) instead of Node.js when possible. Bun offers exceptional performance, built-in TypeScript support without any configuration, and can run TypeScript files natively. It's a modern JavaScript runtime that eliminates the need for additional build tools or transpilers. > > - **For Node.js backends:** When you specifically need Node.js compatibility, use the official and well-maintained [tsx](https://github.com/esbuild-kit/tsx) package, which provides similar functionality with better maintenance and ecosystem support. **esrun** is a "work out of the box" library to execute Typescript (as well as modern Javascript with decorators and stuff) without having to use a bundler. This is useful for quick demonstrations or when launching your tests written in Typescript. This library is a thin wrapper around [esbuild](https://github.com/evanw/esbuild) which compiles Typescript almost instantly. **esrun** properly handles dependencies, handling importing both Typescript and Javascript libraries, and using either the CJS or the ESM format. Things just work as you would expect. ## Usage ### Global installation Install the library globally with your favorite package manager: ```shell npm i -g esrun ``` Then you can execute any Typescript file in the same way Node would execute a Javascript file: ```shell esrun foo.ts ``` You can pass arguments like any process: ```shell esrun foo.ts --option=bar --verbose -S ``` All file dependencies will be bundled and executed as well. External module dependencies won't be bundled, it's up to the `node` engine to resolve dependencies. ### Local installation Install the library locally with your favorite package manager. ```shell npm i -D esrun ``` Then you can use it in your `package.json` scripts: ```json { "scripts": { "test": "esrun test" } } ``` Running `npm run test` will run the first file that exists in the following list: - `/test.ts` - `/test/index.ts` - `/test/test.ts` - `/test/main.ts` - `/test.js` - `/test/index.js` - `/test/test.js` - `/test/main.js` ### CLI parameters syntax There are two kinds of parameters that you can pass to the esrun cli: 1. **esrun parameters**, that will impact how to execute your Typescript file, 2. and **program parameters**, that will be passed into **process.argv** to the file you want to execute. **esrun parameters** follow the same syntax convention as node, ie: - it must start with a double hyphen "--" - then be followed by the name of the parameter - then, if a value is necessary, use an equal symbol "=" followed by the value, without space. Example: ``` esrun --tsconfig=/path/to/tsconfig.json myFileToExecute.ts ``` All parameters that come **after** the file to execute are **program parameters** and will be sent via `process.argv`. In this example, `"foo"` and `"bar"` will be sent to `myFileToExecute`: ``` esrun myFileToExecute.ts foo bar ``` ### Custom tsconfig.json You can pass a custom path to your `tsconfig.json` file from the CLI: ``` esrun --tsconfig=/custom/path/to/tsconfig.json foo.ts ``` ### Top-level await **Esrun** is compatible with top-level await. To enable top-level await, you need to set the option `"module": "esnext"` in your tsconfig.json. ### Watch mode You can also execute **esrun** in watch mode. In watch mode, your file will automatically be re-executed every time itself or one of its dependencies is updated. ```shell esrun --watch foo.ts ``` > The `--watch` (or `-w`) option must be placed before the path of the file to execute. If you place it after the file path, it will be passed as an argument to `foo.ts` instead. This feature is very useful when you are doing test-driven development. You can just run `esrun --watch test.ts` and enjoy a live output of your changes right into your console. You may want to watch other files than your code files. For example, if you load data from a configuration file. In this case you can specify a glob (or a list of globs) that have to be watched: ```shell esrun --watch=src/*.json foo.ts ``` Then any `json` file in the `src/`folder will re-trigger the run. You can use several globs separated by a comma (but no space): ```shell esrun --watch=src/*.json,test/*.json foo.ts ``` #### Preventing console clearing In watch mode, every file change will trigger a console clearing. You can disable this behavior with the `--preserveConsole` option: ```shell esrun --watch --preserveConsole foo.ts ``` ### Inspect mode You can also execute **esrun** in inspect mode. When run in inspect mode, your code will be connected to the Webkit DevTools to benefit the power of the browser console instead of the terminal console. First, run your program in inspect mode: ```shell esrun --inspect foo.ts ``` Then open `about:inspect` in a **Chrome** / **Brave** / **Edge** browser. You should see your program running in the *Remote targets* section. Click on `Open dedicated DevTools for Node` and enjoy the browser console for your back-end program. In case of troubleshooting, read the [node documentation](https://nodejs.org/en/docs/guides/debugging-getting-started/). > Inspect and watch mode are alas not compatible yet. ### Sudo mode You can run the script in sudo mode: ``` esrun --sudo myScript.ts ``` ### Other node cli options `esrun` uses esbuild to transform Typescript to Javascript, and then Node to execute it. You can pass custom options to the node cli by prefixing the option name with "--node", like this: ``` esrun --node-max-old-space-size=4096 foo.ts esrun --node-no-warnings foo.ts ``` ### Importing a CJS module If you import a CJS module (like the `typescript` library itself), it's likely that you will need to set the [esModuleInterop](https://www.typescriptlang.org/tsconfig#esModuleInterop) flag in your `tsconfig.json` file: ```json { "compilerOptions": { "esModuleInterop": true } } ``` This will suppress the import errors from the Typescript compiler and allow you to write `import ts from "typescript"` instead of `import * as ts from "typescript"` - the latest syntax being not standard ESM. ### Using a directory as an entry point If the given entry point is a directory, the following actions will be executed in order to find the right entry file: - check if a package.json file exists with a `main` field. The entry file will be the value of the `main` field, relative to the package.json directory. - check if an `index.ts` file exists in the given directory. - check if an eponym file exists in the given directory. - check if an eponym file with the `.ts` extension exists in the given directory. - check if a `main.ts` file exists in the given directory. - check if a `index.js` file exists in the given directory. - check if an eponym file with the `.js` extension exists in the given directory. - check if a `main.js` file exists in the given directory. ## API The library exports a single function that you can use to programmatically execute a Typescript file. ``` ts import esrun from 'esrun' export async function esrun(filePath: string, options?: Options): Promise export type Options = { // arguments to pass to the script args?: string[] = [] // if true, will reload the script on file changes // you can also pass an additional array of globs to watch watch?: boolean | string[] = false // if true, prevent console clearing on watch mode preserveConsole?: boolean // if true, turn on inspect mode to use browser's console inspect?: boolean = false // if false, do not transform __dirname and __filename // (the CLI option to disable file constants is --noFileConstants) fileConstants?: boolean = true // if false, external packages will be bundled makeAllPackagesExternal?: boolean = true // if false, process.exit() won't be called after execution exitAfterExecution?: boolean = true // enable use of process.send() from the children interProcessCommunication?: boolean = false // additional options to pass to node's cli nodeOptions?: Record = {} // indicate the mode to use to run code // "cliParameters" means the code is passed to the node program // as parameters // "temporaryFile" means a temporary file is created inside // the node_modules folder, then executed, then deleted sendCodeMode?: "cliParameters" | "temporaryFile" // executed before the code is executed (after the build) beforeRun?: () => unknown // executed after the code is executed afterRun?: () => unknown } ``` ### Create a new runner to get / transform generated code To have full control, you can create your own script runner instance: ```ts import { Runner } from 'esrun' const runner = new Runner(inputFile: string, options?: Options) // build the given file and all its dependencies await runner.build(buildOptions?: BuildOptions) // you can see what the generated code is console.log("Generated javascript code:", runner.outputCode) // you can apply transformations to the code await runner.transform(code => `console.log('Hello world!');\n` + code) // then execute the build and return the given status const status = await runner.execute() ``` ### Receive data You can receive data from a script you executed by turning on the option `interProcessCommunication`. When the option is on, the script will be able to call [process.send(message: string)](https://nodejs.org/api/process.html#processsendmessage-sendhandle-options-callback). At the end of the execution, the sent messages will be disponible through `runner.output`. Let's suppose you have the following file `helloWorld.ts`: ```ts // helloWorld.ts process.send('Hello world') ``` Then you can receive data fro this script this way: ```ts const runner = new Runner('helloWorld.ts', { interProcessCommunication: true }) await runner.execute({ exitAfterExecution: false }) console.log("Received data:", runner.output) // should log 'Received data: Hello world' ``` ## Changelog The changelog is available [here](https://github.com/digital-loukoum/esrun/blob/main/CHANGELOG.md). ================================================ FILE: package.json ================================================ { "name": "esrun", "version": "3.2.31", "type": "module", "description": "Execute directly your Typescript files using Esbuild", "main": "./index.js", "exports": { ".": "./index.js", "./*": "./*.js" }, "bin": { "esrun": "./bin.js" }, "scripts": { "lint": "tsc --noEmit", "build": "node scripts/build.js", "deploy": "node scripts/deploy.js", "dev": "./package/bin.js --watch test --watch coco", "test": "./package/bin.js test --watch coco", "test:tsconfig": "./package/bin.js --tsconfig test/tsconfig.json test --watch coco", "test:node-cli": "./package/bin.js --node-max-old-space-size=4096 test --watch coco", "test:readline": "./package/bin.js test/readline", "test:watch": "./package/bin.js --watch=test/*.json test --watch coco", "test:watch:preserve": "./package/bin.js --preserveConsole --watch=test/*.json test --watch coco", "test:inspect": "./package/bin.js --inspect test --watch coco", "test:watch:inspect": "./package/bin.js --watch --inspect test --watch coco", "test:server": "./package/bin.js --watch test/server" }, "engines": { "node": ">=14.0" }, "repository": { "type": "git", "url": "git+https://github.com/digital-loukoum/esrun.git" }, "keywords": [ "esbuild", "run", "execute", "typescript" ], "author": "Lepzulnag", "license": "ISC", "bugs": { "url": "https://github.com/digital-loukoum/esrun/issues" }, "homepage": "https://github.com/digital-loukoum/esrun#readme", "dependencies": { "@digitak/grubber": "^3.1.4", "chokidar": "^3.5.1", "esbuild": "^0.24.2" }, "devDependencies": { "@digitak/bunker": "^3.2.1", "@types/node": "^14.14.20", "cute-print": "^1.0.4", "fartest": "^2.1.6", "fsevents": "^2.3.2", "polka": "^0.5.2", "typescript": "^4.7.0-dev.20220421" } } ================================================ FILE: scripts/build.js ================================================ import { execSync } from "child_process" import { rmSync, chmodSync, copyFileSync } from "fs" console.log("Cleaning library...") rmSync("package", { recursive: true, force: true }) console.log("Compiling typescript...") execSync("tsc", { stdio: "inherit" }) console.log("Copying configuration files...") copyFileSync("./README.md", "./package/README.md") copyFileSync("./package.json", "./package/package.json") console.log("Making binary executable...") chmodSync("package/bin.js", 0o775) console.log("✨ Build done\n") ================================================ FILE: scripts/deploy.js ================================================ import { execSync } from "child_process" import { bumpVersion } from "./utilities/bumpVersion.js" console.log("Bumping version...") const version = bumpVersion() execSync(`git add .`) execSync(`git commit -m "📌 Version ${version}"`) execSync(`git push`) import "./build.js" console.log(`Starting deploy...`) try { execSync(`npm publish`, { cwd: "./package" }) } catch (error) { console.log(`[--- An error occured during deploy ---]`) console.log(error, "\n") process.exit(1) } console.log(`\nDeploy done 🎉\n`) ================================================ FILE: scripts/utilities/bumpVersion.js ================================================ import fs from "fs" export function bumpVersion() { const file = "package.json" const packageInfos = JSON.parse(fs.readFileSync(file, "utf8")) const version = packageInfos.version.split(".").map(value => +value) version[2]++ packageInfos.version = version.join(".") fs.writeFileSync(file, JSON.stringify(packageInfos, null, "\t")) const newVersion = version.join(".") return newVersion } ================================================ FILE: source/bin.ts ================================================ #!/usr/bin/env node import esrun from "./index.js"; import { CliOption } from "./types/CliOption.js"; import { Parameter } from "./types/Parameter.js"; const { argv } = process; const nodeOptionPrefix = "--node-"; const argumentOptions: Record = { "--watch": "watch", "-w": "watch", "--inspect": "inspect", "-i": "inspect", "--preserveConsole": "preserveConsole", "--noFileConstants": "noFileConstants", "--tsconfig": "tsconfig", "--send-code-mode": "sendCodeMode", "--sudo": "sudo", }; const options: Record & { node: Record; } = { watch: false, inspect: false, preserveConsole: false, noFileConstants: false, sudo: false, tsconfig: undefined, node: {}, sendCodeMode: undefined, }; let argsOffset = 2; let argument: string; if (argv.length < argsOffset) { console.log("Missing typescript input file"); process.exit(0); } while ((argument = argv[argsOffset]).startsWith("--")) { const [command, parameters] = getCommandAndParameters(argument); if (command in argumentOptions) { options[argumentOptions[command]] = parameters; argsOffset++; } else if (command.startsWith(nodeOptionPrefix)) { options.node[command.slice(nodeOptionPrefix.length)] = parameters; argsOffset++; } else { console.log(`Unknown option ${command}`); process.exit(9); } } if (typeof options.tsconfig === "boolean") { console.log( "Missing value for the '--tsconfig' parameter. Did you forget to add a \"=\"?", ); console.log( "Example of valid syntax: esrun --tsconfig=/path/to/my/tsconfig.json fileToExecute.ts", ); process.exit(9); } const tsConfigFile = options.tsconfig instanceof Array ? options.tsconfig.join(",") : options.tsconfig; esrun(argv[argsOffset], { args: argv.slice(argsOffset + 1), watch: options.watch, tsConfigFile, inspect: !!options.inspect, preserveConsole: !!options.preserveConsole, fileConstants: !options.noFileConstants, nodeOptions: options.node, sendCodeMode: !Array.isArray(options.sendCodeMode) ? undefined : options.sendCodeMode[0] === "cliParameters" ? "cliParameters" : options.sendCodeMode[0] === "temporaryFile" ? "temporaryFile" : undefined, }).catch((error) => { console.error(error); process.exit(1); }); function getCommandAndParameters(argument: string): [string, Parameter] { let colonIndex = argument.indexOf(":"); if (colonIndex === -1) colonIndex = Infinity; let equalIndex = argument.indexOf("="); if (equalIndex === -1) equalIndex = Infinity; const separatorIndex = Math.min(colonIndex, equalIndex); if (separatorIndex === Infinity) return [argument, true]; return [ argument.slice(0, separatorIndex), argument.slice(separatorIndex + 1).split(","), ]; } ================================================ FILE: source/index.ts ================================================ import Runner from "./runners/Runner.js" import Watcher from "./runners/Watcher.js" import { Options } from "./types/Options.js" export { Runner, Watcher, esrun, Options } /** * Run any .ts or .js file */ export default async function esrun(inputFile: string, options?: Options) { if (options?.watch && options?.inspect) { console.warn( `--inspect and --watch options are not compatible together. Disabling watch mode.` ) options.watch = false } return new (options?.watch ? Watcher : Runner)(inputFile, options).run() } ================================================ FILE: source/plugins/fileConstants.ts ================================================ import { Plugin, Loader } from "esbuild"; import { posix } from "path"; import process from "process"; import fs from "fs"; import { grub } from "@digitak/grubber"; export type FileConstantsPluginOptions = Record; export const fileConstantsPlugin = ( options: FileConstantsPluginOptions = {}, ): Plugin => ({ name: "fileConstants", setup(build) { build.onLoad( { filter: /.\.(c|m)?(js|ts)x?$/, namespace: "file" }, async (options) => { const isWindows = /^win/.test(process.platform); const escapeBackslashes = (path: string) => isWindows ? path.replace(/\\/g, "/") : path; const filename = escapeBackslashes(options.path); const dirname = posix.dirname(options.path); const fileContent = fs.readFileSync(options.path, "utf8"); const contents = grub(fileContent).replace( { from: "__dirname", to: `"${dirname}"` }, { from: "__filename", to: `"${filename}"` }, ); let loader = posix.extname(options.path).slice(1) as Loader; if (["m", "c"].includes(loader[0])) loader = loader.slice(1) as Loader; return { contents, loader, }; }, ); }, }); ================================================ FILE: source/runners/Runner.ts ================================================ import { BuildOptions, Plugin, context, type BuildContext } from "esbuild"; import type { BuildResult, OutputFile } from "esbuild"; import { ChildProcess, spawn } from "child_process"; import findInputFile from "../tools/findInputFile.js"; import { Options } from "../types/Options.js"; import { fileConstantsPlugin } from "../plugins/fileConstants.js"; import path, { posix } from "path"; import { SendCodeMode } from "../types/SendCodeMode.js"; import { unlinkSync, writeFileSync } from "fs"; import { importRequire } from "../tools/importRequire.js"; import { grub } from "@digitak/grubber"; export type BuildOutput = | null | (BuildResult & { outputFiles: OutputFile[]; }); export default class Runner { public inputFile: string; public outputFile: undefined | string = undefined; // temporary output file public output = ""; public stdout = ""; public stderr = ""; public outputCode = ""; public args: string[] = []; public tsConfigFile: string | undefined; public preserveConsole: boolean; public fileConstants: boolean; public beforeRun: Options["beforeRun"]; public afterRun: Options["afterRun"]; public nodeOptions: Options["nodeOptions"] = {}; public sendCodeMode: SendCodeMode; public sudo: boolean; public esbuildOptions: BuildOptions; protected watched: boolean | string[]; protected inspect: boolean; protected interProcessCommunication; protected makeAllPackagesExternal; protected exitAfterExecution; protected buildContext?: BuildContext; protected buildOutput?: BuildResult; protected dependencies: string[] = []; protected childProcess?: ChildProcess; getDependencies(): readonly string[] { return this.dependencies; } protected retrieveDependencies(): string[] { return Object.keys(this.buildOutput?.metafile?.inputs ?? []).map((input) => posix.resolve(input), ); } constructor(inputFile: string, options?: Options) { this.inputFile = findInputFile(inputFile); this.args = options?.args ?? []; this.sudo = options?.sudo ?? false; this.watched = options?.watch ?? false; this.preserveConsole = options?.preserveConsole ?? false; this.inspect = options?.inspect ?? false; this.fileConstants = options?.fileConstants ?? true; this.tsConfigFile = options?.tsConfigFile; this.interProcessCommunication = options?.interProcessCommunication ?? false; this.makeAllPackagesExternal = options?.makeAllPackagesExternal ?? true; this.exitAfterExecution = options?.exitAfterExecution ?? true; this.beforeRun = options?.beforeRun; this.afterRun = options?.afterRun; this.nodeOptions = options?.nodeOptions ?? {}; this.nodeOptions = options?.nodeOptions ?? {}; this.esbuildOptions = options?.esbuildOptions ?? {}; this.sendCodeMode = options?.sendCodeMode ?? process.platform === "win32" ? "temporaryFile" : "cliParameters"; } async run() { try { await this.build(); const status = await this.execute(); if (this.exitAfterExecution) { process.exit(status); } } catch (error) { console.error(error); process.exit(1); } } async build(buildOptions: BuildOptions = {}) { const plugins: Plugin[] = []; if (this.fileConstants) { plugins.push(fileConstantsPlugin()); } try { this.buildContext = await context({ entryPoints: [this.inputFile], bundle: true, platform: "node", format: "esm", plugins, sourcemap: "inline", sourceRoot: process.cwd(), tsconfig: this.tsConfigFile, external: this.makeAllPackagesExternal ? [ "./node_modules/*", "../node_modules/*", "../../node_modules/*", "../../../node_modules/*", "../../../../node_modules/*", "../../../../../node_modules/*", "../../../../../../node_modules/*", "../../../../../../../node_modules/*", "../../../../../../../../node_modules/*", "../../../../../../../../../node_modules/*", "../../../../../../../../../../node_modules/*", ] : [], ...this.esbuildOptions, ...buildOptions, write: false, metafile: true, }); this.buildOutput = await this.buildContext?.rebuild(); this.outputCode = this.getOutputCode(); // remove shebangs this.outputCode = grub(this.outputCode).replace( { from: /^#!.*$/gm, to: "" }, ) this.dependencies = this.retrieveDependencies(); } catch (error) { // No need to log the error as it has already been done by esbuild. this.buildOutput = undefined; this.outputCode = ""; } } async transform(transformer: (content: string) => string | Promise) { this.outputCode = await transformer(this.outputCode); } async execute(): Promise { this.output = this.stdout = this.stderr = ""; if (!this.buildOutput) return 1; let code = this.outputCode; let command = "node"; let commandArgs: string[] = ["--enable-source-maps"]; for (const nodeOption in this.nodeOptions) { let argument = `--${nodeOption}`; const parameters = this.nodeOptions[nodeOption]; if (Array.isArray(parameters)) { argument += `=${parameters.join(",")}`; } commandArgs.push(argument); } if (this.inspect) { commandArgs.push("--inspect"); code = `setTimeout(() => console.log("Process timeout"), 3_600_000);\n${code}`; } const evalArgs: Array = []; if (this.sendCodeMode === "temporaryFile") { // we create a temporary file that we will execute const uniqueId = Date.now(); this.outputFile = path.join(process.cwd(), `esrun-${uniqueId}.tmp.mjs`); code = importRequire(code, this.outputFile); code = `process.argv = [process.argv[0], ...process.argv.slice(3)];\n${code}`; writeFileSync(this.outputFile, code); evalArgs.push(this.outputFile); } else { code = importRequire(code, posix.resolve("index.js")); // we pass the code directly from the command line evalArgs.push("--input-type=module", "--eval", code); } commandArgs.push(...evalArgs, "--", this.inputFile, ...this.args); if (this.sudo) { commandArgs = [command, ...commandArgs]; command = "sudo"; } await this.beforeRun?.(); try { this.childProcess = spawn(command, commandArgs, { stdio: this.interProcessCommunication ? ["pipe", "pipe", "pipe", "ipc"] : "inherit", }); if (this.interProcessCommunication) { this.childProcess?.on("message", (message) => { this.output += message.toString(); }); this.childProcess?.stdout?.on("data", (data) => { this.stdout += data.toString(); }); this.childProcess?.stderr?.on("data", (data) => { this.stderr += data.toString(); }); } return new Promise((resolve) => { const done = async (code?: number) => { await this.afterRun?.(); if (this.outputFile) { unlinkSync(this.outputFile); } resolve(code ?? 0); }; this.childProcess?.on("close", done); this.childProcess?.on("error", async (error) => { console.error(error); done(1); }); }); } catch (error) { console.error(error); return 1; } } getOutputCode() { return this.buildOutput?.outputFiles?.[0]?.text || ""; } } ================================================ FILE: source/runners/Watcher.ts ================================================ import Runner, { BuildOutput } from "./Runner.js"; import { watch } from "chokidar"; import type { FSWatcher } from "chokidar"; import { posix } from "path"; import { Options } from "../types/Options.js"; function debounce(func: Function, wait: number) { let timeout: NodeJS.Timeout | null = null; return function (...parameters: unknown[]) { if (timeout) clearTimeout(timeout); else func(...parameters); timeout = setTimeout(() => (timeout = null), wait); }; } export default class Watcher extends Runner { protected watcher: FSWatcher | null = null; protected watched: string[] = []; constructor(input: string, options?: Options) { super(input, options); this.watched = options?.watch instanceof Array ? options.watch.map((glob) => posix.resolve(glob)) : []; } async run() { try { console.clear(); await this.build(); this.execute(); this.watch(); } catch (error) {} } async rerun() { if (!this.watcher) throw "Cannot re-run before a first run"; if (this.childProcess) { this.childProcess?.kill("SIGINT"); this.childProcess = undefined; } await this.rebuild(); // we update the list of watched files if (this.buildOutput) { this.watch(); } await this.execute(); } async rebuild() { if (!this.preserveConsole) { console.clear(); } this.dependencies.length = 0; if (this.buildOutput) { try { this.buildOutput = await this.buildContext?.rebuild(); this.outputCode = this.getOutputCode(); this.dependencies = this.retrieveDependencies(); } catch (error) { this.buildOutput = undefined; this.outputCode = ""; } } else { await this.build(); } } watch() { void this.watcher?.close(); this.watcher = watch([ ...this.dependencies, "package.json", ...this.watched, ]); this.watcher.on("change", debounce(this.rerun.bind(this), 300)); this.watcher.on("unlink", debounce(this.rerun.bind(this), 300)); } } ================================================ FILE: source/tools/findInputFile.ts ================================================ import { existsSync, statSync, readFileSync } from "fs"; import { posix } from "path"; export default function findInputFile(path: string): string { if (!existsSync(path)) { if (existsSync(`${path}.ts`)) path = `${path}.ts`; else if (existsSync(`${path}.js`)) path = `${path}.js`; else throw `Path '${path}' does not exist`; } const stat = statSync(path); if (stat.isFile()) return path; else if (stat.isDirectory()) { // first we check if there is a package.json file with a `main` key const packageFile = posix.resolve(path, "package.json"); if (existsSync(packageFile) && statSync(packageFile).isFile()) { const { main } = JSON.parse(readFileSync(packageFile, "utf8")); if (main) return findInputFile(posix.resolve(path, main)); } // otherwise we look for a default entry point const name = posix.basename(path); for (const subpath of [ posix.resolve(path, "index.ts"), posix.resolve(path, name), posix.resolve(path, `${name}.ts`), posix.resolve(path, "main.ts"), posix.resolve(path, "index.js"), posix.resolve(path, `${name}.js`), posix.resolve(path, "main.js"), ]) if (existsSync(subpath) && statSync(subpath).isFile()) return subpath; throw `Could not resolve an entry point in folder '${path}`; } else throw `Path '${path}' should be a file or a directory`; } ================================================ FILE: source/tools/importRequire.ts ================================================ export function importRequire(code: string, location: string) { // return `import { createRequire } from "module";\nconst require = createRequire("${location}");\n` + code return ` import __esrun_url from 'url';\n import { createRequire as __esrun_createRequire } from "module";\n const __esrun_fileUrl = __esrun_url.pathToFileURL("${location}");\n const require = __esrun_createRequire(__esrun_fileUrl);\n${code} ` } ================================================ FILE: source/tools/onExit.ts ================================================ export default function onExit(cleanUp: Function) { // const events = [`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `uncaughtException`, `SIGTERM`] for (const event of [ `exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `uncaughtException`, `SIGTERM`, ]) { process.on(event, () => cleanUp(event)) } } ================================================ FILE: source/tools/resolveDependency.ts ================================================ import { createRequire } from "module" const require = createRequire(process.cwd()) export default (dependency: string) => require.resolve(dependency, { paths: [process.cwd()] }) ================================================ FILE: source/types/CliOption.ts ================================================ export type CliOption = | "watch" | "inspect" | "preserveConsole" | "noFileConstants" | "tsconfig" | "sendCodeMode" | "sudo"; ================================================ FILE: source/types/Options.ts ================================================ import { BuildOptions } from "esbuild"; import { Parameter } from "./Parameter.js"; import { SendCodeMode } from "./SendCodeMode.js"; export type Options = { args?: string[]; watch?: boolean | string[]; preserveConsole?: boolean; inspect?: boolean; interProcessCommunication?: boolean; makeAllPackagesExternal?: boolean; exitAfterExecution?: boolean; fileConstants?: boolean; tsConfigFile?: string; sendCodeMode?: SendCodeMode; sudo?: boolean; beforeRun?: () => unknown; afterRun?: () => unknown; nodeOptions?: Record; esbuildOptions?: BuildOptions; }; ================================================ FILE: source/types/Parameter.ts ================================================ export type Parameter = string[] | true ================================================ FILE: source/types/SendCodeMode.ts ================================================ export type SendCodeMode = "cliParameters" | "temporaryFile" ================================================ FILE: test/index.ts ================================================ import Zabu from "~/samples/Zabu"; import coco from "~/samples/coco"; import ts from "typescript"; import fsevents from "fsevents"; import { bunker } from "@digitak/bunker"; import { bunkerFile } from "@digitak/bunker/io"; import start from "fartest"; import print from "cute-print"; import Runner from "../source/runners/Runner"; import dotImport from "./samples/dotImport/dotImport"; import doubleDotImport from "./samples/dotImport/doubleDotImport"; import { foo } from "alias/foo"; start(async ({ stage, same, test }) => { stage("CLI arguments received"); same(process.argv[3], "coco"); stage("CLI arguments received [--watch is passed as argument]"); same(process.argv[2], "--watch"); stage("Import typescript library"); same(ts.SyntaxKind.EndOfFileToken, 1); stage("Import fsevents"); same(!!fsevents.watch, true); stage("Import CJS library"); same(typeof print, "function"); stage("Import ESM library"); same(bunker(3), Uint8Array.of(5, 3)); stage("Import specific file in library"); same(typeof bunkerFile, "function"); stage("Import custom typescript file"); same(new Zabu().yell(), "ZABU"); stage("Import another custom typescript file"); same(coco, 11); stage("import '.' syntax"); same(dotImport, 12); stage("import '..' syntax"); same(doubleDotImport, 12); stage("Backticks in comments"); { const runner = new Runner("test/samples/backticksInComments"); await runner.build(); } stage("File constants"); { const runner = new Runner("test/samples/fileConstants"); await runner.build(); test( runner.outputCode.includes( `["/Users/Lepzulnag/Code/esrun/test/samples", "/Users/Lepzulnag/Code/esrun/test/samples/fileConstants.ts", "__dirname", "__filename"]`, ), "__filename and __dirname are transformed", ); } stage("Temporary file mode"); { const runner = new Runner("test/samples/fileConstants", { sendCodeMode: "temporaryFile", }); await runner.build(); test( runner.outputCode.includes( `["/Users/Lepzulnag/Code/esrun/test/samples", "/Users/Lepzulnag/Code/esrun/test/samples/fileConstants.ts", "__dirname", "__filename"]`, ), "__filename and __dirname are transformed in temporary file mode", ); } stage("Use transformer"); { const runner = new Runner("test/samples/coco"); await runner.build(); await runner.transform((content) => content.replace("11", "12")); test(runner.outputCode.includes("12"), "Should export 12"); } stage("Inter process communication"); { const runner = new Runner("test/samples/ipc/child", { interProcessCommunication: true, }); await runner.build(); await runner.execute(); same(runner.output, "Hello", "process.send"); } stage("Build events"); { const messages = []; const runner = new Runner("test/samples/coco", { beforeRun: () => messages.push("beforeRun"), afterRun: () => messages.push("afterRun"), }); await runner.build(); await runner.execute(); same(messages, ["beforeRun", "afterRun"]); } stage("alias"); { same(foo, "foo", "resolved alias not treated as a dependency"); } }); ================================================ FILE: test/readline.js ================================================ // test/readline.ts import readline from "readline" async function input(msg) { return new Promise(resolve => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }) rl.question(msg, answer => { rl.close() resolve(answer) }) }) } try { console.log(await input("test")) } catch (error) { console.log("error", error) } ================================================ FILE: test/readline.ts ================================================ // When using readline or any other command line input library (I tested prompt-sync, cli-select, inquirer, prompts and readline) the program instantly exits without any errors // here's some code that results in the issue // main.ts import readline from 'readline' async function input(msg: string): Promise { return new Promise((resolve) => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }) rl.question(msg, (answer) => { rl.close() resolve(answer) } ) } ) } try { console.log(await input("test")) } catch (error) { console.log("error", error) } // input("test").then(() => { // console.log("This line will never be printed") // }) ================================================ FILE: test/resolvedAlias/foo.ts ================================================ export const foo = "foo" ================================================ FILE: test/samples/Zabu.ts ================================================ export default class Zabu { yell() { return "ZABU" } } ================================================ FILE: test/samples/backticksInComments.ts ================================================ // `/a` // `/b` ================================================ FILE: test/samples/coco.ts ================================================ export default 11 ================================================ FILE: test/samples/dotImport/dotImport.ts ================================================ import { randomExportedValue } from "." export default randomExportedValue ================================================ FILE: test/samples/dotImport/doubleDotImport/index.ts ================================================ import { randomExportedValue } from ".." export default randomExportedValue ================================================ FILE: test/samples/dotImport/index.ts ================================================ export const randomExportedValue = 12 ================================================ FILE: test/samples/fileConstants.ts ================================================ export default [__dirname, __filename, "__dirname", "__filename"] ================================================ FILE: test/samples/ipc/child.ts ================================================ process.send("Hello") process.stdout.write("world") process.disconnect() ================================================ FILE: test/samples/shebang.ts ================================================ #! hello i'm a shebang export const shebang = "Shebangs are stripped" ================================================ FILE: test/server/index.ts ================================================ import polka from "polka" import { message } from "./message" const port = 3000 polka() .get("/", (_, response) => response.end(message)) .listen(port, (error: Error) => { if (error) throw error console.log(`> Running on localhost:${port}`) }) ================================================ FILE: test/server/message.ts ================================================ export const message = "Hellow from Polka server :)" ================================================ FILE: test/tsconfig.json ================================================ { "compilerOptions": { "esModuleInterop": true, "module": "esnext", "moduleResolution": "Node", "paths": { "~/*": [ "./*" ], "alias/*": [ "./resolvedAlias/*" ], } }, } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "moduleResolution": "NodeNext", "skipLibCheck": true, "target": "es2020", "lib": [ "es2020", "dom" ], "module": "esnext", "strict": true, "declaration": true, "esModuleInterop": true, "outDir": "package", }, "include": [ "source" ], "exclude": [ "test" ] }