translated to typescript and added .ts file support
This commit is contained in:
2022-06-14 12:13:28 -05:00
commit 832cecfe76
15 changed files with 1667 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules
lib
dist
testing

2
.prettierignore Normal file
View File

@@ -0,0 +1,2 @@
lib/*
pnpm-lock.yaml

10
.prettierrc Normal file
View File

@@ -0,0 +1,10 @@
{
"printWidth": 80,
"tabWidth": 4,
"useTabs": false,
"semi": false,
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "avoid"
}

48
eslintrc.json Normal file
View File

@@ -0,0 +1,48 @@
{
"root": true,
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/strict-boolean-expressions": [
2,
{
"allowString": false,
"allowNumber": false
}
],
"prefer-const": "error",
"quotes": ["error", "single"],
"block-scoped-var": "error",
"camelcase": "error",
"consistent-this": ["error", "that"],
"no-else-return": "error",
"no-eq-null": "error",
"no-floating-decimal": "error",
"no-implicit-coercion": "error",
"no-implied-eval": "error",
"no-invalid-this": "error",
"require-await": "error",
"yoda": "error",
"semi": ["error", "always"],
"semi-style": ["error", "last"],
"no-unreachable-loop": "error",
"no-unused-private-class-members": "error",
"no-use-before-define": "error",
"no-unmodified-loop-condition": "error",
"no-duplicate-imports": "error",
"no-promise-executor-return": "error",
"no-self-compare": "error",
"no-constructor-return": "error",
"no-template-curly-in-string": "error",
"array-callback-return": "error",
"no-eval": "error",
"no-extend-native": "error",
"no-extra-bind": "error"
},
"ignorePatterns": ["lib/**/*.test.ts"]
}

38
package.json Normal file
View File

@@ -0,0 +1,38 @@
{
"name": "file-node",
"version": "1.1.0",
"description": "",
"module": "./lib/index.js",
"main": "./lib/index.js",
"scripts": {
"start": "node .",
"prebuild": "node -p \"'export const BIN_VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/util/version.ts",
"buildts": "prettier --write . && tsc -p ./src",
"build": "npm run buildts && pkg ."
},
"pkg": {
"targets": [
"node16-linux-x64",
"node16-macos-x64",
"node16-win-x64"
],
"outputPath": "dist",
"compress": "gzip"
},
"author": "",
"license": "UNLICENSE",
"bin": {
"file-node": "./lib/index.js"
},
"dependencies": {
"command-line-args": "^5.2.1"
},
"devDependencies": {
"@types/command-line-args": "^5.2.0",
"@types/node": "^17.0.42",
"esbuild": "^0.14.43",
"pkg": "^5.7.0",
"prettier": "^2.6.2",
"typescript": "^4.7.3"
}
}

1245
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

41
readme.md Normal file
View File

@@ -0,0 +1,41 @@
# file-node
an objective improvement to the defualt node repl
## features
you can initialize a repl by running a javscript file and using variables from there
## installation
- download the executable version for your os and rename it to `file-node` (or `file-node.exe` on windows)
- put it in a folder in your path
## usage
```text
file-node [options] <script.js>
Options:
-V, --verbose Verbose output
-v, --version Print version
-h, --help Show this help message
```
### notes
inside `<script.js>`, any variables you want to access from the repl must be either global (i.e. no `var`/`let`/`const`, prefixing the variable with `global.` also works) or exported (i.e. `export let a = 'hi'`)
no `await` at the top level of the repl
## file scope structure
```text
index
vm
repl
```
other files are scope-ambiguous and do not matter
`util/version.ts` is auto generated

44
src/build.ts Normal file
View File

@@ -0,0 +1,44 @@
import { transformSync } from 'esbuild'
import { buildSync } from 'esbuild'
import { parse } from 'path'
export type SupportedLoader = 'ts' | 'js'
export function supportedExt(file: string): SupportedLoader | false {
const ext = parse(file).ext.substring(1)
if (ext === 'js' || ext === 'ts') return ext as SupportedLoader // 'js' and 'ts' are both in there
return false
}
export function readScript(file: string): string {
const loader = supportedExt(file)
if (!loader) {
throw new Error(`Unsupported file type: ${file}`)
}
let result = buildSync({
entryPoints: [file],
target: 'node16',
platform: 'node',
format: 'cjs',
write: false,
})
return result.outputFiles[0].text
}
export function readInput(text: string, loader: SupportedLoader): string {
let result = transformSync(text, {
target: 'node16',
// platform: 'node',
format: 'cjs',
loader,
})
if (result.warnings.length > 0) {
throw new Error(`Errors in input: ${result.warnings.join('\n')}`)
}
return result.code
}

3
src/index.ts Normal file
View File

@@ -0,0 +1,3 @@
import { runVM } from './vm'
runVM()

52
src/repl.ts Normal file
View File

