Skip to content

Commit

Permalink
2.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
sashapop10 committed May 25, 2023
1 parent 9519854 commit a08b8d5
Show file tree
Hide file tree
Showing 11 changed files with 122 additions and 59 deletions.
14 changes: 11 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## [2.1.0][] - 2023-05-25

- Universal script reader
- Change in names - execution switched to exec, createContext switched to createCtx
- Types update
- Documentation update

## [2.0.0][] - 2023-05-25

- Npm update
Expand All @@ -24,6 +31,7 @@

- Stable version

[2.0.0]: https://github.com/LeadFisherSolutions/workspace-example/compare/v1.1.0...v2.0.0
[1.1.0]: https://github.com/LeadFisherSolutions/workspace-example/compare/release...v1.1.0
[1.0.0]: https://github.com/LeadFisherSolutions/workspace-example/releases/tag/release
[2.1.0]: https://github.com/LeadFisherSolutions/leadvm/compare/v2.0.0...v2.1.0
[2.0.0]: https://github.com/LeadFisherSolutions/leadvm/compare/v1.1.0...v2.0.0
[1.1.0]: https://github.com/LeadFisherSolutions/leadvm/compare/release...v1.1.0
[1.0.0]: https://github.com/LeadFisherSolutions/leadvm/releases/tag/release
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,26 @@ const leadvm = require('leadvm');
// }
```

- **Universal script reader**
You can load folder or script file with this function, but it's slower compare to previous two.

```js
const leadvm = require('leadvm');
(async () => {
const dir = await leadvm.read('./test/examples/readDir');
const arrow = await leadvm.read('./test/examples/arrow.js');
console.log({ ...dir, arrow });
})();
// Output:
// {
// simple: { field: 'value', add: [Function: add], sub: [Function: sub] },
// deep: {
// arrow: [Function: anonymous]
// },
// arrow: [Function: anonymous]
// }
```

- **Nested modules scripts**
By default nested modules can't be required, to require them you must add access field in options:

Expand All @@ -57,10 +77,10 @@ const leadvm = require('leadvm');
(async () => {
const sandbox = { console };
sandbox.global = sandbox;
const ms = await leadvm.execute(`module.exports = require('./examples/module.cjs');`, {
const ms = await leadvm.exec(`module.exports = require('./examples/module.cjs');`, {
type: 'cjs',
dir: __dirname,
context: leadvm.execute(Object.freeze(sandbox)),
ctx: leadvm.createCtx(Object.freeze(sandbox)),
access: {
// You can also use path to dir
// [path.join(__dirname, 'examples']: true
Expand Down Expand Up @@ -91,7 +111,7 @@ const leadvm = require('leadvm');
}
};
`;
const ms = leadvm.execute(src, {
const ms = leadvm.exec(src, {
access: {
fs: {
readFile(filename, callback) {
Expand Down
5 changes: 3 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export interface VMScriptOptions extends BaseOptions {
npmIsolation?: boolean;
}

export function createContext(context?: Context | Object, preventEscape?: boolean): Context;
export function execute(src: string, options?: VMScriptOptions): Script;
export function createCtx(ctx?: Context | Object, preventEscape?: boolean): Context;
export function exec(src: string, options?: VMScriptOptions): Script;
export function read(path: string, options?: VMScriptOptions): Promise<Script>;
export function readScript(path: string, options?: VMScriptOptions): Promise<Script>;
export function readDir(path: string, options?: VMScriptOptions, deep?: boolean): Promise<Script[]>;
16 changes: 11 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
'use strict';

const { basename, extname, join, dirname } = require('node:path');
const { readFile, readdir, stat } = require('node:fs').promises;
const { COMMON_CTX, MODULE_TYPES } = require('./lib/config');
const { createContext, scriptType } = require('./lib/utils');
const { readFile, readdir } = require('node:fs').promises;
const execute = require('./lib/exec');
const { createCtx, scriptType } = require('./lib/utils');
const exec = require('./lib/exec');

const readScript = async (path, options = {}) => {
const src = await readFile(path, 'utf8');
if (!src) throw new SyntaxError(`File ${path} is empty`);
const filename = basename(path);
return execute(src, { ...options, filename, dir: dirname(path), type: scriptType(filename) });
return exec(src, { ...options, filename, dir: dirname(path), type: scriptType(filename) });
};

const readDir = async (dir, options = {}, deep = true) => {
Expand All @@ -31,4 +31,10 @@ const readDir = async (dir, options = {}, deep = true) => {
return scripts;
};

module.exports = { execute, readScript, readDir, createContext, COMMON_CTX };
const read = async (path, options = {}, deep = true) => {
const file = await stat(path);
const result = await (file.isDirectory() ? readDir(path, options, deep) : readScript(path, options));
return result;
};

module.exports = { exec, read, readScript, readDir, createCtx, COMMON_CTX };
15 changes: 8 additions & 7 deletions lib/exec.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'use strict';

const { wrapSource, createContext, scriptType } = require('./utils');
const { createCtx, scriptType } = require('./utils');
const createRequire = require('./require');
const { RUN_OPTS } = require('./config');
const { RUN_OPTS, WRAPPERS } = require('./config');
const vm = require('node:vm');

const cjs = (closure, options) => {
Expand All @@ -12,18 +12,19 @@ const cjs = (closure, options) => {
return module.exports;
};

const execute = (src, settings = {}) => {
const wrapSrc = (src, ext = 'js') => `'use strict';\n${WRAPPERS[ext](src.replace(/'use strict';\n?/, ''))}`;
const exec = (src, settings = {}) => {
const { script = {}, run = {}, ctx = {} } = settings;
const type = settings.type ?? (settings.filename ? scriptType(settings.filename) : 'js');
const [filename, dir] = [settings.filename ?? '', settings.dir ?? process.cwd()];
const updatedSettings = { ...settings, script, run, ctx, filename, dir, type };

const isJS = type === 'js';
const defaultCTX = { require: createRequire(updatedSettings, execute), __filename: filename, __dirname: dir };
const runner = new vm.Script(wrapSource(src, type), { filename, lineOffset: -1 - isJS, ...script });
const context = isJS ? createContext(Object.freeze({ ...defaultCTX, ...ctx })) : createContext(ctx);
const defaultCTX = { require: createRequire(updatedSettings, exec), __filename: filename, __dirname: dir };
const runner = new vm.Script(wrapSrc(src, type), { filename, lineOffset: -1 - isJS, ...script });
const context = isJS ? createCtx(Object.freeze({ ...defaultCTX, ...ctx })) : createCtx(ctx);
const exports = runner.runInContext(context, { ...RUN_OPTS, ...run });
return isJS ? exports : cjs(exports, defaultCTX);
};

module.exports = execute;
module.exports = exec;
11 changes: 5 additions & 6 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
'use strict';

const vm = require('node:vm');
const { MODULE_TYPES, CTX_OPTS, EMPTY_CTX, WRAPPERS } = require('./config');
const { createContext } = require('node:vm');
const { MODULE_TYPES, CTX_OPTS, EMPTY_CTX } = require('./config');

const scriptType = name => {
if (!name?.includes('.')) return MODULE_TYPES[0];
const type = name.split('.').at(-1);
return MODULE_TYPES.includes(type) ? type : MODULE_TYPES[0];
};

const createContext = (ctx, mode = false) =>
ctx ? vm.createContext(ctx, { ...CTX_OPTS, preventEscape: mode ? 'afterEvaluate' : '' }) : EMPTY_CTX;
const createCtx = (ctx, mode = false) =>
ctx ? createContext(ctx, { ...CTX_OPTS, preventEscape: mode ? 'afterEvaluate' : '' }) : EMPTY_CTX;

const wrapSource = (src, ext = 'js') => `'use strict';\n${WRAPPERS[ext](src.replace(/'use strict';\n?/, ''))}`;
module.exports = { wrapSource, createContext, scriptType };
module.exports = { createCtx, scriptType };
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"description": "[Node js] Leadfisher script loader with vm wrapper",
"author": "Alexander Ivanov <sashapop101@gmail.com>",
"homepage": "https://leadfisher.ru",
"version": "2.0.0",
"version": "2.1.0",
"license": "MIT",
"packageManager": "npm@9.6.4",
"type": "commonjs",
Expand Down
46 changes: 37 additions & 9 deletions tests/core/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,44 @@
const test = require('node:test');
const assert = require('node:assert');
const path = require('node:path');
const { execute, readDir, COMMON_CTX, createContext } = require('../..');
const { exec, readDir, COMMON_CTX, createCtx, readScript, read } = require('../..');

const target = name => path.join(__dirname, name);

test('Script executor', async () => {
const ms = execute(`({field: 'value'});`);
const ms = exec(`({field: 'value'});`);
assert.deepStrictEqual(Object.keys(ms), ['field']);
assert.strictEqual(ms.field, 'value');
});

test('[JS/CJS] Script loader', async t => {
const simple = await readScript(target('examples/simple.js'));

assert.deepStrictEqual(Object.keys(simple), ['field', 'add', 'sub']);
assert.strictEqual(simple.field, 'value');
assert.strictEqual(simple.add(2, 3), 5);
assert.strictEqual(simple.sub(2, 3), -1);
});

test('[JS/CJS] Universal loader', async t => {
const scripts = await read(target('examples'));
const { deep, simple } = scripts;
const { arrow } = deep;

assert.strictEqual(typeof scripts, 'object');
assert.strictEqual(Object.keys(scripts).length, 2);

assert.deepStrictEqual(Object.keys(simple), ['field', 'add', 'sub']);
assert.strictEqual(simple.field, 'value');
assert.strictEqual(simple.add(2, 3), 5);
assert.strictEqual(simple.sub(2, 3), -1);

assert.strictEqual(typeof arrow, 'function');
assert.strictEqual(arrow.toString(), '(a, b) => a + b');
assert.strictEqual(arrow(2, 3), 5);
assert.strictEqual(arrow(-1, 1), 0);
});

test('[JS/CJS] Folder loader', async t => {
const scripts = await readDir(target('examples'));
const { deep, simple } = scripts;
Expand All @@ -33,13 +61,13 @@ test('[JS/CJS] Folder loader', async t => {
});

test('Create Default Context', async t => {
const context = createContext();
const context = createCtx();
assert.deepEqual(Object.keys(context), []);
assert.strictEqual(context.global, undefined);
});

test('Create Common Context', async t => {
const context = createContext(COMMON_CTX);
const context = createCtx(COMMON_CTX);
assert.strictEqual(typeof context, 'object');
assert.strictEqual(context.console, console);
assert.strictEqual(context.global, undefined);
Expand All @@ -48,22 +76,22 @@ test('Create Common Context', async t => {
test('Create Custom Context', async t => {
const sandbox = { field: 'value' };
sandbox.global = sandbox;
const context = createContext(Object.freeze(sandbox));
const context = createCtx(Object.freeze(sandbox));
assert.strictEqual(context.field, 'value');
assert.deepEqual(Object.keys(context), ['field', 'global']);
assert.strictEqual(context.global, sandbox);
});

test('[JS/CJS] Access internal not permitted', async test => {
try {
const ms = execute(`const fs = require('fs');`, { type: 'cjs' });
const ms = exec(`const fs = require('fs');`, { type: 'cjs' });
assert.strictEqual(ms, undefined);
} catch (err) {
assert.strictEqual(err.message, `Access denied 'fs'`);
}

try {
const ms = execute(`const fs = require('fs');`);
const ms = exec(`const fs = require('fs');`);
assert.strictEqual(ms, undefined);
} catch (err) {
assert.strictEqual(err.message, `Access denied 'fs'`);
Expand All @@ -73,15 +101,15 @@ test('[JS/CJS] Access internal not permitted', async test => {
test('[JS/CJS] Access non-existent not permitted', async test => {
try {
const src = `const notExist = require('nothing');`;
const ms = execute(src, { type: 'cjs' });
const ms = exec(src, { type: 'cjs' });
assert.strictEqual(ms, undefined);
} catch (err) {
assert.strictEqual(err.message, `Access denied 'nothing'`);
}

try {
const src = `const notExist = require('nothing');`;
const ms = execute(src);
const ms = exec(src);
assert.strictEqual(ms, undefined);
} catch (err) {
assert.strictEqual(err.message, `Access denied 'nothing'`);
Expand Down
20 changes: 10 additions & 10 deletions tests/errors/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
const test = require('node:test');
const assert = require('node:assert');
const path = require('node:path');
const { execute, readScript } = require('../..');
const { exec, read } = require('../..');

const target = name => path.join(__dirname, 'examples', name);

test('[CJS] Eval error ', async test => {
try {
execute(`module.exports = eval('100 * 2');`, { type: 'cjs' });
exec(`module.exports = eval('100 * 2');`, { type: 'cjs' });
assert.fail(new Error('Should throw an error.'));
} catch (error) {
assert.strictEqual(error.constructor.name, 'EvalError');
Expand All @@ -18,7 +18,7 @@ test('[CJS] Eval error ', async test => {

test('[JS] Eval error', async test => {
try {
execute(`eval('100 * 2')`);
exec(`eval('100 * 2')`);
assert.fail(new Error('Should throw an error.'));
} catch (error) {
assert.strictEqual(error.constructor.name, 'EvalError');
Expand All @@ -28,7 +28,7 @@ test('[JS] Eval error', async test => {
test('[JS] Error.notfound.js', async t => {
let ms;
try {
ms = await readScript(target('error.notfound.js'));
ms = await read(target('error.notfound.js'));
assert.fail(new Error('Should throw an error.'));
} catch (err) {
assert.strictEqual(err.code, 'ENOENT');
Expand All @@ -38,7 +38,7 @@ test('[JS] Error.notfound.js', async t => {

test('[JS] Error.syntax.js', async t => {
try {
await readScript(target('error.syntax'));
await read(target('error.syntax'));
assert.fail(new Error('Should throw an error.'));
} catch (err) {
assert.strictEqual(err.constructor.name, 'SyntaxError');
Expand All @@ -47,7 +47,7 @@ test('[JS] Error.syntax.js', async t => {

test('[JS] Error.reference.js', async t => {
try {
const script = await readScript(target('error.reference.js'));
const script = await read(target('error.reference.js'));
await script();

assert.fail(new Error('Should throw an error.'));
Expand All @@ -58,7 +58,7 @@ test('[JS] Error.reference.js', async t => {

test('[JS] Call undefined as a function', async t => {
try {
const script = await readScript(target('error.undef.js'));
const script = await read(target('error.undef.js'));
await script();
assert.fail(new Error('Should throw an error.'));
} catch (err) {
Expand All @@ -68,7 +68,7 @@ test('[JS] Call undefined as a function', async t => {

test('[JS/CJS] Error.reference.js Error.reference.cjs (line number)', async t => {
try {
const script = await readScript(target('error.reference.js'));
const script = await read(target('error.reference.js'));
await script();

assert.fail(new Error('Should throw an error.'));
Expand All @@ -79,7 +79,7 @@ test('[JS/CJS] Error.reference.js Error.reference.cjs (line number)', async t =>
}

try {
const script = await readScript(target('error.reference.cjs'));
const script = await read(target('error.reference.cjs'));
await script();
assert.fail(new Error('Should throw an error.'));
} catch (err) {
Expand All @@ -91,7 +91,7 @@ test('[JS/CJS] Error.reference.js Error.reference.cjs (line number)', async t =>

test('Error.empty.js', async t => {
try {
await readScript(target('error.empty.js'));
await read(target('error.empty.js'));
assert.fail(new Error('Should throw an error.'));
} catch (err) {
assert.strictEqual(err.constructor.name, 'SyntaxError');
Expand Down
Loading

0 comments on commit a08b8d5

Please sign in to comment.