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