Skip to content

WIP: Implement Import assertions and JSON modules #1039

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 84 additions & 11 deletions quickjs-libc.c
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,35 @@ int js_module_set_import_meta(JSContext *ctx, JSValueConst func_val,
return 0;
}

static int js_module_loader_json(JSContext *ctx, JSModuleDef *m)
{
size_t buf_len;
uint8_t *buf;
JSValue parsed;
JSPropertyEnum *props;
uint32_t len;
JSAtom module_name = JS_GetModuleName(ctx, m);
const char *module_name_cstr = JS_AtomToCString(ctx, module_name);

buf = js_load_file(ctx, &buf_len, module_name_cstr);

/* XXX: Not ideal to parse the file twice, but didn't want to introduce
extra state to JSModuleDef like opaque */
parsed = JS_ParseJSON(ctx, (const char*) buf, buf_len, module_name_cstr);

JS_GetOwnPropertyNames(ctx, &props, &len, parsed, JS_GPN_STRING_MASK);

for (uint32_t i = 0; i < len; i++) {
JSValue val = JS_GetProperty(ctx, parsed, props[i].atom);
JS_SetModuleExport(ctx, m, JS_AtomToCString(ctx, props[i].atom), val);
}

JS_FreePropertyEnum(ctx, props, len);
JS_FreeValue(ctx, parsed);
js_free(ctx, buf);
return 0;
}

