]> git.kaiwu.me - njs.git/commitdiff
Modules: added jsVarNames() method.
authorDmitry Volyntsev <xeioex@nginx.com>
Sat, 16 May 2026 22:14:37 +0000 (15:14 -0700)
committerDmitry Volyntsev <xeioexception@gmail.com>
Mon, 18 May 2026 17:45:51 +0000 (10:45 -0700)
The method returns names of variables declared with js_var.  An optional
string argument filters returned names by prefix.

This lets JavaScript code discover a configuration-defined variable schema
without duplicating every variable name in the script.  For example:

    js_var $extract_name;
    js_var $extract_arguments_workspace;

    function handler(r) {
        r.jsVarNames("extract_").forEach(function(name) {
            r.variables[name] = extract(name.slice(8));
        });
    }

The same API is available as s.jsVarNames() in stream.  Variables created by
js_set or by the core modules are not returned.

nginx/ngx_http_js_module.c
nginx/ngx_stream_js_module.c
nginx/t/js_var_names.t [new file with mode: 0644]
nginx/t/stream_js_var_names.t [new file with mode: 0644]

index 2ac3963798d6fcded6f186c4fc4cc8c53f628584..530bea320f49912d0fbcec33e857e58a5fd7ddfb 100644 (file)
@@ -366,6 +366,9 @@ static njs_int_t ngx_http_js_ext_variables(njs_vm_t *vm,
 static njs_int_t ngx_http_js_periodic_session_variables(njs_vm_t *vm,
     njs_object_prop_t *prop, uint32_t atom_id, njs_value_t *value,
     njs_value_t *setval, njs_value_t *retval);
+static njs_int_t ngx_http_js_ext_js_var_names(njs_vm_t *vm,
+    njs_value_t *args, njs_uint_t nargs, njs_index_t unused,
+    njs_value_t *retval);
 static njs_int_t ngx_http_js_ext_subrequest(njs_vm_t *vm, njs_value_t *args,
     njs_uint_t nargs, njs_index_t unused, njs_value_t *retval);
 static ngx_int_t ngx_http_js_subrequest_done(ngx_http_request_t *r,
@@ -461,6 +464,8 @@ static JSValue ngx_http_qjs_ext_raw_headers(JSContext *cx,
     JSValueConst this_val, int out);
 static JSValue ngx_http_qjs_ext_variables(JSContext *cx,
     JSValueConst this_val, int type);
+static JSValue ngx_http_qjs_ext_js_var_names(JSContext *cx,
+    JSValueConst this_val, int argc, JSValueConst *argv);
 
 static int ngx_http_qjs_variables_own_property(JSContext *cx,
     JSPropertyDescriptor *pdesc, JSValueConst obj, JSAtom prop);
@@ -1115,6 +1120,17 @@ static njs_external_t  ngx_http_js_ext_request[] = {
         }
     },
 
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("jsVarNames"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = ngx_http_js_ext_js_var_names,
+        }
+    },
+
     {
         .flags = NJS_EXTERN_PROPERTY,
         .name.string = njs_str("status"),
@@ -1407,6 +1423,7 @@ static const JSCFunctionListEntry ngx_http_qjs_ext_request[] = {
     JS_CFUNC_DEF("sendBuffer", 2, ngx_http_qjs_ext_send_buffer),
     JS_CFUNC_DEF("sendHeader", 0, ngx_http_qjs_ext_send_header),
     JS_CFUNC_DEF("setReturnValue", 1, ngx_http_qjs_ext_set_return_value),
+    JS_CFUNC_DEF("jsVarNames", 1, ngx_http_qjs_ext_js_var_names),
     JS_CGETSET_DEF("status", ngx_http_qjs_ext_status_get,
                    ngx_http_qjs_ext_status_set),
     JS_CFUNC_MAGIC_DEF("readRequestArrayBuffer", 0,
@@ -4439,6 +4456,88 @@ ngx_http_js_ext_keys_header_in(njs_vm_t *vm, njs_value_t *value,
 }
 
 
+static ngx_uint_t
+ngx_http_js_var_name_matches(ngx_str_t *name, const u_char *prefix,
+    size_t prefix_len)
+{
+    if (prefix_len == 0) {
+        return 1;
+    }
+
+    return name->len >= prefix_len
+           && ngx_memcmp(name->data, prefix, prefix_len) == 0;
+}
+
+
+static njs_int_t
+ngx_http_js_ext_js_var_names(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t unused, njs_value_t *retval)
+{
+    njs_int_t                   rc;
+    njs_str_t                   prefix;
+    njs_value_t                *arg, *value;
+    ngx_uint_t                  i;
+    ngx_http_request_t         *r;
+    ngx_http_variable_t        *v;
+    ngx_http_core_main_conf_t  *cmcf;
+
+    r = njs_vm_external(vm, ngx_http_js_request_proto_id,
+                        njs_argument(args, 0));
+    if (r == NULL) {
+        njs_vm_error(vm, "\"this\" is not an external");
+        return NJS_ERROR;
+    }
+
+    prefix.start = NULL;
+    prefix.length = 0;
+
+    arg = njs_arg(args, nargs, 1);
+
+    if (!njs_value_is_undefined(arg)) {
+        if (!njs_value_is_string(arg)) {
+            njs_vm_type_error(vm, "\"prefix\" must be a string");
+            return NJS_ERROR;
+        }
+
+        njs_value_string_get(vm, arg, &prefix);
+    }
+
+    cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
+
+    rc = njs_vm_array_alloc(vm, retval, 4);
+    if (rc != NJS_OK) {
+        return NJS_ERROR;
+    }
+
+    v = cmcf->variables.elts;
+
+    for (i = 0; i < cmcf->variables.nelts; i++) {
+        if (v[i].get_handler != ngx_http_js_variable_var) {
+            continue;
+        }
+
+        if (!ngx_http_js_var_name_matches(&v[i].name, prefix.start,
+                                          prefix.length))
+        {
+            continue;
+        }
+
+        value = njs_vm_array_push(vm, retval);
+        if (value == NULL) {
+            return NJS_ERROR;
+        }
+
+        rc = njs_vm_value_string_create(vm, value, v[i].name.data,
+                                        v[i].name.len);
+        if (rc != NJS_OK) {
+            return NJS_ERROR;
+        }
+    }
+
+    return NJS_OK;
+}
+
+
 static njs_int_t
 ngx_http_js_request_variables(njs_vm_t *vm, njs_object_prop_t *prop,
     uint32_t atom_id, ngx_http_request_t *r, njs_value_t *setval,
@@ -7910,6 +8009,81 @@ ngx_http_qjs_ext_variables(JSContext *cx, JSValueConst this_val, int type)
 }
 
 
+static JSValue
+ngx_http_qjs_ext_js_var_names(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv)
+{
+    JSValue                     array, value;
+    uint32_t                    n;
+    size_t                      prefix_len;
+    const char                 *prefix;
+    ngx_uint_t                  i;
+    ngx_http_request_t         *r;
+    ngx_http_variable_t        *v;
+    ngx_http_core_main_conf_t  *cmcf;
+
+    r = ngx_http_qjs_request(this_val);
+    if (r == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
+    }
+
+    prefix = NULL;
+    prefix_len = 0;
+
+    if (argc > 0 && !JS_IsUndefined(argv[0])) {
+        if (!JS_IsString(argv[0])) {
+            return JS_ThrowTypeError(cx, "\"prefix\" must be a string");
+        }
+
+        prefix = JS_ToCStringLen(cx, &prefix_len, argv[0]);
+        if (prefix == NULL) {
+            return JS_EXCEPTION;
+        }
+    }
+
+    array = JS_NewArray(cx);
+    if (JS_IsException(array)) {
+        JS_FreeCString(cx, prefix);
+        return JS_EXCEPTION;
+    }
+
+    n = 0;
+    cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
+    v = cmcf->variables.elts;
+
+    for (i = 0; i < cmcf->variables.nelts; i++) {
+        if (v[i].get_handler != ngx_http_js_variable_var) {
+            continue;
+        }
+
+        if (!ngx_http_js_var_name_matches(&v[i].name, (u_char *) prefix,
+                                          prefix_len))
+        {
+            continue;
+        }
+
+        value = qjs_string_create(cx, v[i].name.data, v[i].name.len);
+        if (JS_IsException(value)) {
+            JS_FreeCString(cx, prefix);
+            JS_FreeValue(cx, array);
+            return JS_EXCEPTION;
+        }
+
+        if (JS_DefinePropertyValueUint32(cx, array, n++, value,
+                                         JS_PROP_C_W_E) < 0)
+        {
+            JS_FreeCString(cx, prefix);
+            JS_FreeValue(cx, array);
+            return JS_EXCEPTION;
+        }
+    }
+
+    JS_FreeCString(cx, prefix);
+
+    return array;
+}
+
+
 static int
 ngx_http_qjs_variables_own_property(JSContext *cx, JSPropertyDescriptor *pdesc,
     JSValueConst obj, JSAtom prop)
index 3b12a3356514ec9c8d879410d14e7a81524e0a76..5e550c571be28aa3ab79133fa79a724d7f85110b 100644 (file)
@@ -138,6 +138,9 @@ static njs_int_t ngx_stream_js_ext_send(njs_vm_t *vm, njs_value_t *args,
 static njs_int_t ngx_stream_js_ext_set_return_value(njs_vm_t *vm,
     njs_value_t *args, njs_uint_t nargs, njs_index_t unused,
     njs_value_t *retval);
+static njs_int_t ngx_stream_js_ext_js_var_names(njs_vm_t *vm,
+    njs_value_t *args, njs_uint_t nargs, njs_index_t unused,
+    njs_value_t *retval);
 
 static njs_int_t ngx_stream_js_ext_variables(njs_vm_t *vm,
     njs_object_prop_t *prop, uint32_t atom_id, njs_value_t *value,
@@ -164,6 +167,8 @@ static JSValue ngx_stream_qjs_ext_send(JSContext *cx, JSValueConst this_val,
     int argc, JSValueConst *argv, int from_upstream);
 static JSValue ngx_stream_qjs_ext_set_return_value(JSContext *cx,
     JSValueConst this_val, int argc, JSValueConst *argv);
+static JSValue ngx_stream_qjs_ext_js_var_names(JSContext *cx,
+    JSValueConst this_val, int argc, JSValueConst *argv);
 static JSValue ngx_stream_qjs_ext_variables(JSContext *cx,
     JSValueConst this_val, int type);
 static JSValue ngx_stream_qjs_ext_uint(JSContext *cx, JSValueConst this_val,
@@ -676,6 +681,17 @@ static njs_external_t  ngx_stream_js_ext_session[] = {
         }
     },
 
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("jsVarNames"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = ngx_stream_js_ext_js_var_names,
+        }
+    },
+
     {
         .flags = NJS_EXTERN_PROPERTY,
         .name.string = njs_str("status"),
@@ -837,6 +853,7 @@ static const JSCFunctionListEntry ngx_stream_qjs_ext_session[] = {
     JS_CFUNC_MAGIC_DEF("sendUpstream", 1, ngx_stream_qjs_ext_send,
                        NGX_JS_BOOL_FALSE),
     JS_CFUNC_DEF("setReturnValue", 1, ngx_stream_qjs_ext_set_return_value),
+    JS_CFUNC_DEF("jsVarNames", 1, ngx_stream_qjs_ext_js_var_names),
     JS_CGETSET_MAGIC_DEF("status", ngx_stream_qjs_ext_uint, NULL,
                          offsetof(ngx_stream_session_t, status)),
     JS_CGETSET_MAGIC_DEF("variables", ngx_stream_qjs_ext_variables,
@@ -1751,6 +1768,88 @@ ngx_stream_js_ext_set_return_value(njs_vm_t *vm, njs_value_t *args,
 }
 
 
+static ngx_uint_t
+ngx_stream_js_var_name_matches(ngx_str_t *name, const u_char *prefix,
+    size_t prefix_len)
+{
+    if (prefix_len == 0) {
+        return 1;
+    }
+
+    return name->len >= prefix_len
+           && ngx_memcmp(name->data, prefix, prefix_len) == 0;
+}
+
+
+static njs_int_t
+ngx_stream_js_ext_js_var_names(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t unused, njs_value_t *retval)
+{
+    njs_int_t                     rc;
+    njs_str_t                     prefix;
+    njs_value_t                  *arg, *value;
+    ngx_uint_t                    i;
+    ngx_stream_session_t         *s;
+    ngx_stream_variable_t        *v;
+    ngx_stream_core_main_conf_t  *cmcf;
+
+    s = njs_vm_external(vm, ngx_stream_js_session_proto_id,
+                        njs_argument(args, 0));
+    if (s == NULL) {
+        njs_vm_error(vm, "\"this\" is not an external");
+        return NJS_ERROR;
+    }
+
+    prefix.start = NULL;
+    prefix.length = 0;
+
+    arg = njs_arg(args, nargs, 1);
+
+    if (!njs_value_is_undefined(arg)) {
+        if (!njs_value_is_string(arg)) {
+            njs_vm_type_error(vm, "\"prefix\" must be a string");
+            return NJS_ERROR;
+        }
+
+        njs_value_string_get(vm, arg, &prefix);
+    }
+
+    cmcf = ngx_stream_get_module_main_conf(s, ngx_stream_core_module);
+
+    rc = njs_vm_array_alloc(vm, retval, 4);
+    if (rc != NJS_OK) {
+        return NJS_ERROR;
+    }
+
+    v = cmcf->variables.elts;
+
+    for (i = 0; i < cmcf->variables.nelts; i++) {
+        if (v[i].get_handler != ngx_stream_js_variable_var) {
+            continue;
+        }
+
+        if (!ngx_stream_js_var_name_matches(&v[i].name, prefix.start,
+                                            prefix.length))
+        {
+            continue;
+        }
+
+        value = njs_vm_array_push(vm, retval);
+        if (value == NULL) {
+            return NJS_ERROR;
+        }
+
+        rc = njs_vm_value_string_create(vm, value, v[i].name.data,
+                                        v[i].name.len);
+        if (rc != NJS_OK) {
+            return NJS_ERROR;
+        }
+    }
+
+    return NJS_OK;
+}
+
+
 static njs_int_t
 ngx_stream_js_session_variables(njs_vm_t *vm, njs_object_prop_t *prop,
     uint32_t atom_id, ngx_stream_session_t *s, njs_value_t *setval,
@@ -2520,6 +2619,81 @@ ngx_stream_qjs_ext_variables(JSContext *cx, JSValueConst this_val, int type)
 }
 
 
+static JSValue
+ngx_stream_qjs_ext_js_var_names(JSContext *cx, JSValueConst this_val, int argc,
+    JSValueConst *argv)
+{
+    JSValue                       array, value;
+    uint32_t                      n;
+    size_t                        prefix_len;
+    const char                   *prefix;
+    ngx_uint_t                    i;
+    ngx_stream_session_t         *s;
+    ngx_stream_variable_t        *v;
+    ngx_stream_core_main_conf_t  *cmcf;
+
+    s = ngx_stream_qjs_session(this_val);
+    if (s == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a session object");
+    }
+
+    prefix = NULL;
+    prefix_len = 0;
+
+    if (argc > 0 && !JS_IsUndefined(argv[0])) {
+        if (!JS_IsString(argv[0])) {
+            return JS_ThrowTypeError(cx, "\"prefix\" must be a string");
+        }
+
+        prefix = JS_ToCStringLen(cx, &prefix_len, argv[0]);
+        if (prefix == NULL) {
+            return JS_EXCEPTION;
+        }
+    }
+
+    array = JS_NewArray(cx);
+    if (JS_IsException(array)) {
+        JS_FreeCString(cx, prefix);
+        return JS_EXCEPTION;
+    }
+
+    n = 0;
+    cmcf = ngx_stream_get_module_main_conf(s, ngx_stream_core_module);
+    v = cmcf->variables.elts;
+
+    for (i = 0; i < cmcf->variables.nelts; i++) {
+        if (v[i].get_handler != ngx_stream_js_variable_var) {
+            continue;
+        }
+
+        if (!ngx_stream_js_var_name_matches(&v[i].name, (u_char *) prefix,
+                                            prefix_len))
+        {
+            continue;
+        }
+
+        value = qjs_string_create(cx, v[i].name.data, v[i].name.len);
+        if (JS_IsException(value)) {
+            JS_FreeCString(cx, prefix);
+            JS_FreeValue(cx, array);
+            return JS_EXCEPTION;
+        }
+
+        if (JS_DefinePropertyValueUint32(cx, array, n++, value,
+                                         JS_PROP_C_W_E) < 0)
+        {
+            JS_FreeCString(cx, prefix);
+            JS_FreeValue(cx, array);
+            return JS_EXCEPTION;
+        }
+    }
+
+    JS_FreeCString(cx, prefix);
+
+    return array;
+}
+
+
 static JSValue
 ngx_stream_qjs_ext_uint(JSContext *cx, JSValueConst this_val, int offset)
 {
diff --git a/nginx/t/js_var_names.t b/nginx/t/js_var_names.t
new file mode 100644 (file)
index 0000000..0054d5b
--- /dev/null
@@ -0,0 +1,178 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) F5, Inc.
+
+# Tests for http njs module, r.jsVarNames() method.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/http rewrite/)
+    ->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+http {
+    %%TEST_GLOBALS_HTTP%%
+
+    js_import test.js;
+
+    js_var $foo;
+    js_var $test_method;
+    js_var $test_params_name;
+    js_var $test_params_arguments_workspace;
+    js_var $other_name;
+    js_set $js_set_var test.set_var;
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        set $set_var conf_set;
+
+        location /all {
+            js_content test.all;
+        }
+
+        location /prefix {
+            js_content test.prefix;
+        }
+
+        location /empty_prefix {
+            js_content test.empty_prefix;
+        }
+
+        location /none {
+            js_content test.none;
+        }
+
+        location /array {
+            js_content test.array;
+        }
+
+        location /excludes {
+            js_content test.excludes;
+        }
+
+        location /fresh {
+            js_content test.fresh;
+        }
+
+        location /type {
+            js_content test.type;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<'EOF');
+    function render(names) {
+        return names.sort().join('|');
+    }
+
+    function all(r) {
+        r.return(200, render(r.jsVarNames()));
+    }
+
+    function prefix(r) {
+        r.return(200, render(r.jsVarNames('test_')));
+    }
+
+    function empty_prefix(r) {
+        r.return(200, render(r.jsVarNames('')));
+    }
+
+    function none(r) {
+        r.return(200, String(r.jsVarNames('none_').length));
+    }
+
+    function array(r) {
+        r.return(200, String(Array.isArray(r.jsVarNames())));
+    }
+
+    function excludes(r) {
+        let names = r.jsVarNames();
+
+        r.return(200, String(names.indexOf('js_set_var') == -1
+                             && names.indexOf('set_var') == -1
+                             && names.indexOf('remote_addr') == -1));
+    }
+
+    function fresh(r) {
+        let names = r.jsVarNames('test_');
+        names.push('test_bad');
+
+        r.return(200, r.jsVarNames('test_').indexOf('test_bad') == -1
+                      ? 'fresh' : 'shared');
+    }
+
+    function type(r) {
+        try {
+            r.jsVarNames(1);
+            r.return(200, 'no error');
+
+        } catch (e) {
+            r.return(200, e.name + ':' + e.message);
+        }
+    }
+
+    function set_var(r) {
+        return 'set';
+    }
+
+    export default {all, prefix, empty_prefix, none, array, excludes, fresh,
+                    type, set_var};
+EOF
+
+$t->try_run('no r.jsVarNames')->plan(8);
+
+###############################################################################
+
+my $all = 'foo|other_name|test_method|'
+    . 'test_params_arguments_workspace|test_params_name';
+
+my $prefix = 'test_method|test_params_arguments_workspace|test_params_name';
+
+is(http_get_body('/all'), $all, 'jsVarNames all js_var names');
+is(http_get_body('/prefix'), $prefix, 'jsVarNames prefix');
+is(http_get_body('/empty_prefix'), $all, 'jsVarNames empty prefix');
+is(http_get_body('/none'), '0', 'jsVarNames prefix no match');
+is(http_get_body('/array'), 'true', 'jsVarNames returns an array');
+is(http_get_body('/excludes'), 'true', 'jsVarNames excludes other variables');
+is(http_get_body('/fresh'), 'fresh', 'jsVarNames fresh array');
+like(http_get_body('/type'), qr/^TypeError:.*prefix.*string/,
+    'jsVarNames prefix type');
+
+$t->stop();
+
+###############################################################################
+
+sub http_get_body {
+    my ($uri) = @_;
+
+    http_get($uri) =~ /\x0d\x0a?\x0d\x0a?(.*)/ms;
+    return $1;
+}
+
+###############################################################################
diff --git a/nginx/t/stream_js_var_names.t b/nginx/t/stream_js_var_names.t
new file mode 100644 (file)
index 0000000..9c94919
--- /dev/null
@@ -0,0 +1,184 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) F5, Inc.
+
+# Tests for stream njs module, s.jsVarNames() method.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+use Test::Nginx::Stream qw/ stream /;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/stream stream_return/)
+       ->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+stream {
+    %%TEST_GLOBALS_STREAM%%
+
+    js_import test.js;
+
+    js_var $foo;
+    js_var $test_method;
+    js_var $test_params_name;
+    js_var $test_params_arguments_workspace;
+    js_var $other_name;
+    js_set $js_set_var test.set_var;
+    js_set $all test.all;
+    js_set $prefix test.prefix;
+    js_set $empty_prefix test.empty_prefix;
+    js_set $none test.none;
+    js_set $array test.array;
+    js_set $excludes test.excludes;
+    js_set $fresh test.fresh;
+    js_set $type test.type;
+    server {
+        listen  127.0.0.1:8081;
+        return  $all;
+    }
+
+    server {
+        listen  127.0.0.1:8082;
+        return  $prefix;
+    }
+
+    server {
+        listen  127.0.0.1:8083;
+        return  $empty_prefix;
+    }
+
+    server {
+        listen  127.0.0.1:8084;
+        return  $none;
+    }
+
+    server {
+        listen  127.0.0.1:8085;
+        return  $array;
+    }
+
+    server {
+        listen  127.0.0.1:8086;
+        return  $excludes;
+    }
+
+    server {
+        listen  127.0.0.1:8087;
+        return  $fresh;
+    }
+
+    server {
+        listen  127.0.0.1:8088;
+        return  $type;
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<'EOF');
+    function render(names) {
+        return names.sort().join('|');
+    }
+
+    function all(s) {
+        return render(s.jsVarNames());
+    }
+
+    function prefix(s) {
+        return render(s.jsVarNames('test_'));
+    }
+
+    function empty_prefix(s) {
+        return render(s.jsVarNames(''));
+    }
+
+    function none(s) {
+        return String(s.jsVarNames('none_').length);
+    }
+
+    function array(s) {
+        return String(Array.isArray(s.jsVarNames()));
+    }
+
+    function excludes(s) {
+        let names = s.jsVarNames();
+
+        return String(names.indexOf('js_set_var') == -1
+                      && names.indexOf('remote_addr') == -1);
+    }
+
+    function fresh(s) {
+        let names = s.jsVarNames('test_');
+        names.push('test_bad');
+
+        return s.jsVarNames('test_').indexOf('test_bad') == -1
+               ? 'fresh' : 'shared';
+    }
+
+    function type(s) {
+        try {
+            s.jsVarNames(1);
+            return 'no error';
+
+        } catch (e) {
+            return e.name + ':' + e.message;
+        }
+    }
+
+    function set_var(s) {
+        return 'set';
+    }
+
+    export default {all, prefix, empty_prefix, none, array, excludes, fresh,
+                    type, set_var};
+EOF
+
+$t->try_run('no s.jsVarNames')->plan(8);
+
+###############################################################################
+
+my $all = 'foo|other_name|test_method|'
+       . 'test_params_arguments_workspace|test_params_name';
+
+my $prefix = 'test_method|test_params_arguments_workspace|test_params_name';
+
+is(stream('127.0.0.1:' . port(8081))->read(), $all,
+       'jsVarNames all js_var names');
+is(stream('127.0.0.1:' . port(8082))->read(), $prefix,
+       'jsVarNames prefix');
+is(stream('127.0.0.1:' . port(8083))->read(), $all,
+       'jsVarNames empty prefix');
+is(stream('127.0.0.1:' . port(8084))->read(), '0',
+       'jsVarNames prefix no match');
+is(stream('127.0.0.1:' . port(8085))->read(), 'true',
+       'jsVarNames returns an array');
+is(stream('127.0.0.1:' . port(8086))->read(), 'true',
+       'jsVarNames excludes other variables');
+is(stream('127.0.0.1:' . port(8087))->read(), 'fresh',
+       'jsVarNames fresh array');
+like(stream('127.0.0.1:' . port(8088))->read(),
+       qr/^TypeError:.*prefix.*string/, 'jsVarNames prefix type');
+
+$t->stop();
+
+###############################################################################