Permalink
Browse files

core, vm: refactor module system

Make it more similar to Node y wrapping files in a CommonJS wrapper
before executing. Also expose __filename and __dirname.

This uses a slightly modified version of the Node module system Duktape
extra.

Refs: #32
  • Loading branch information...
1 parent 16ca862 commit 2569f12882a45995c92953ea9754b23bf0f497fa @saghul committed Jun 12, 2016
View
@@ -99,6 +99,7 @@ set_target_properties(duktape
# libsjs
#
set(SOURCES
+ src/duk_module_node.c
src/modules.c
src/system.c
src/util.c
@@ -7,6 +7,8 @@ Modules
`sjs` includes a builtin `Common JS <http://wiki.commonjs.org/wiki/Modules/1.1>`_ module loading system.
There is also a :ref:`stdlib` whith many modules. The goal is to provide a `kitchen-sink` collection of modules.
+Starting with version 0.4.0, `sjs` includes a module system more similar to Node.
+
.. _stdlib:
@@ -54,8 +56,13 @@ These functions / objects are accessible globally.
Utility function to write `data` to stdout.
-.. js:data:: __file__
+.. js:data:: __filename
A sort of global attribute containing the absolute path to the file. In the global scope it constains the filename
- which is currently being executed, ``input`` if running the REPL or ``eval`` if evaluating code straight from
- the CLI. Inside a module, it contains the absolute path to the module file.
+ which is currently being executed, ``<repl>`` if running in the REPL, ``<stdin>`` if code is being read from `stdin`,
+ or ``<eval>`` if evaluating code straight from the CLI. Inside a module, it contains the absolute path to the
+ module file.
+
+.. js:data:: __dirname
+
+ The directory of the file being evaluated, obtained by applying :man:`dirname(3)` over ``__filename``.
@@ -88,7 +88,7 @@ API
.. c:function:: int sjs_vm_eval_code(const sjs_vm_t* vm, const char* filename, const char* code, size_t len, FILE* foutput, FILE* ferror, bool use_strict)
- Evaluate the given JavaScript `code`.
+ Evaluate the given JavaScript `code`. The code is wrapped in a CommonJS module function and executed.
:param vm: The VM reference.
:param filename: Indicates the filename that is being executed. It will be printed in tracebacks and such.
@@ -99,9 +99,14 @@ API
:param use_strict: Indicates if the code should be evaluated in strict mode or not.
:returns: 0 if the code was evaluated without errors, != 0 otherwise.
+.. c:function:: int sjs_vm_eval_code_global(const sjs_vm_t* vm, const char* filename, const char* code, size_t len, FILE* foutput, FILE* ferror, bool use_strict)
+
+ Similar to :c:func:`sjs_vm_eval_code` but it evaluates the code in the global scope instead of creating a new
+ CommonJS style context.
+
.. c:function:: int sjs_vm_eval_file(const sjs_vm_t* vm, const char* filename, FILE* foutput, FILE* ferror, bool use_strict)
- Evaluate the given file as JavaScript code.
+ Evaluate the given file as JavaScript code. The code is wrapped in a CommonJS module function and executed.
:param vm: The VM reference.
:param filename: The file to be evaluated.
View
@@ -15,6 +15,13 @@ DUK_EXTERNAL_DECL sjs_vm_t* sjs_vm_create(void);
DUK_EXTERNAL_DECL void sjs_vm_destroy(sjs_vm_t* vm);
DUK_EXTERNAL_DECL void sjs_vm_setup_args(sjs_vm_t* vm, int argc, char* argv[]);
DUK_EXTERNAL_DECL duk_context* sjs_vm_get_duk_ctx(sjs_vm_t* vm);
+DUK_EXTERNAL_DECL int sjs_vm_eval_code_global(const sjs_vm_t* vm,
+ const char* filename,
+ const char* code,
+ size_t len,
+ FILE* foutput,
+ FILE* ferror,
+ bool use_strict);
DUK_EXTERNAL_DECL int sjs_vm_eval_code(const sjs_vm_t* vm,
const char* filename,
const char* code,
View
@@ -253,7 +253,7 @@ static int handle_stdin(void) {
bufoff += got;
}
- r = sjs_vm_eval_code(cli.vm, "stdin", buf, bufoff, NULL, stderr, cli.options.use_strict);
+ r = sjs_vm_eval_code(cli.vm, "<stdin>", buf, bufoff, NULL, stderr, cli.options.use_strict);
free(buf);
buf = NULL;
@@ -277,7 +277,7 @@ static int handle_interactive(void) {
char history_file[4096];
int use_history;
- sjs_vm_eval_code(cli.vm, "input", SJS__CLI_GREET_CODE, strlen(SJS__CLI_GREET_CODE), NULL, NULL, 0);
+ sjs_vm_eval_code_global(cli.vm, "<repl>", SJS__CLI_GREET_CODE, strlen(SJS__CLI_GREET_CODE), NULL, NULL, 0);
/* setup history file */
tmp = getenv("SJS_HISTORY_FILE");
@@ -302,7 +302,7 @@ static int handle_interactive(void) {
linenoiseHistoryAdd(line);
}
- sjs_vm_eval_code(cli.vm, "input", line, strlen(line), stdout, stdout, cli.options.use_strict);
+ sjs_vm_eval_code_global(cli.vm, "<repl>", line, strlen(line), stdout, stdout, cli.options.use_strict);
linenoiseFree(line);
}
@@ -383,7 +383,7 @@ int main(int argc, char *argv[]) {
}
break;
case SJS_CLI_EVAL:
- if (sjs_vm_eval_code(cli.vm, "eval", cli.options.data, strlen(cli.options.data), NULL, stderr, cli.options.use_strict) != 0) {
+ if (sjs_vm_eval_code(cli.vm, "<eval>", cli.options.data, strlen(cli.options.data), NULL, stderr, cli.options.use_strict) != 0) {
goto error;
}
break;
View
@@ -0,0 +1,285 @@
+/*
+ * Node.js-like module loading framework for Duktape
+ *
+ * https://nodejs.org/api/modules.html
+ */
+
+#include <libgen.h>
+
+#include "duktape.h"
+#include "duk_module_node.h"
+
+
+static duk_int_t duk__eval_module_source(duk_context *ctx);
+static void duk__push_module_object(duk_context *ctx, const char *id);
+
+static duk_bool_t duk__get_cached_module(duk_context *ctx, const char *id) {
+ duk_push_global_stash(ctx);
+ (void) duk_get_prop_string(ctx, -1, "\xff" "requireCache");
+ if (duk_get_prop_string(ctx, -1, id)) {
+ duk_remove(ctx, -2);
+ duk_remove(ctx, -2);
+ return 1;
+ } else {
+ duk_pop_3(ctx);
+ return 0;
+ }
+}
+
+/* Place a `module` object on the top of the value stack into the require cache
+ * based on its `.id` property. As a convenience to the caller, leave the
+ * object on top of the value stack afterwards.
+ */
+static void duk__put_cached_module(duk_context *ctx) {
+ /* [ ... module ] */
+
+ duk_push_global_stash(ctx);
+ (void) duk_get_prop_string(ctx, -1, "\xff" "requireCache");
+ duk_dup(ctx, -3);
+
+ /* [ ... module stash req_cache module ] */
+
+ (void) duk_get_prop_string(ctx, -1, "id");
+ duk_dup(ctx, -2);
+ duk_put_prop(ctx, -4);
+
+ duk_pop_3(ctx); /* [ ... module ] */
+}
+
+static void duk__del_cached_module(duk_context *ctx, const char *id) {
+ duk_push_global_stash(ctx);
+ (void) duk_get_prop_string(ctx, -1, "\xff" "requireCache");
+ duk_del_prop_string(ctx, -1, id);
+ duk_pop_2(ctx);
+}
+
+static duk_ret_t duk__handle_require(duk_context *ctx) {
+ /*
+ * Value stack handling here is a bit sloppy but should be correct.
+ * Call handling will clean up any extra garbage for us.
+ */
+
+ const char *id;
+ const char *parent_id;
+ duk_idx_t module_idx;
+ duk_idx_t stash_idx;
+ duk_int_t ret;
+
+ duk_push_global_stash(ctx);
+ stash_idx = duk_normalize_index(ctx, -1);
+
+ duk_push_current_function(ctx);
+ (void) duk_get_prop_string(ctx, -1, "\xff" "moduleId");
+ parent_id = duk_require_string(ctx, -1);
+ (void) parent_id; /* not used directly; suppress warning */
+
+ /* [ id stash require parent_id ] */
+
+ id = duk_require_string(ctx, 0);
+
+ (void) duk_get_prop_string(ctx, stash_idx, "\xff" "modResolve");
+ duk_dup(ctx, 0); /* module ID */
+ duk_dup(ctx, -3); /* parent ID */
+ duk_call(ctx, 2);
+
+ /* [ ... stash ... resolved_id ] */
+
+ id = duk_require_string(ctx, -1);
+
+ if (duk__get_cached_module(ctx, id)) {
+ goto have_module; /* use the cached module */
+ }
+
+ duk__push_module_object(ctx, id);
+ duk__put_cached_module(ctx); /* module remains on stack */
+
+ /*
+ * From here on out, we have to be careful not to throw. If it can't be
+ * avoided, the error must be caught and the module removed from the
+ * require cache before rethrowing. This allows the application to
+ * reattempt loading the module.
+ */
+
+ module_idx = duk_normalize_index(ctx, -1);
+
+ /* [ ... stash ... resolved_id module ] */
+
+ (void) duk_get_prop_string(ctx, stash_idx, "\xff" "modLoad");
+ duk_dup(ctx, -3); /* resolved ID */
+ (void) duk_get_prop_string(ctx, module_idx, "exports");
+ duk_dup(ctx, module_idx);
+ ret = duk_pcall(ctx, 3);
+ if (ret != DUK_EXEC_SUCCESS) {
+ duk__del_cached_module(ctx, id);
+ duk_throw(ctx); /* rethrow */
+ }
+
+ if (duk_is_string(ctx, -1)) {
+ duk_int_t ret;
+
+ /* [ ... module source ] */
+
+ ret = duk_safe_call(ctx, duk__eval_module_source, 2, 1);
+ if (ret != DUK_EXEC_SUCCESS) {
+ duk__del_cached_module(ctx, id);
+ duk_throw(ctx); /* rethrow */
+ }
+ } else if (duk_is_undefined(ctx, -1)) {
+ duk_pop(ctx);
+ } else {
+ duk__del_cached_module(ctx, id);
+ duk_error(ctx, DUK_ERR_API_ERROR, "invalid module load callback return value");
+ }
+
+ /* fall through */
+
+ have_module:
+ /* [ ... module ] */
+
+ (void) duk_get_prop_string(ctx, -1, "exports");
+ return 1;
+}
+
+static void duk__push_require_function(duk_context *ctx, const char *id) {
+ duk_push_c_function(ctx, duk__handle_require, 1);
+ duk_push_string(ctx, "name");
+ duk_push_string(ctx, "require");
+ duk_def_prop(ctx, -3, DUK_DEFPROP_HAVE_VALUE);
+ duk_push_string(ctx, id);
+ duk_put_prop_string(ctx, -2, "\xff" "moduleId");
+
+ /* require.cache */
+ duk_push_global_stash(ctx);
+ (void) duk_get_prop_string(ctx, -1, "\xff" "requireCache");
+ duk_put_prop_string(ctx, -3, "cache");
+ duk_pop(ctx);
+}
+
+static void duk__push_module_object(duk_context *ctx, const char *id) {
+ duk_push_object(ctx);
+
+ /* Node.js uses the canonicalized filename of a module for both module.id
+ * and module.filename. We have no concept of a file system here, so just
+ * use the module ID for both values.
+ */
+ duk_push_string(ctx, id);
+ duk_dup(ctx, -1);
+ duk_put_prop_string(ctx, -3, "filename");
+ duk_put_prop_string(ctx, -2, "id");
+
+ /* module.exports = {} */
+ duk_push_object(ctx);
+ duk_put_prop_string(ctx, -2, "exports");
+
+ /* module.loaded = false */
+ duk_push_false(ctx);
+ duk_put_prop_string(ctx, -2, "loaded");
+
+ /* module.require */
+ duk__push_require_function(ctx, id);
+ duk_put_prop_string(ctx, -2, "require");
+}
+
+static duk_int_t duk__eval_module_source(duk_context *ctx) {
+ /*
+ * Stack: [ ... module source ]
+ */
+
+ const char* filename;
+ char tmp[8192];
+
+ /* Wrap the module code in a function expression. This is the simplest
+ * way to implement CommonJS closure semantics and matches the behavior of
+ * e.g. Node.js.
+ */
+ duk_push_string(ctx, "(function main(exports,require,module,__filename,__dirname){");
+ duk_dup(ctx, -2); /* source */
+ duk_push_string(ctx, "})");
+ duk_concat(ctx, 3);
+
+ /* [ ... module source func_src ] */
+
+ (void) duk_get_prop_string(ctx, -3, "filename");
+ filename = duk_get_string(ctx, -1);
+ strcpy(tmp, filename);
+ duk_compile(ctx, DUK_COMPILE_EVAL);
+ duk_call(ctx, 0);
+
+ /* [ ... module source func ] */
+
+ /* call the function wrapper */
+ (void) duk_get_prop_string(ctx, -3, "exports"); /* exports */
+ (void) duk_get_prop_string(ctx, -4, "require"); /* require */
+ duk_dup(ctx, -5); /* module */
+ (void) duk_get_prop_string(ctx, -6, "filename"); /* __filename */
+ duk_push_string(ctx, dirname(tmp)); /* __dirname */
+ duk_call(ctx, 5);
+
+ /* [ ... module source result(ignore) ] */
+
+ /* module.loaded = true */
+ duk_push_true(ctx);
+ duk_put_prop_string(ctx, -4, "loaded");
+
+ /* [ ... module source retval ] */
+
+ duk_pop_2(ctx);
+
+ /* [ ... module ] */
+
+ return 1;
+}
+
+duk_ret_t duk_module_node_eval_code(duk_context *ctx, const char* filename) {
+ /*
+ * Stack: [ ... source ]
+ */
+
+ duk__push_module_object(ctx, filename);
+ /* [ ... source module ] */
+
+ duk_dup(ctx, 0);
+ /* [ ... source module source ] */
+
+ return duk_safe_call(ctx, duk__eval_module_source, 2, 1);
+}
+
+void duk_module_node_init(duk_context *ctx) {
+ /*
+ * Stack: [ ... options ] => [ ... ]
+ */
+
+ duk_idx_t options_idx;
+
+ duk_require_object_coercible(ctx, -1); /* error before setting up requireCache */
+ options_idx = duk_require_normalize_index(ctx, -1);
+
+ /* Initialize the require cache to a fresh object. */
+ duk_push_global_stash(ctx);
+ duk_push_object(ctx);
+ duk_put_prop_string(ctx, -2, "\xff" "requireCache");
+ duk_pop(ctx);
+
+ /* Stash callbacks for later use. User code can overwrite them later
+ * on directly by accessing the global stash.
+ */
+ duk_push_global_stash(ctx);
+ duk_get_prop_string(ctx, options_idx, "resolve");
+ duk_require_function(ctx, -1);
+ duk_put_prop_string(ctx, -2, "\xff" "modResolve");
+ duk_get_prop_string(ctx, options_idx, "load");
+ duk_require_function(ctx, -1);
+ duk_put_prop_string(ctx, -2, "\xff" "modLoad");
+ duk_pop(ctx);
+
+ /* register `require` as a global function */
+ duk_push_global_object(ctx);
+ duk_push_string(ctx, "require");
+ duk__push_require_function(ctx, "");
+ duk_def_prop(ctx, -3, DUK_DEFPROP_HAVE_VALUE |
+ DUK_DEFPROP_SET_WRITABLE |
+ DUK_DEFPROP_SET_CONFIGURABLE);
+ duk_pop(ctx);
+
+ duk_pop(ctx); /* pop argument */
+}
Oops, something went wrong.

0 comments on commit 2569f12

Please sign in to comment.