@@ -0,0 +1,52 @@
import * as repl from 'repl'
import { Context, createContext, runInContext } from 'vm'
import { readInput, SupportedLoader } from './build'
import { logv } from './util'
export function runRepl(
context: Context,
loader: SupportedLoader,
exported?: string[]
) {
if (exported) {
console.log(`Exports: ${exported.join(', ')}`)
}
console.log(`REPL Loader: ${loader.toUpperCase()}`)
const r = repl
.start({
useGlobal: true,
eval: (cmd, context, _filename, callback) => {
//add async wrapper
// context.__FILE_NODE__INTERNAL__ = callback
// cmd = `(async () => {
// ${cmd}
// })()`
let js = readInput(cmd, loader)
logv(`Transformed: ${js}`)
try {
let val = runInContext(js, context)
callback(null, val)
} catch (e) {
if (e instanceof Error) {
callback(e, null)
} else {
callback(new Error((e as any).toString()), null)
}
}
},
})
.on('close', () => process.exit(0))
Object.keys(context).forEach(key =>
Object.defineProperty(r.context, key, {
configurable: false,
enumerable: true,
value: context[key],
})
)
createContext(r.context)
}

18
src/tsconfig.json Normal file
View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"strict": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"sourceMap": true,
"rootDir": ".",
"outDir": "../lib",
"moduleResolution": "node",
"resolveJsonModule": true
}
}

44
src/util/args.ts Normal file
View File

@@ -0,0 +1,44 @@
import commandLineArgs from 'command-line-args'
import { BIN_VERSION } from './version'
export interface CLIArgsT {
verbose: boolean
version: boolean
help: boolean
script: string
}
const optionDefinitions = [
{ name: 'verbose', alias: 'V', type: Boolean, defaultValue: false },
{ name: 'version', alias: 'v', type: Boolean, defaultValue: false },
{ name: 'help', alias: 'h', type: Boolean, defaultValue: false },
{ name: 'script', defaultOption: true },
]
const Options = commandLineArgs(optionDefinitions)
if (Options.help) {
console.log(`
Usage: file-node [options] <script.js>
Options:
-V, --verbose Verbose output
-v, --version Print version
-h, --help Show this help message
`)
process.exit(0)
}
if (Options.version) {
console.log(BIN_VERSION)
process.exit(0)
}
if (!Options.script) {
console.log('Usage: file-node [options] <script.js>')
process.exit(1)
}
export function getArgs() {
return Options
}

7
src/util/index.ts Normal file
View File

@@ -0,0 +1,7 @@
import { getArgs } from './args'
export function logv(...args: any[]) {
if (getArgs().verbose) {
console.log(...['VERBOSE', ...args])
}
}

1
src/util/version.ts Normal file
View File

@@ -0,0 +1 @@
export const BIN_VERSION = '1.0.1'

110
src/vm.ts Normal file
View File

@@ -0,0 +1,110 @@
import { existsSync, readFileSync } from 'fs'
import { dirname, join, parse } from 'path'
import { REPLServer } from 'repl'
import { pathToFileURL } from 'url'
import { runInContext, createContext, RunningScriptOptions, Script } from 'vm'
import { logv } from './util'
import { getArgs } from './util/args'
import { readScript, supportedExt } from './build'
import { runRepl } from './repl'
// TODO possible `export` solution
// i might be able to convert TS to CJS and custom define `module.exports` into something i can pass to the REPL for context
export function runVM() {
const Options = getArgs()
const path = parse(Options.script)
const file = join(path.dir, path.base)
const content = readScript(file)
const loader = supportedExt(file)
if (!loader) {
throw new Error(`Unsupported file type: ${file}`)
}
const context: any = {
...global,
__dirname: process.cwd(),
__filename: join(process.cwd(), file),
// exports: {},
// module,
console,
require: (module: string) => {
logv(`Requiring ${module}`)
try {
return require(module)
} catch (e) {
if (module.startsWith('.')) {
return require(join(process.cwd(), path.dir, module))
} else {
return require(join(process.cwd(), 'node_modules', module))
}
}
},
}
logv(`Running ${file}`)
context.__FILE_NODE__INTERNAL__ = function (cont: any, exp: any, mod: any) {
logv(`Done`)
let context = {
...cont,
...exp,
...mod.exports,
}
let exported = Object.keys({ ...exp, ...mod.exports })
runRepl(context, loader, exported)
}
REPLServer.prototype.context
const script = `(async function() {
const exports = {}
const module = {exports: {}}
${content}
__FILE_NODE__INTERNAL__(this, exports, module);
})().catch(e => {console.error('script error', e)})`
logv(script)
createContext(context)
runInContext(script, context, {
filename: path.base,
importModuleDynamically,
} as RunningScriptOptions)
}
function importModuleDynamically(
module: string,
_script: Script,
_importAssertions: any
) {
const Options = getArgs()
logv(`Importing ${module}`)
let modulePath = ''
if (module.startsWith('.')) {
modulePath = join(process.cwd(), dirname(Options.script), module)
} else {
modulePath = join(process.cwd(), 'node_modules', module)
}
//windows `C:`
if (/^[a-z]:\\/i.test(modulePath)) modulePath = modulePath.substring(2)
if (existsSync(join(modulePath, 'package.json'))) {
let { main } = JSON.parse(
readFileSync(join(modulePath, 'package.json')).toString()
)
modulePath = join(modulePath, main)
}
modulePath = pathToFileURL(modulePath).href
return import(modulePath)
}