diff --git a/quickjs-libc.c b/quickjs-libc.c index 6082b7465..37ccca77a 100644 --- a/quickjs-libc.c +++ b/quickjs-libc.c @@ -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) { @@ -788,19 +817,70 @@ 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) { + JSValueConst with_clause = JS_GetImportAssertion(ctx); + bool is_json = false; + 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_FreeValue(ctx, key); + JS_FreeCString(ctx, str); + JS_ThrowTypeError(ctx, "'type' is not 'json'"); + return NULL; + } + JS_FreeValue(ctx, key); + JS_FreeCString(ctx, str); + is_json = true; + break; + } + } + } + + if (is_json) { + 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; } diff --git a/quickjs-opcode.h b/quickjs-opcode.h index bd5be754e..8ea427a46 100644 --- a/quickjs-opcode.h +++ b/quickjs-opcode.h @@ -122,7 +122,7 @@ DEF( apply_eval, 3, 2, 1, u16) /* func array -> ret_eval */ DEF( regexp, 1, 2, 1, none) /* create a RegExp object from the pattern and a bytecode string */ DEF( get_super, 1, 1, 1, none) -DEF( import, 1, 1, 1, none) /* dynamic module import */ +DEF( import, 1, 2, 1, none) /* dynamic module import */ DEF( check_var, 5, 0, 1, atom) /* check if a variable exists */ DEF( get_var_undef, 5, 0, 1, atom) /* push undefined if the variable does not exist */ diff --git a/quickjs.c b/quickjs.c index b6277b562..46cd6df05 100644 --- a/quickjs.c +++ b/quickjs.c @@ -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; + /* if NULL, RegExp compilation is not supported */ JSValue (*compile_regexp)(JSContext *ctx, JSValueConst pattern, JSValueConst flags); @@ -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; }; typedef struct JSJobEntry { @@ -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, @@ -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); @@ -2437,6 +2445,7 @@ static void JS_MarkContext(JSRuntime *rt, JSContext *ctx, JS_MarkValue(rt, ctx->regexp_ctor, mark_func); JS_MarkValue(rt, ctx->function_ctor, mark_func); JS_MarkValue(rt, ctx->function_proto, mark_func); + JS_MarkValue(rt, ctx->import_assertion, mark_func); if (ctx->array_shape) mark_func(rt, &ctx->array_shape->header); @@ -2507,6 +2516,7 @@ void JS_FreeContext(JSContext *ctx) JS_FreeValue(ctx, ctx->regexp_ctor); JS_FreeValue(ctx, ctx->function_ctor); JS_FreeValue(ctx, ctx->function_proto); + JS_FreeValue(ctx, ctx->import_assertion); js_free_shape_null(ctx->rt, ctx->array_shape); @@ -17124,10 +17134,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; @@ -25150,6 +25161,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); @@ -27689,6 +27708,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_UNDEFINED; m->resolving_funcs[0] = JS_UNDEFINED; m->resolving_funcs[1] = JS_UNDEFINED; list_add_tail(&m->link, &ctx->loaded_modules); @@ -27712,9 +27732,11 @@ 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); + JS_MarkValue(rt, m->import_assertion, mark_func); } static void js_free_module_def(JSContext *ctx, JSModuleDef *m) @@ -27754,6 +27776,7 @@ static void js_free_module_def(JSContext *ctx, JSModuleDef *m) JS_FreeValue(ctx, m->promise); JS_FreeValue(ctx, m->resolving_funcs[0]); JS_FreeValue(ctx, m->resolving_funcs[1]); + JS_FreeValue(ctx, m->import_assertion); list_del(&m->link); js_free(ctx, m); } @@ -28484,6 +28507,11 @@ JSValue JS_GetModuleNamespace(JSContext *ctx, JSModuleDef *m) return js_dup(m->module_ns); } +JSValueConst JS_GetImportAssertion(JSContext *ctx) +{ + return ctx->import_assertion; +} + #ifdef ENABLE_DUMPS // JS_DUMP_MODULE_RESOLVE #define module_trace(ctx, ...) \ do { \ @@ -28509,6 +28537,8 @@ static int js_resolve_module(JSContext *ctx, JSModuleDef *m) } #endif m->resolved = true; + + 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]; @@ -28522,6 +28552,8 @@ static int js_resolve_module(JSContext *ctx, JSModuleDef *m) if (js_resolve_module(ctx, m1) < 0) return -1; } + JS_FreeValue(ctx, ctx->import_assertion); + ctx->import_assertion = JS_UNDEFINED; return 0; } @@ -29047,6 +29079,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 = unsafe_unconst(argv[4]); const char *basename = NULL, *filename; JSValue ret, err; @@ -29076,11 +29109,12 @@ static JSValue js_dynamic_import_job(JSContext *ctx, 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]; + assertion = JS_UNDEFINED; basename = JS_GetScriptOrModuleName(ctx, 0); if (basename == JS_ATOM_NULL) @@ -29097,14 +29131,51 @@ static JSValue js_dynamic_import(JSContext *ctx, JSValueConst specifier) return promise; } + if (JS_IsObject(import_assertion)) { + assertion = JS_NewArray(ctx); + if (!JS_HasProperty(ctx, import_assertion, JS_NewAtom(ctx, "with"))) { + JS_FreeValue(ctx, promise); + JS_FreeValue(ctx, resolving_funcs[0]); + JS_FreeValue(ctx, resolving_funcs[1]); + JS_FreeValue(ctx, assertion); + 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); + JS_FreeValue(ctx, obj); + } + 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]); @@ -29737,6 +29808,68 @@ 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; + m->import_assertion = JS_NewArray(s->ctx); + + if (next_token(s)) + return -1; + + if (js_parse_expect(s, '{')) + return -1; + + while (s->token.val != '}') { + JSValue key, value; + key = value = JS_UNDEFINED; + + 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, m->import_assertion); + 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; @@ -29841,6 +29974,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); diff --git a/quickjs.h b/quickjs.h index 59c4956fd..7a11cbb2f 100644 --- a/quickjs.h +++ b/quickjs.h @@ -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 JSValueConst JS_GetImportAssertion(JSContext *ctx); /* JS Job support */