v1.1.0
translated to typescript and added .ts file support
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
lib
|
||||||
|
dist
|
||||||
|
testing
|
||||||
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
lib/*
|
||||||
|
pnpm-lock.yaml
|
||||||
10
.prettierrc
Normal file
10
.prettierrc
Normal 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
48
eslintrc.json
Normal 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
38
package.json
Normal 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
1245
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
readme.md
Normal file
41
readme.md
Normal 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
44
src/build.ts
Normal 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
3
src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { runVM } from './vm'
|
||||||
|
|
||||||
|
runVM()
|
||||||
52
src/repl.ts
Normal file
52
src/repl.ts
Normal 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
18
src/tsconfig.json
Normal 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
44
src/util/args.ts
Normal 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
7
src/util/index.ts
Normal 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
1
src/util/version.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const BIN_VERSION = '1.0.1'
|
||||||
110
src/vm.ts
Normal file
110
src/vm.ts
Normal 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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user