diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..ebba38f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "printWidth": 100, + "trailingComma": "none", + "arrowParens": "avoid", + "parser": "typescript", + "singleQuote": true, + "tabWidth": 2 +} diff --git a/README.md b/README.md index c04608e..13361b2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # JSPython CLI. -We provide a command line interface to run [JSPython](https://github.com/jspython-dev/jspython) scripts +Command line interface to run [JSPython](https://github.com/jspython-dev/jspython) in NodeJS environment ## Install from NPM @@ -12,22 +12,21 @@ We provide a command line interface to run [JSPython](https://github.com/jspytho ### Run in terminal ``` - jspython path/to/jspython/file - jspython --file path/to/jspython/file + jspython --f path/to/jspython/file jspython --file=test.jspy ``` -### Pass parameters to script +### JSPython command line arguments In CLI ``` -jspython --file=path/to/jspython/file --param1=value --param -jspython path/to/jspython/file param1=value param +jspython --file=path/to/jspython/file --arg1=value --arg2='test value 1' +jspython path/to/jspython/file arg1=value ``` -In script +Inside your JSPython script yu can access arguments with `args` object. ```py -params("param1") == "value" # true -params("param") == false # true +a1 = args.arg1 +a2 = args.arg2 or 'another value' ``` ### Run file @@ -46,7 +45,7 @@ jspython --file=path/to/jspython/file.jspy --entryFunction=myFunc1 ``` or ``` -jspython -f=path/to/jspython/file.jspy -e=myFunc1 +jspython -f path/to/jspython/file.jspy -e myFunc1 ``` @@ -55,7 +54,7 @@ jspython -f=path/to/jspython/file.jspy -e=myFunc1 ``` jspython --file=path/to/jspython/file.jspy --srcRoot=src ``` -Normally you would expect package.json and node_modules to be in the root level and all scripts in the `src` folder +Normally, you would expect package.json and node_modules to be in the root level and all scripts in the `src` folder ``` -|- .git @@ -73,7 +72,7 @@ Then, from a root folder you can: or -> jspython -f=my_code.jspy -s=src --param1=some_Value +> jspython -f my_code.jspy -s src --param1=some_Value ### Version info @@ -87,8 +86,7 @@ or ## Development Run example using node. (Works only if you have build project `npm run build`) ``` -node ./bin/jspython --file=../jspython-examples/axios-test.jspy -node ./bin/jspython --file ../jspython-examples/parse.jspy +node ./bin/jspython --file=../jspython-examples/test.jspy node ./bin/jspython --file=test.jspy --srcRoot=../jspython-examples/ ``` diff --git a/package.json b/package.json index c8a1185..a86d98e 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "jspython-cli", - "version": "2.1.3", + "version": "2.1.18", "description": "CLI for jspython. Allows you to run jspython (*.jspy) files", - "main": "src/index.ts", + "main": "./lib/public-api.js", "bin": { "jspython": "bin/jspython" }, + "typings": "./lib/public-api.d.ts", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "dev": "npx rollup -c rollup.config.dev.js", @@ -29,13 +30,16 @@ }, "homepage": "https://github.com/jspython-dev/jspython-cli#readme", "dependencies": { - "arg": "^4.1.2", - "jspython-interpreter": "~2.1.3" + "arg": "^5.0.1", + "jspython-interpreter": "^2.1.10" }, "devDependencies": { - "rollup": "^1.27.13", - "rollup-plugin-copy": "^3.1.0", - "rollup-plugin-typescript2": "^0.25.3", - "typescript": "^3.7.3" + "rollup": "^2.70.1", + "rollup-plugin-copy": "^3.4.0", + "rollup-plugin-typescript2": "^0.31.2", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-prettier": "^4.0.0", + "prettier": "^2.5.1", + "typescript": "^4.6.3" } } diff --git a/rollup.config.dev.js b/rollup.config.dev.js index b702784..307e913 100644 --- a/rollup.config.dev.js +++ b/rollup.config.dev.js @@ -14,7 +14,7 @@ export default { plugins: [ typescript() ], - external: ['arg', 'fs', 'jspython-interpreter', 'json5', 'http', 'https'], + external: ['arg', 'fs', 'jspython-interpreter', 'http', 'https'], watch: { exclude: ['node_modules/**'], include: 'src/**' diff --git a/rollup.config.js b/rollup.config.js index 30a0586..8302d2b 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -3,18 +3,37 @@ import copy from 'rollup-plugin-copy' const pkg = require('./package.json'); -export default { - input: pkg.main, - output: { file: pkg.bin['jspython'], format: 'cjs', sourcemap: true, compact: true, banner: '#!/usr/bin/env node' }, +const external = ['fs', 'jspython-interpreter']; + +export default [{ + input: 'src/cli.ts', + output: { file: pkg.bin['jspython'], format: 'cjs', sourcemap: false, compact: true, banner: '#!/usr/bin/env node' }, plugins: [ typescript({ - clean: true + clean: true, + tsconfigOverride: { + compilerOptions: { declaration: false } + } }), copy({ targets: [ - { src: 'src/examples', dest: 'dist' } + { src: 'src/examples', dest: 'lib' } ] }) ], - external: ['arg', 'fs', 'jspython-interpreter', 'http', 'https'] -}; + external: ['arg', 'http', 'https', ...external] +}, { + input: 'src/public-api.ts', + output: [ + { + file: pkg.main, + format: 'cjs' + } + ], + plugins: [ + typescript({ + clean: true + }) + ], + external +}]; diff --git a/src/assert.ts b/src/assert.ts new file mode 100644 index 0000000..e9e4bca --- /dev/null +++ b/src/assert.ts @@ -0,0 +1,158 @@ +export class Assert { + #chainedCheckCount: number = 1; + public status: boolean = true; + public message?: string; + readonly #logFn: (success: boolean, msg: string) => void; + + constructor( + public assert: string, + private dataContext: object, + logFn: (success: boolean, msg: string) => void + ) { + this.#logFn = logFn; + } + + equal(expected: unknown, received: unknown): Assert { + return this.assertFunction( + expected, + received, + (e, r) => { + if (typeof e === 'object') { + // deepClone + return JSON.stringify(e) !== JSON.stringify(r); + } + + return e === r; + }, + (e, r) => `Expected '${e}', received '${r}'` + ); + } + + notEqual(expected: unknown, received: unknown): Assert { + return this.assertFunction( + expected, + received, + (e, r) => { + if (typeof e === 'object') { + // deepClone + return JSON.stringify(e) !== JSON.stringify(r); + } + + return e !== r; + }, + (e, r) => `Expected '${e}' is the same as received '${r}'` + ); + } + + isTruthy(value: unknown): Assert { + return this.assertFunction( + value, + null, + (e, r) => !!e, + (e, r) => + `Value '${e}' is not Truthy - https://developer.mozilla.org/en-US/docs/Glossary/Truthy` + ); + } + + isFalsy(value: unknown): Assert { + return this.assertFunction( + value, + null, + (e, r) => !e, + (e, r) => + `Value '${e}' is not Falsy - https://developer.mozilla.org/en-US/docs/Glossary/Falsy` + ); + } + + isTrue(value: unknown): Assert { + return this.assertFunction( + value, + null, + (e, r) => e === true, + (e, r) => `Value '${e}' is not true` + ); + } + + isFalse(value: unknown): Assert { + return this.assertFunction( + value, + null, + (e, r) => e === false, + (e, r) => `Value '${e}' is not false` + ); + } + + greaterThan(value1: number | Date, value2: number | Date): Assert { + return this.assertFunction( + value1, + value2, + (e: any, r: any) => e > r, + (e, r) => `${e}' is not greater than ${r}` + ); + } + + greaterOrEqualThan(value1: number | Date, value2: number | Date): Assert { + return this.assertFunction( + value1, + value2, + (e: any, r: any) => e >= r, + (e, r) => `${e}' is not greater (or equal) than ${r}` + ); + } + + inRange(value: number | Date, min: number | Date, max: number | Date): Assert { + return this.between(value, min, max); + } + + between(value: number | Date, min: number | Date, max: number | Date): Assert { + return this.assertFunction( + value, + { min, max }, + (v: any, r: any) => v >= r.min && v <= r.max, + (v, r: any) => `${v}' is NOT in range of ${r.min} and ${r.max}` + ); + } + + lessThan(value1: number | Date, value2: number | Date): Assert { + return this.assertFunction( + value1, + value2, + (e: any, r: any) => e < r, + (e, r) => `${e}' is not lesser than ${r}` + ); + } + + lessOrEqualThan(value1: number | Date, value2: number | Date): Assert { + return this.assertFunction( + value1, + value2, + (e: any, r: any) => e <= r, + (e, r) => `${e}' is not lesser (or equal) than ${r}` + ); + } + + private assertFunction( + expected: unknown, + received: unknown, + assertExpressionFn: (e: unknown, r: unknown) => boolean, + errorMessageFn: (e: unknown, r: unknown) => string + ): Assert { + // resolve function, if any + if (typeof expected === 'function') { + expected = expected(this.dataContext); + } + if (typeof received === 'function') { + received = received(this.dataContext); + } + + if (!assertExpressionFn(expected, received)) { + this.status = false; + this.message = `[${this.#chainedCheckCount}] - ${errorMessageFn(expected, received)}`; + this.#logFn(false, this.message); + return this; + } + this.#logFn(true, ''); + this.#chainedCheckCount++; + return this; + } +} diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..6ea74a3 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,156 @@ +import arg from 'arg'; +import fs from 'fs'; +import { Interpreter } from 'jspython-interpreter'; +import { AssertInfo, initialScope, jsPythonForNode } from './jspython-node'; +import { InterpreterOptions } from './types'; +import { trimChar } from './utils'; +var util = require('util'); + +const pkg = require('../package.json'); + +// catch the uncaught errors that weren't wrapped in a domain or try catch statement +// do not use this in modules, but only in applications, as otherwise we could have multiple of these bound +process + .on('unhandledRejection', (reason, p) => { + console.error(reason, 'Unhandled Rejection at Promise', p); + }) + .on('uncaughtException', err => { + console.error(err, 'Uncaught Exception thrown'); + process.exit(1); + }); + +function getOptionsFromArguments(rawArgs: string[]): InterpreterOptions { + const args = arg( + { + '--file': String, + '--srcRoot': String, + '--entryFunction': String, + '--version': Boolean, + '--output': String, + '-f': '--file', + '-s': '--srcRoot', + '-e': '--entryFunction', + '-v': '--version', + '-o': '--output' + }, + { permissive: true, argv: process.argv.slice(2) } + ); + + const params = args._.reduce((obj: { [key: string]: any }, a: string) => { + const kv = a.replace('--', ''); + let [key, val]: any = kv.split('='); + if (kv === key) { + val = true; + } + obj[key] = val; + return obj; + }, {}); + + const res: InterpreterOptions = { + file: args['--file'] || (rawArgs.length === 3 && !rawArgs[2].startsWith('-') ? rawArgs[2] : '') + }; + + if (args['--version']) { + res.version = args['--version']; + } + if (args['--output']) { + res.output = args['--output']; + } + if (args['--entryFunction']) { + res.entryFunction = args['--entryFunction']; + } + if (args['--srcRoot']) { + res.srcRoot = args['--srcRoot']; + } + + if (trimChar(res.srcRoot || '', '/').length) { + res.srcRoot = res.srcRoot + '/'; + } + + res.params = { ...res, ...params }; + + return res; +} + +async function main() { + const options = getOptionsFromArguments(process.argv); + const interpreter: Interpreter = (await jsPythonForNode(options)) as Interpreter; + const jspyContext: Record = { ...initialScope }; + + if (options.version) { + console.log(interpreter.jsPythonInfo()); + console.log(`JSPython cli v${(pkg || {}).version}\n`); + return; + } + + if (!options.file) { + console.log(interpreter.jsPythonInfo()); + console.log(`JSPython cli v${(pkg || {}).version}\n`); + console.log(` :\> jspython (fileName.jspy)`); + console.log(` :\> jspython -f (fileName.jspy)`); + console.log(` :\> jspython --file=(fileName.jspy)`); + console.log(` :\> jspython --file=(fileName.jspy) --srcRoot=src`); + console.log(` :\> jspython --file=(fileName.jspy) --param1=someValue1 --param2=someValue2`); + console.log(' '); + return; + } + + if (options.output) { + var logFile = fs.createWriteStream(options.output, { flags: 'w' }); + var logStdout = process.stdout; + + console.log = function () { + const req = new RegExp('\\x1b\\[\\d\\dm', 'g'); + logFile.write( + util.format.apply( + null, + Array.from(arguments).map(a => (a && a.replace ? a.replace(req, '') : a)) + ) + '\n' + ); + logStdout.write(util.format.apply(null, arguments) + '\n'); + }; + console.error = console.log; + } + + if (options.file) { + let fileName = `${options.srcRoot || ''}${options.file}`; + + // try to check if file exists in 'src' folder + if (!fs.existsSync(fileName)) { + fileName = `src/${options.file}`; + } + + const scripts = fs.readFileSync(fileName, 'utf8'); + console.log(interpreter.jsPythonInfo()); + console.log(`> ${options.file}`); + jspyContext.__env = { + args: options.params, + entryFunction: options.entryFunction || '', + entryModule: options.file, + runsAt: 'jspython-cli' + }; + try { + const res = await interpreter.evaluate( + scripts, + jspyContext, + options.entryFunction || undefined, + options.file + ); + + if (!!jspyContext.asserts?.length) { + const asserts = (jspyContext.asserts || []) as AssertInfo[]; + console.log(` > assert success : ${asserts.filter(a => a.success).length}`); + console.log(` > assert failed : ${asserts.filter(a => !a.success).length}`); + } + + if (!!res || res === 0) { + console.log('>', res); + } + } catch (err) { + console.log('JSPython execution failed: ', (err as Error)?.message || err, err); + throw err; + } + } +} + +main().catch(e => console.log('error:', e?.message || e)); diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 69cee83..0000000 --- a/src/index.ts +++ /dev/null @@ -1,204 +0,0 @@ -import arg from 'arg'; -import fs from 'fs'; -import { jsPython, Interpreter, PackageLoader } from 'jspython-interpreter'; -var util = require('util'); - -const pkg = require('../package.json'); - -const context: any = { - asserts: [], - params: {} -} - -// catch the uncaught errors that weren't wrapped in a domain or try catch statement -// do not use this in modules, but only in applications, as otherwise we could have multiple of these bound -process - .on('unhandledRejection', (reason, p) => { - console.error(reason, 'Unhandled Rejection at Promise', p); - }) - .on('uncaughtException', err => { - console.error(err, 'Uncaught Exception thrown'); - process.exit(1); - }); - -const initialScope: Record = { - app: {} -}; - -const rootFolder = process.cwd().split('\\').join('/'); -const interpreter: Interpreter = jsPython() as Interpreter; -const options = getOptionsFromArguments(process.argv); - -function trimChar(text: string, charToRemove: string): string { - while (text.charAt(0) == charToRemove) { - text = text.substring(1); - } - - while (text.charAt(text.length - 1) == charToRemove) { - text = text.substring(0, text.length - 1); - } - - return text; -} - -async function initialize(baseSource: string) { - // process app.json (if exists) - // add configuration to the 'app' - if (fs.existsSync(`${rootFolder}/${baseSource}app.json`)) { - const app = require(`${rootFolder}/${baseSource}app.json`); - initialScope.app = Object.assign(initialScope.app || {}, app); - } - - // process app.js (if exists) - // - run _init - // - delete _ init - // - run _initAsync - // - delete _initAsync - // - load content into 'app' - if (fs.existsSync(`${rootFolder}/${baseSource}app.js`)) { - const app = require(`${rootFolder}/${baseSource}app.js`); - - if (typeof app._init == 'function') { - app._init(); - delete app._init; - } - - if (typeof app._initAsync == 'function') { - await app._initAsync(); - delete app._initAsync; - } - - Object.assign(initialScope, app); - } - - initialScope.assert = (condition: boolean, name?: string, description?: string) => context.asserts.push({ condition, name, description }); - initialScope.showAsserts = () => console.table(context.asserts); - initialScope.params = (name: string) => { - const value = context.params[name]; - return value === undefined ? null : value; - } -} - -function getOptionsFromArguments(rawArgs: string[]) { - const args = arg({ - '--file': String, - '--srcRoot': String, - '--entryFunction': String, - '--version': Boolean, - '--output': String, - '-f': '--file', - '-s': '--srcRoot', - '-e': '--entryFunction', - '-v': '--version', - '-o': '--output' - }, { - argv: rawArgs.slice(2), - permissive: true - }); - - const params = args._.reduce((obj: { [key: string]: any }, a: string) => { - const kv = a.replace('--', ''); - let [key, val]: any = kv.split('='); - if (kv === key) { - val = true; - } - obj[key] = val; - return obj; - }, {}); - - const res = { - file: args['--file'] || (rawArgs.length === 3 && !rawArgs[2].startsWith('-') ? rawArgs[2] : ''), - version: args['--version'], - output: args['--output'], - entryFunction: args['--entryFunction'], - srcRoot: args['--srcRoot'] || '' - }; - - res.srcRoot = trimChar(res.srcRoot || '', '/'); - if (res.srcRoot.length) { - res.srcRoot = res.srcRoot + '/'; - } - - context.params = { ...res, ...params }; - - return res; -} - -function moduleLoader(filePath: string): Promise { - try { - const script = fs.readFileSync(`${options.srcRoot}${trimChar(filePath, '/')}.jspy`, 'utf8'); - return Promise.resolve(script); - } catch (e) { - try { - // try without JSPY - const script = fs.readFileSync(`${options.srcRoot}${trimChar(filePath, '/')}`, 'utf8'); - return Promise.resolve(script); - } catch (e) { - return Promise.reject(e); - } - } -} - -/**@type {PackageLoader} */ -function packageLoader(packageName: string): any { - try { - if (['fs', 'path', 'readline', 'timers', 'child_process', 'util', 'zlib', 'stream', 'net', 'https', 'http', 'events', 'os', 'buffer'] - .includes(packageName)) { - return require(packageName) - } - - if (packageName.toLowerCase().endsWith('.js') || packageName.toLowerCase().endsWith('.json')) { - return require(`${rootFolder}/${options.srcRoot}${packageName}`) - } - - return require(`${rootFolder}/node_modules/${packageName}`); - } - catch (err) { - console.log('Import Error: ', err?.message || err); - throw err; - } -} - -async function main() { - if (options.version) { - console.log(interpreter.jsPythonInfo()); - console.log(`JSPython cli v${(pkg || {}).version}\n`); - } - - if (options.output) { - var logFile = fs.createWriteStream(options.output, { flags: 'w' }); - var logStdout = process.stdout; - - console.log = function () { - const req = new RegExp('\\x1b\\[\\d\\dm', 'g'); - logFile.write(util.format.apply(null, Array.from(arguments).map(a => a && a.replace ? a.replace(req, '') : a)) + '\n'); - logStdout.write(util.format.apply(null, arguments) + '\n'); - } - console.error = console.log; - } - - await initialize(options.srcRoot); - - if (options.file) { - const scripts = fs.readFileSync(`${options.srcRoot}${options.file}`, 'utf8'); - context.asserts.length = 0; - console.log(interpreter.jsPythonInfo()) - console.log(`> ${options.file}`) - try { - const res = await interpreter - .registerPackagesLoader(packageLoader as PackageLoader) - .registerModuleLoader(moduleLoader) - .evaluate(scripts, initialScope, options.entryFunction || undefined, options.file); - - if (!!res || res === 0) { - console.log('>', res); - } - } catch (err) { - console.log('JSPython execution failed: ', err?.message || err, err); - throw err; - } - } -} - -main() - .catch(e => console.log('error:', e?.message || e)) diff --git a/src/jspython-node.ts b/src/jspython-node.ts new file mode 100644 index 0000000..d3b6ea4 --- /dev/null +++ b/src/jspython-node.ts @@ -0,0 +1,216 @@ +import fs from 'fs'; +import { jsPython, Interpreter, PackageLoader } from 'jspython-interpreter'; +import { Assert } from './assert'; +import { InterpreterOptions } from './types'; +import { trimChar } from './utils'; + +const rootFolder = process.cwd().split('\\').join('/'); +export const initialScope: any = { + session: {}, + asserts: [] as AssertInfo[] +}; + +type NodeJsInterpreter = Interpreter & { evaluateFile: (fileName: string) => Promise }; +export type AssertInfo = { success: boolean; name: string; description?: string }; +type LogInfo = { level: 'info' | 'fail' | 'success'; message: string; time: Date; logId?: string }; + +let previousLogMessage = ''; + +const context: { + params: any; +} = { + params: {} +}; + +function logFn(msg: LogInfo): void { + const level = msg.level === 'success' ? msg.level : msg.level + ' '; + const message = `${level} | ${msg.message}`; + + if (message !== previousLogMessage) { + console.log(`| ${msg.time.toTimeString().slice(0, 8)} | ${message}`); + previousLogMessage = message; + } +} + +function assert(name: string, dataContext?: boolean | any): Assert | void { + // an original case when + if (typeof dataContext === 'boolean') { + logFn({ + level: dataContext ? 'success' : 'fail', + message: name || '', + time: new Date() + }); + initialScope.asserts.push({ success: !!dataContext, name }); + return; + } + + function assertCallback(success: boolean, message: string) { + logFn({ + logId: name, + level: success ? 'success' : 'fail', + message: `${name} ${message ? ':' : ''} ${message || ''}`, + time: new Date() + }); + + const existingAssert = initialScope.asserts?.find((a: AssertInfo) => a.name === name); + if (existingAssert) { + // only if condition it is not fail yet + if (!!existingAssert.success) { + existingAssert.success = !!success; + existingAssert.description = message; + } + } else { + initialScope.asserts.push({ success: !!success, name: name, description: message }); + } + } + + return new Assert(name, dataContext, assertCallback); +} + +function getScript(fileName: string): string { + if (!fs.existsSync(fileName)) { + throw Error(`File not found`); + } + + const scripts = fs.readFileSync(fileName, 'utf8'); + return scripts; +} + +async function initialize(baseSource: string) { + // process app.js (if exists) + // - run _init + // - delete _ init + // - run _initAsync + // - delete _initAsync + // - load content into 'app' + + let appJsPath = `${rootFolder}/${baseSource}app.js`; + console.log({ rootFolder, baseSource }); + if (!fs.existsSync(appJsPath)) { + appJsPath = `${rootFolder}/src/app.js`; + } + + if (fs.existsSync(appJsPath)) { + const app = require(appJsPath); + + if (typeof app._init == 'function') { + app._init(); + delete app._init; + } + + if (typeof app._initAsync == 'function') { + await app._initAsync(); + delete app._initAsync; + } + + Object.assign(initialScope, app); + } +} + +initialScope.assert = (name: string, dataContext: any) => assert(name, dataContext); +initialScope.showAsserts = () => + console.table( + initialScope.asserts?.map((r: any) => ({ + status: r.success ? 'success' : 'fail', + assert: `${r.name}${!!r.description ? ': ' : ''}${r.description || ''}`.trim() + })) + ); +initialScope.print = (...args: any) => + logFn({ + time: new Date(), + level: 'info', + message: args.map((v: any) => (typeof v === 'object' ? JSON.stringify(v) : v)).join(' ') + }); + +export async function jsPythonForNode( + options: InterpreterOptions = { + srcRoot: '' + } +): Promise { + const interpreter: NodeJsInterpreter = jsPython() as NodeJsInterpreter; + Object.assign(context.params, options.params); + + await initialize(options.srcRoot || ''); + + interpreter + .registerPackagesLoader(packageLoader as PackageLoader) + .registerModuleLoader(moduleLoader); + + const evaluate = interpreter.evaluate; + + interpreter.evaluate = async function ( + script: string, + evaluationContext?: object | undefined, + entryFunctionName?: string | undefined, + moduleName?: string | undefined + ) { + initialScope.asserts.splice(0, initialScope.asserts.length); + return evaluate.call(interpreter, script, evaluationContext, entryFunctionName, moduleName); + }; + + interpreter.evaluateFile = function (filePath: string, context = {}) { + const script = getScript(filePath); + return interpreter.evaluate(script, context, options.entryFunction); + }; + + return interpreter; + + function moduleLoader(filePath: string): Promise { + filePath = trimChar(trimChar(filePath, '/'), '.'); + let fileName = `${options.srcRoot || ''}${filePath}.jspy`; + + if (!fs.existsSync(fileName)) { + fileName = `${options.srcRoot || ''}${filePath}`; + } + + if (!fs.existsSync(fileName)) { + fileName = `src/${filePath}`; + } + + try { + const script = fs.readFileSync(fileName, 'utf8'); + return Promise.resolve(script); + } catch (e) { + console.log('* module loader error ', (e as Error)?.message || e); + return Promise.reject(e); + } + } + + /**@type {PackageLoader} */ + function packageLoader(packageName: string): any { + try { + if ( + [ + 'fs', + 'path', + 'readline', + 'timers', + 'child_process', + 'util', + 'zlib', + 'stream', + 'net', + 'https', + 'http', + 'events', + 'os', + 'buffer' + ].includes(packageName) + ) { + return require(packageName); + } + + if ( + packageName.toLowerCase().endsWith('.js') || + packageName.toLowerCase().endsWith('.json') + ) { + return require(`${rootFolder}/${options.srcRoot || ''}${packageName}`); + } + + return require(`${rootFolder}/node_modules/${packageName}`); + } catch (err) { + console.log('Import Error: ', (err as Error)?.message ?? err); + throw err; + } + } +} diff --git a/src/public-api.ts b/src/public-api.ts new file mode 100644 index 0000000..782132e --- /dev/null +++ b/src/public-api.ts @@ -0,0 +1,2 @@ +export * from './jspython-node'; +export * from './types'; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..3f52582 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,8 @@ +export interface InterpreterOptions { + file?: string; + srcRoot?: string; + entryFunction?: string; + version?: boolean; + output?: string; + params?: Record; +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..41cd4ba --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,11 @@ +export function trimChar(text: string, charToRemove: string): string { + while (text.charAt(0) == charToRemove) { + text = text.substring(1); + } + + while (text.charAt(text.length - 1) == charToRemove) { + text = text.substring(0, text.length - 1); + } + + return text; +} diff --git a/tsconfig.json b/tsconfig.json index 70c4186..8f93a67 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,13 +2,13 @@ "compilerOptions": { /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ - "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ + "target": "es2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ // "lib": [], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ - // "declaration": true, /* Generates corresponding '.d.ts' file. */ + "declaration": true, /* Generates corresponding '.d.ts' file. */ "declarationMap": false, /* Generates a sourcemap for each corresponding '.d.ts' file. */ "sourceMap": false, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ @@ -39,7 +39,7 @@ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ /* Module Resolution Options */ - // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */