From 5b06de69b89e074fba21fb33ac0689292087952e Mon Sep 17 00:00:00 2001 From: Alessandro Molina Date: Sat, 9 Apr 2016 21:49:58 +0200 Subject: [PATCH] Experimental support for 'require' in js code --- dukpy/evaljs.py | 58 ++++++++++++++++++++++++++------ dukpy/module_loader.py | 65 ++++++++++++++++++++++++++++++++++++ setup.py | 4 ++- tests/tests_jsinterpreter.py | 15 ++++++++- 4 files changed, 130 insertions(+), 12 deletions(-) create mode 100644 dukpy/module_loader.py diff --git a/dukpy/evaljs.py b/dukpy/evaljs.py index edcd9b3..69e2b9a 100644 --- a/dukpy/evaljs.py +++ b/dukpy/evaljs.py @@ -1,4 +1,8 @@ +from __future__ import print_function import json +import os + +from dukpy.module_loader import JSModuleLoader from . import _dukpy try: @@ -16,19 +20,17 @@ except NameError: # pragma: no cover class JSInterpreter(object): """JavaScript Interpreter""" def __init__(self): + self._loader = JSModuleLoader() self._ctx = _dukpy.create_context() self._funcs = {} - def _adapt_code(self, code): - def _read_files(f): - if hasattr(f, 'read'): - return f.read() - else: - return f - code = _read_files(code) - if not isinstance(code, string_types) and hasattr(code, '__iter__'): - code = ';\n'.join(map(_read_files, code)) - return code + self._init_process() + self._init_console() + self._init_require() + + @property + def loader(self): + return self._loader def evaljs(self, code, **kwargs): """Runs JavaScript code in the context of the interpreter. @@ -73,6 +75,42 @@ class JSInterpreter(object): if ret is not None: return json.dumps(ret).encode('utf-8') + def _init_process(self): + self.evaljs("process = {}; process.env = dukpy.environ", environ=dict(os.environ)) + + def _init_console(self): + self.export_function('dukpy.print', print) + self.evaljs(""" + ;console = { + log: function() { + call_python('dukpy.print', Array.prototype.join.call(arguments, ' ')); + } + }; + """) + + def _init_require(self): + self.export_function('dukpy.lookup_module', self._loader.load) + self.evaljs(""" + ;Duktape.modSearch = function (id, require, exports, module) { + var m = call_python('dukpy.lookup_module', id); + if (!m) + throw new Error('cannot find module: ' + id); + return m; + }; +""") + + def _adapt_code(self, code): + def _read_files(f): + if hasattr(f, 'read'): + return f.read() + else: + return f + + code = _read_files(code) + if not isinstance(code, string_types) and hasattr(code, '__iter__'): + code = ';\n'.join(map(_read_files, code)) + return code + def evaljs(code, **kwargs): """Evaluates the given ``code`` as JavaScript and returns the result""" diff --git a/dukpy/module_loader.py b/dukpy/module_loader.py new file mode 100644 index 0000000..8b46f69 --- /dev/null +++ b/dukpy/module_loader.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +import json +import os + + +class JSModuleLoader(object): + """Manages finding and loading JS modules in CommonJS format. + + This allows to import a module from JSInterpreter using the + `require('modulename')` command. + + To register additional paths where to look for modules use + the `JSModuleLoader.register_path` method. + """ + def __init__(self): + self._paths = [] + self.register_path(os.path.join(os.path.dirname(__file__), 'jsmodules')) + self.register_path(os.getcwd()) + + def register_path(self, path): + """Registers a directory where to look for modules. + + By default only modules relative to current path are found. + """ + self._paths.insert(0, os.path.abspath(path)) + + def lookup(self, module_name): + """Searches for a file providing given module.""" + for search_path in self._paths: + module_path = os.path.join(search_path, module_name) + module_file = self._lookup(module_path) + if module_file: + return module_file + + def load(self, module_name): + """Returns source code of the given module.""" + path = self.lookup(module_name) + if path: + with open(path, 'rb') as f: + return f.read() + + def _lookup(self, module_path): + # Module is a plain .js file + for path in (module_path, os.path.extsep.join((module_path, 'js'))): + if os.path.exists(path) and os.path.isfile(path): + return path + + # Module is a package + package = os.path.join(module_path, os.path.extsep.join(('package', 'json'))) + try: + with open(package) as f: + package = json.load(f) + except IOError: + pass + else: + package_main = package.get('main') + if package_main: + path = self._lookup(os.path.join(module_path, package_main)) + if path: + return path + + # Module is directory with index.js inside + indexjs = os.path.join(module_path, os.path.extsep.join(('index', 'js'))) + if os.path.exists(indexjs): + return indexjs diff --git a/setup.py b/setup.py index 10a84e9..b17a75d 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,9 @@ except IOError: README = '' duktape = Extension('dukpy._dukpy', - define_macros=[('DUK_OPT_DEEP_C_STACK', '1')], + define_macros=[('DUK_OPT_DEEP_C_STACK', '1'), + ('DUK_OPT_NONSTD_REGEXP_DOLLAR_ESCAPE', '1'), + ('DUK_OPT_OCTAL_SUPPORT', '1')], sources=[os.path.join('src', 'duktape', 'duktape.c'), os.path.join('src','_support.c'), os.path.join('src','pyduktape.c')], diff --git a/tests/tests_jsinterpreter.py b/tests/tests_jsinterpreter.py index fab679e..fbc78d7 100644 --- a/tests/tests_jsinterpreter.py +++ b/tests/tests_jsinterpreter.py @@ -1,4 +1,5 @@ import dukpy +from diffreport import report_diff class TestJSInterpreter(object): @@ -16,4 +17,16 @@ class TestJSInterpreter(object): interpreter = dukpy.JSInterpreter() interpreter.export_function('say_hello', _say_hello) res = interpreter.evaljs("call_python('say_hello', 3, 'world')") - assert res == 'Hello world world world', res \ No newline at end of file + assert res == 'Hello world world world', res + + def test_module_loader(self): + interpreter = dukpy.JSInterpreter() + res = interpreter.evaljs(''' + babel = require('babel-6.4.4.min'); + babel.transform(dukpy.es6code, {presets: ["es2015"]}).code; +''', es6code='let i=5;') + + expected = '''"use strict"; + +var i = 5;''' + assert res == expected, report_diff(expected, res) \ No newline at end of file