JSModuleDef *js_module_loader(JSContext *ctx,
const char *module_name, void *opaque)
{
Expand All @@ -788,19 +817,63 @@ JSModuleDef *js_module_loader(JSContext *ctx,
return NULL;
}

/* compile the module */
func_val = JS_Eval(ctx, (char *)buf, buf_len, module_name,
JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
js_free(ctx, buf);
if (JS_IsException(func_val))
return NULL;
if (js_module_set_import_meta(ctx, func_val, true, false) < 0) {
JSValue with_clause = JS_GetImportAssertion(ctx);
if (JS_IsArray(with_clause)) {
int64_t array_len;
JS_GetLength(ctx, with_clause, &array_len);

for (int64_t i = 0; i < array_len; i += 2) {
JSValue prop = JS_GetPropertyInt64(ctx, with_clause, i);
const char *name = JS_ToCString(ctx, prop);
if (strcmp(name, "type") == 0) {
JSValue key = JS_GetPropertyInt64(ctx, with_clause, i + 1);
if (!JS_IsString(key)) {
JS_ThrowTypeError(ctx, "value of 'type' is expecting string");
return NULL;
}

const char *str = JS_ToCString(ctx, key);
if (strcmp(str, "json") != 0) {
JS_ThrowTypeError(ctx, "'type' is not 'json'");
return NULL;
}
break;
}
}

m = JS_NewCModule(ctx, module_name, js_module_loader_json);

if (!m)
return NULL;

JSValue parsed = JS_ParseJSON(ctx, (const char*) buf, buf_len, module_name);

JSPropertyEnum *props;
uint32_t len;

JS_GetOwnPropertyNames(ctx, &props, &len, parsed, JS_GPN_STRING_MASK);

for (uint32_t i = 0; i < len; i++) {
JS_AddModuleExport(ctx, m, JS_AtomToCString(ctx, props[i].atom));
}

JS_FreePropertyEnum(ctx, props, len);
JS_FreeValue(ctx, parsed);
} else {
/* compile the module */
func_val = JS_Eval(ctx, (char *)buf, buf_len, module_name,
JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);
if (JS_IsException(func_val))
return NULL;
if (js_module_set_import_meta(ctx, func_val, true, false) < 0) {
JS_FreeValue(ctx, func_val);
return NULL;
}
/* the module is already referenced, so we must free it */
m = JS_VALUE_GET_PTR(func_val);
JS_FreeValue(ctx, func_val);
return NULL;
}
/* the module is already referenced, so we must free it */
m = JS_VALUE_GET_PTR(func_val);
JS_FreeValue(ctx, func_val);
js_free(ctx, buf);
}
return m;
}
Expand Down
135 changes: 129 additions & 6 deletions quickjs.c
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,10 @@ struct JSContext {

struct list_head loaded_modules; /* list of JSModuleDef.link */

/* To minimize creating breaking changes, I have instead decided
to carry the parsed import_assertion instead */
JSValue import_assertion;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mark/free this in JS_MarkContext & JS_FreeContext.


/* if NULL, RegExp compilation is not supported */
JSValue (*compile_regexp)(JSContext *ctx, JSValueConst pattern,
JSValueConst flags);
Expand Down Expand Up @@ -855,6 +859,9 @@ struct JSModuleDef {
bool eval_has_exception;
JSValue eval_exception;
JSValue meta_obj; /* for import.meta */

/* a list of key/value strings - [key, value, key, value] */
JSValue import_assertion;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mark/free this in js_mark_module_def & js_free_module_def.

};

typedef struct JSJobEntry {
Expand Down Expand Up @@ -1247,7 +1254,7 @@ static void js_free_module_def(JSContext *ctx, JSModuleDef *m);
static void js_mark_module_def(JSRuntime *rt, JSModuleDef *m,
JS_MarkFunc *mark_func);
static JSValue js_import_meta(JSContext *ctx);
static JSValue js_dynamic_import(JSContext *ctx, JSValueConst specifier);
static JSValue js_dynamic_import(JSContext *ctx, JSValueConst specifier, JSValueConst import_assertion);
static void free_var_ref(JSRuntime *rt, JSVarRef *var_ref);
static JSValue js_new_promise_capability(JSContext *ctx,
JSValue *resolving_funcs,
Expand Down Expand Up @@ -2307,6 +2314,7 @@ JSContext *JS_NewContextRaw(JSRuntime *rt)
ctx->error_back_trace = JS_UNDEFINED;
ctx->error_prepare_stack = JS_UNDEFINED;
ctx->error_stack_trace_limit = js_int32(10);
ctx->import_assertion = JS_UNDEFINED;
init_list_head(&ctx->loaded_modules);

JS_AddIntrinsicBasicObjects(ctx);
Expand Down Expand Up @@ -17119,10 +17127,11 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj,
{
JSValue val;
sf->cur_pc = pc;
val = js_dynamic_import(ctx, sp[-1]);
val = js_dynamic_import(ctx, sp[-2], sp[-1]);
if (JS_IsException(val))
goto exception;
JS_FreeValue(ctx, sp[-1]);
JS_FreeValue(ctx, sp[-2]);
sp[-1] = val;
}
BREAK;
Expand Down Expand Up @@ -25145,6 +25154,14 @@ static __exception int js_parse_postfix_expr(JSParseState *s, int parse_flags)
return js_parse_error(s, "invalid use of 'import()'");
if (js_parse_assign_expr(s))
return -1;
if (s->token.val == ',') {
if (next_token(s))
return -1;
if (js_parse_object_literal(s))
return -1;
} else
emit_op(s, OP_undefined);

if (js_parse_expect(s, ')'))
return -1;
emit_op(s, OP_import);
Expand Down Expand Up @@ -27684,6 +27701,7 @@ static JSModuleDef *js_new_module_def(JSContext *ctx, JSAtom name)
m->eval_exception = JS_UNDEFINED;
m->meta_obj = JS_UNDEFINED;
m->promise = JS_UNDEFINED;
m->import_assertion = JS_NewArray(ctx);
m->resolving_funcs[0] = JS_UNDEFINED;
m->resolving_funcs[1] = JS_UNDEFINED;
list_add_tail(&m->link, &ctx->loaded_modules);
Expand All @@ -27707,6 +27725,7 @@ static void js_mark_module_def(JSRuntime *rt, JSModuleDef *m,
JS_MarkValue(rt, m->func_obj, mark_func);
JS_MarkValue(rt, m->eval_exception, mark_func);
JS_MarkValue(rt, m->meta_obj, mark_func);
JS_MarkValue(rt, m->import_assertion, mark_func);
JS_MarkValue(rt, m->promise, mark_func);
JS_MarkValue(rt, m->resolving_funcs[0], mark_func);
JS_MarkValue(rt, m->resolving_funcs[1], mark_func);
Expand Down Expand Up @@ -28479,6 +28498,11 @@ JSValue JS_GetModuleNamespace(JSContext *ctx, JSModuleDef *m)
return js_dup(m->module_ns);
}

JSValue JS_GetImportAssertion(JSContext *ctx)
{
return ctx->import_assertion;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return ctx->import_assertion;
return js_dup(ctx->import_assertion);

Or change the return type to JSValueConst but that's much less idiomatic (even internally we only have three functions that do that.)

}

#ifdef ENABLE_DUMPS // JS_DUMP_MODULE_RESOLVE
#define module_trace(ctx, ...) \
do { \
Expand All @@ -28504,6 +28528,7 @@ static int js_resolve_module(JSContext *ctx, JSModuleDef *m)
}
#endif
m->resolved = true;
ctx->import_assertion = m->import_assertion;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ctx->import_assertion = m->import_assertion;
ctx->import_assertion = js_dup(m->import_assertion);

/* resolve each requested module */
for(i = 0; i < m->req_module_entries_count; i++) {
JSReqModuleEntry *rme = &m->req_module_entries[i];
Expand All @@ -28517,6 +28542,7 @@ static int js_resolve_module(JSContext *ctx, JSModuleDef *m)
if (js_resolve_module(ctx, m1) < 0)
return -1;
}
ctx->import_assertion = JS_UNDEFINED;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ctx->import_assertion = JS_UNDEFINED;
JS_FreeValue(ctx, ctx->import_assertion);
ctx->import_assertion = JS_UNDEFINED;

return 0;
}

Expand Down Expand Up @@ -29042,6 +29068,7 @@ static JSValue js_dynamic_import_job(JSContext *ctx,
JSValueConst *resolving_funcs = argv;
JSValueConst basename_val = argv[2];
JSValueConst specifier = argv[3];
ctx->import_assertion = argv[4];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it should either js_dup or the JS_FreeValue further down shouldn't be there. argv is JSValueConst, meaning the caller doesn't transfer ownership.

const char *basename = NULL, *filename;
JSValue ret, err;

Expand All @@ -29068,14 +29095,15 @@ static JSValue js_dynamic_import_job(JSContext *ctx,
JS_FreeValue(ctx, ret); /* XXX: what to do if exception ? */
JS_FreeValue(ctx, err);
JS_FreeCString(ctx, basename);
JS_FreeValue(ctx, argv[4]);
return JS_UNDEFINED;
}

static JSValue js_dynamic_import(JSContext *ctx, JSValueConst specifier)
static JSValue js_dynamic_import(JSContext *ctx, JSValueConst specifier, JSValueConst import_assertion)
{
JSAtom basename;
JSValue promise, resolving_funcs[2], basename_val;
JSValue args[4];
JSValue promise, resolving_funcs[2], basename_val, assertion;
JSValue args[5];

basename = JS_GetScriptOrModuleName(ctx, 0);
if (basename == JS_ATOM_NULL)
Expand All @@ -29092,14 +29120,46 @@ static JSValue js_dynamic_import(JSContext *ctx, JSValueConst specifier)
return promise;
}

assertion = JS_NewArray(ctx);

if (JS_IsObject(import_assertion)) {
if (!JS_HasProperty(ctx, import_assertion, JS_NewAtom(ctx, "with")))
return JS_ThrowTypeError(ctx, "expected 'with' property");

JSValue obj = JS_GetPropertyStr(ctx, import_assertion, "with");
if (!JS_IsObject(obj) || JS_IsArray(obj))
return JS_ThrowTypeError(ctx, "expected object");

JSPropertyEnum *props;
uint32_t index, len;
index = 0;

JS_GetOwnPropertyNames(ctx, &props, &len, obj, JS_GPN_STRING_MASK);

for (uint32_t i = 0; i < len; i++) {
JSValue key = JS_AtomToString(ctx, props[i].atom);
JSValue val = JS_GetProperty(ctx, obj, props[i].atom);

JS_DefinePropertyValueUint32(ctx, assertion, index,
key, JS_PROP_HAS_ENUMERABLE);
index++;
JS_DefinePropertyValueUint32(ctx, assertion, index,
val, JS_PROP_HAS_ENUMERABLE);
index++;
}

JS_FreePropertyEnum(ctx, props, len);
}

args[0] = resolving_funcs[0];
args[1] = resolving_funcs[1];
args[2] = basename_val;
args[3] = unsafe_unconst(specifier);
args[4] = assertion;

/* cannot run JS_LoadModuleInternal synchronously because it would
cause an unexpected recursion in js_evaluate_module() */
JS_EnqueueJob(ctx, js_dynamic_import_job, 4, vc(args));
JS_EnqueueJob(ctx, js_dynamic_import_job, 5, vc(args));

JS_FreeValue(ctx, basename_val);
JS_FreeValue(ctx, resolving_funcs[0]);
Expand Down Expand Up @@ -29732,6 +29792,65 @@ static int add_import(JSParseState *s, JSModuleDef *m,
return 0;
}

static __exception int js_parse_import_assertion(JSParseState *s, JSModuleDef *m)
{
uint32_t index = 0;

if (next_token(s))
return -1;

if (js_parse_expect(s, '{'))
return -1;

while (s->token.val != '}') {
JSValue key, value;

if (!token_is_ident(s->token.val) && s->token.val != TOK_STRING) {
js_parse_error(s, "identifier or string expected");
return -1;
}

if (token_is_ident(s->token.val))
key = JS_AtomToValue(s->ctx, s->token.u.ident.atom);
else
key = js_dup(s->token.u.str.str);

if (next_token(s))
goto fail;

if (js_parse_expect(s, ':'))
goto fail;

if (s->token.val != TOK_STRING) {
js_parse_error(s, "string expected");
goto fail;
}

value = js_dup(s->token.u.str.str);

JS_DefinePropertyValueUint32(s->ctx, m->import_assertion, index,
key, JS_PROP_HAS_ENUMERABLE);
index++;
JS_DefinePropertyValueUint32(s->ctx, m->import_assertion, index,
value, JS_PROP_HAS_ENUMERABLE);
index++;

if (next_token(s))
goto fail;

continue;
fail:
JS_FreeValue(s->ctx, key);
JS_FreeValue(s->ctx, value);
return -1;
}

if (next_token(s))
return -1;

return 0;
}

static __exception int js_parse_import(JSParseState *s)
{
JSContext *ctx = s->ctx;
Expand Down Expand Up @@ -29836,6 +29955,10 @@ static __exception int js_parse_import(JSParseState *s)
module_name = js_parse_from_clause(s);
if (module_name == JS_ATOM_NULL)
return -1;

if (s->token.val == TOK_WITH)
if (js_parse_import_assertion(s, m) < 0)
return -1;
}
idx = add_req_module_entry(ctx, m, module_name);
JS_FreeAtom(ctx, module_name);
Expand Down
1 change: 1 addition & 0 deletions quickjs.h
Original file line number Diff line number Diff line change
Expand Up @@ -1053,6 +1053,7 @@ JS_EXTERN void JS_SetModuleLoaderFunc(JSRuntime *rt,
JS_EXTERN JSValue JS_GetImportMeta(JSContext *ctx, JSModuleDef *m);
JS_EXTERN JSAtom JS_GetModuleName(JSContext *ctx, JSModuleDef *m);
JS_EXTERN JSValue JS_GetModuleNamespace(JSContext *ctx, JSModuleDef *m);
JS_EXTERN JSValue JS_GetImportAssertion(JSContext *ctx);

/* JS Job support */

Expand Down