Skip to content

Commit

Permalink
2.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
sashapop10 committed May 24, 2023
1 parent 04bea55 commit 6e972b7
Show file tree
Hide file tree
Showing 25 changed files with 239 additions and 358 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

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

- Npm update
- Migration from new Script to execute function
- Code quality & optimizations
- Tests update
- Documentation update
- createScript removed, use execute as analogue
- readDir and readScript now force paste filename and dir fields
- Script execution doesn't return object of Script anymore, it returns execution result instead
- Rename src folder to lib
- Types update
- Shorthand names, for example scriptOptions field renamed into script, see documentation

## [1.1.0][] - 2023-05-14

- CHANGELOG.md
Expand All @@ -10,5 +24,6 @@

- 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
99 changes: 21 additions & 78 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,11 @@ npm i leadvm --save
const leadvm = require('leadvm');
const ms = leadvm.createScript(`({ field: 'value' });`, {}, 'Example');
// const ms = leadvm.createScript(`(a, b) => a + b;`);
console.log(ms);
// Output:
// Script {
// script: Script {},
// context: {},
// access: {}
// type: 'js',
// __dirname: '/home/user/app',
// __name: 'Example',
// exports: { field: 'value' }
// }
console.log(ms); // Output: { field: 'value' }
```

- **Read scripts from file**
You can read exact script from file. It will help you with code decomposition.
You can read exact script from file. It will help you with code decomposition. The extension of the script may be any or may not be at all.

```js
const leadvm = require('leadvm');
Expand All @@ -38,15 +28,7 @@ const leadvm = require('leadvm');
console.log(ms);
})();
// Output:
// Script {
// script: Script {},
// context: {},
// access: {},
// type: 'js',
// __dirname: '/home/user/app/test/examples',
// __name: 'simple.js',
// exports: { field: 'value', add: [Function: add], sub: [Function: sub] }
// }
// { field: 'value', add: [Function: add], sub: [Function: sub] }
```

- **Read scripts from folder**
Expand All @@ -60,25 +42,9 @@ const leadvm = require('leadvm');
})();
// Output:
// {
// simple: {
// script: Script {},
// context: {},
// access: {},
// type: 'js',
// __dirname: '/home/user/app/test/examples',
// __filename: 'simple.js',
// exports: { field: 'value', add: [Function: add], sub: [Function: sub] }
// },
// simple: { field: 'value', add: [Function: add], sub: [Function: sub] },
// deep: {
// arrow: {
// script: Script {},
// context: {},
// access: {},
// type: 'cjs',
// __dirname: '/home/user/app/test/examples',
// __filename: 'arrow.js',
// exports: { field: 'value', add: [Function: add], sub: [Function: sub] }
// }
// arrow: [Function: anonymous]
// }
// }
```
Expand All @@ -91,33 +57,22 @@ const leadvm = require('leadvm');
(async () => {
const sandbox = { console };
sandbox.global = sandbox;
const ms = await leadvm.createScript(`module.exports = require('./examples/module.cjs');`, {
const ms = await leadvm.execute(`module.exports = require('./examples/module.cjs');`, {
type: 'cjs',
__dirname,
context: leadvm.createContext(Object.freeze(sandbox)),
dir: __dirname,
context: leadvm.execute(Object.freeze(sandbox)),
access: {
// You can also use path to dir
// [path.join(__dirname, 'examples']: true
// NOTICE: Use it only with boolean value
// NOTICE: Use it only with boolean value in this case
[path.join(__dirname, 'examples', 'module.cjs')]: true,
[path.join(__dirname, 'examples', 'module.nested.js')]: true,
},
});
console.log(ms);
})();
// Output:
// Script {
// script: Script {},
// context: { console },
// access: {
// '/home/user/app/test/examples/module.cjs': true,
// '/home/user/app/test/examples/module.nested.js': true
// },
// type: 'cjs',
// __dirname: '/home/user/app/tests',
// __filename: 'module.cjs',
// exports: { name: 'module', value: 1, nested: { name: 'module.nested', value: 2 } }
// }
// { name: 'module', value: 1, nested: { name: 'module.nested', value: 2 } }
```

- **Library substitution**
Expand All @@ -136,7 +91,7 @@ const leadvm = require('leadvm');
}
};
`;
const ms = leadvm.createScript(src, {
const ms = leadvm.execute(src, {
access: {
fs: {
readFile(filename, callback) {
Expand All @@ -146,20 +101,19 @@ const leadvm = require('leadvm');
},
type: 'cjs',
});
const res = await ms.exports.useStub();
const res = await ms.useStub();
console.log(res);
})();
// Output: stub-content
})(); // Output: stub-content
```

- **Class script types**
- **type**:
<code>_js_</code> Script execution returns last expression
<code>_cjs_</code> Script execution returns all that module.exports includes
- **\_\_filename** Stands for the name of the module, by default <code>N404.js</code>
- **\_\_dirname** Stands for the name of the module directory, by default <code>process.cwd()</code>
- **filename** Stands for the name of the module, by default it's empty string
- **dir** Stands for the name of the module directory, by default <code>process.cwd()</code>
- **npmIsolation** Use it if you want to isolate your npm modules in vm context.
- **context** Script execution closured context, by default its clear that is why you can't use even <code>setTimeout</code> or <code>setInterval</code>.
- **ctx** Script execution closured context, by default it's clear that is why you can't use even <code>setTimeout</code> or <code>setInterval</code>.
- **access** Contains _absolute paths_ to nested modules or name of _npm/origin_ libraries as keys, with stub-content or boolean values, _by default_ you can't require nested modules.

```ts
Expand All @@ -169,25 +123,14 @@ type MODULE_TYPE = 'js' | 'cjs';
type TOptions<value> = { [key: string]: value };

interface VMScriptOptions extends BaseOptions {
__dirname?: string;
__filename?: string;
dir?: string;
filename?: string;
type?: MODULE_TYPE;
access?: TOptions<boolean | object>;
context?: Context;
ctx?: Context;
npmIsolation?: boolean;
runOptions?: RunningCodeOptions;
scriptOptions?: ScriptOptions;
}

class Script {
constructor(src: string, options?: VMScriptOptions);
__filename: string;
__dirname: string;
type: MODULE_TYPE;
access: TOptions<boolean | object>;
script: Script;
context: Context;
exports: any;
run?: RunningCodeOptions;
script?: ScriptOptions;
}
```

Expand Down
27 changes: 8 additions & 19 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,17 @@ export const COMMON_CTX: Context;
export const MODULE_TYPES: MODULE_TYPE[];

export interface VMScriptOptions extends BaseOptions {
__dirname?: string;
__filename?: string;
dir?: string;
filename?: string;
type?: MODULE_TYPE;
access?: TOptions<boolean | object>;
context?: Context;
runOptions?: RunningCodeOptions;
scriptOptions?: ScriptOptions;
ctx?: Context;
run?: RunningCodeOptions;
script?: ScriptOptions;
npmIsolation?: boolean;
}

export class Script {
constructor(src: string, options?: VMScriptOptions);
__filename: string;
__dirname: string;
type: MODULE_TYPE;
access: TOptions<boolean | object>;
script: Script;
context: Context;
exports: any;
}

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

const { basename, extname, join, dirname } = require('node:path');
const { promises } = require('node:fs');
const { readFile, readdir } = promises;

const Script = require('./src/script');
const { COMMON_CTX, MODULE_TYPES } = require('./src/config');
const { createContext, VMOptions } = require('./src/utils');

const readScript = async (filePath, options = {}) => {
const src = await readFile(filePath, 'utf8');
if (!src) throw new SyntaxError(`File ${filePath} is empty`);
return new Script(src, new VMOptions(options, { __filename: basename(filePath), __dirname: dirname(filePath) }));
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 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) });
};

const readDir = async (dir, options = {}, deep = true) => {
const files = await readdir(dir, { withFileTypes: true });
const scripts = {};

const loader = async (file, filePath, options) => {
const reader = file.isFile() ? readScript : readDir;
const ext = extname(file.name);
scripts[basename(file.name, MODULE_TYPES.includes(ext.slice(1)) ? ext : '')] = await reader(filePath, options);
const loader = async (file, path) => {
const [reader, ext] = [file.isFile() ? readScript : readDir, extname(file.name)];
scripts[basename(file.name, MODULE_TYPES.includes(ext.slice(1)) ? ext : '')] = await reader(path, options);
};

// prettier-ignore
await Promise.all(files.reduce((acc, file) => {
if (file.isDirectory() && !deep) return acc;
return acc.push(loader(file, join(dir, file.name), options)), acc;
if ((file.isDirectory() && !deep)) return acc;
return acc.push(loader(file, join(dir, file.name))), acc;
}, []));

return scripts;
};

const createScript = (src, options) => new Script(src, options);
module.exports = { Script, readScript, readDir, createContext, createScript, COMMON_CTX };
module.exports = { execute, readScript, readDir, createContext, COMMON_CTX };
18 changes: 18 additions & 0 deletions lib/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict';

const { createContext } = require('node:vm');

const WRAPPERS = {
js: src => `{\n${src}\n}`,
cjs: src => `(({exports, require, module, __filename, __dirname}) => {\n${src}\n});`,
};

const MODULE_TYPES = ['js', 'cjs'];
const RUN_OPTS = { timeout: 1000 };
const buffer = { Buffer, TextDecoder, TextEncoder };
const CTX_OPTS = { codeGeneration: { strings: false, wasm: false } };
const timers = { clearImmediate, clearInterval, clearTimeout, setInterval, setImmediate, setTimeout };
const ctx = Object.freeze({ ...timers, ...buffer, URL, URLSearchParams, console, queueMicrotask });
const [EMPTY_CTX, COMMON_CTX] = [createContext(Object.freeze({}), CTX_OPTS), createContext(ctx, CTX_OPTS)];

module.exports = { EMPTY_CTX, CTX_OPTS, COMMON_CTX, MODULE_TYPES, RUN_OPTS, WRAPPERS };
29 changes: 29 additions & 0 deletions lib/exec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict';

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

const cjs = (closure, options) => {
const exports = {};
const module = { exports };
closure({ exports, module, ...options });
return module.exports;
};

const execute = (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 exports = runner.runInContext(context, { ...RUN_OPTS, ...run });
return isJS ? exports : cjs(exports, defaultCTX);
};

module.exports = execute;
32 changes: 20 additions & 12 deletions src/require.js → lib/require.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
'use strict';

const { readFileSync } = require('node:fs');
const { scriptType } = require('./utils');
const { basename, dirname, resolve } = require('node:path');
const { VMError, checkAccess, scriptType } = require('./utils');
const internalRequire = require;
const { readFileSync } = require('node:fs');
const path = require('node:path');

const checkAccess = (module, access) => {
if (!access) return null;
const dir = path.join(module);
for (const key of Object.keys(access)) {
if (!dir.startsWith(key)) continue;
return Reflect.get(access, key);
}
return null;
};

const createRequire = (options, Script) => {
const { __dirname, npmIsolation, access } = options;
class VMError extends Error {}
const internalRequire = require;
module.exports = (options, execute) => {
const { dir, npmIsolation, access } = options;
return module => {
const npm = !module.includes('.');
const name = !npm ? resolve(__dirname, module) : module;
const name = !npm ? resolve(dir, module) : module;
const lib = checkAccess(name, access);

if (lib instanceof Object) return lib;
Expand All @@ -18,15 +30,11 @@ const createRequire = (options, Script) => {
const absolute = internalRequire.resolve(name);
if (npm && absolute === name) return internalRequire(name); //? Integrated nodejs API
if (npm && !npmIsolation) return internalRequire(absolute); //? VM uncover Npm packages

const [__filename, __dirname] = [basename(absolute), dirname(absolute)];
const type = scriptType(__filename);
return new Script(readFileSync(absolute, 'utf8'), { ...options, __filename, __dirname, type }).exports;
const [filename, dir] = [basename(absolute), dirname(absolute)];
return execute(readFileSync(absolute, 'utf8'), { ...options, filename, dir, type: scriptType(filename) });
} catch (err) {
if (err instanceof VMError) throw err;
throw new VMError(`Cannot find module '${module}'`);
}
};
};

module.exports = createRequire;
Loading

0 comments on commit 6e972b7

Please sign in to comment.