diff --git a/examples/demo/remote.js b/examples/demo/remote.js new file mode 100644 index 0000000..cdefbe9 --- /dev/null +++ b/examples/demo/remote.js @@ -0,0 +1,4 @@ +import { hello } from 'https://r.justjs.dev/hello@0.0.1'; + +console.log(`hello is ${typeof hello}`); +void hello(); diff --git a/include/esm/remote.h b/include/esm/remote.h new file mode 100644 index 0000000..50a82e7 --- /dev/null +++ b/include/esm/remote.h @@ -0,0 +1,11 @@ +#ifndef ESM_REMOTE_H +#define ESM_REMOTE_H + +#include +#include + +bool esm_is_url(const char *spec); +char *esm_fetch_url(const char *url, size_t *out_len, char **out_error); +char *esm_resolve_url(const char *specifier, const char *base_url); + +#endif diff --git a/meson.build b/meson.build index 60b5334..264af47 100644 --- a/meson.build +++ b/meson.build @@ -22,6 +22,7 @@ sources = files( 'src/repl.c', 'src/runtime.c', 'src/snapshot.c', + 'src/esm/remote.c', ) + files(module_files) tls_lib = get_option('tls_library') @@ -162,7 +163,7 @@ else timestamp = timestamp_opt endif -version_conf.set('ANT_VERSION', '0.3.5.' + timestamp + '-g' + git_hash) +version_conf.set('ANT_VERSION', '0.3.6.' + timestamp + '-g' + git_hash) version_conf.set('ANT_BUILD_TIMESTAMP', timestamp) cmd_cc = meson.get_compiler('c') diff --git a/src/ant.c b/src/ant.c index f21dc45..eec6bb2 100644 --- a/src/ant.c +++ b/src/ant.c @@ -11,6 +11,7 @@ #include "runtime.h" #include "internal.h" +#include #include #include #include @@ -20,16 +21,18 @@ #include #include #include +#include +#include +#include +#include +#include + #ifdef _WIN32 #include #else #include #include #endif -#include -#include -#include -#include #define MCO_USE_VMEM_ALLOCATOR #define MCO_ZERO_MEMORY @@ -45,6 +48,7 @@ #include "modules/child_process.h" #include "modules/readline.h" #include "modules/json.h" +#include "esm/remote.h" #define CORO_MALLOC(size) calloc(1, size) #define CORO_FREE(ptr) free(ptr) @@ -180,6 +184,9 @@ typedef struct esm_module { bool is_json; bool is_text; bool is_image; + bool is_url; + char *url_content; + size_t url_content_len; struct esm_module *next; UT_hash_handle hh; } esm_module_t; @@ -18994,7 +19001,8 @@ static esm_module_t *esm_find_module(const char *resolved_path) { } static esm_module_t *esm_create_module(const char *path, const char *resolved_path) { - char *canonical_path = esm_canonicalize_path(resolved_path); + bool is_url = esm_is_url(resolved_path); + char *canonical_path = is_url ? strdup(resolved_path) : esm_canonicalize_path(resolved_path); if (!canonical_path) return NULL; esm_module_t *existing_mod = NULL; @@ -19019,6 +19027,9 @@ static esm_module_t *esm_create_module(const char *path, const char *resolved_pa mod->is_json = esm_is_json(resolved_path); mod->is_text = esm_is_text(resolved_path); mod->is_image = esm_is_image(resolved_path); + mod->is_url = is_url; + mod->url_content = NULL; + mod->url_content_len = 0; mod->next = NULL; HASH_ADD_STR(global_module_cache.modules, resolved_path, mod); @@ -19033,6 +19044,7 @@ static void esm_cleanup_module_cache(void) { HASH_DEL(global_module_cache.modules, current); if (current->path) free(current->path); if (current->resolved_path) free(current->resolved_path); + if (current->url_content) free(current->url_content); free(current); } global_module_cache.count = 0; @@ -19169,25 +19181,47 @@ static jsval_t esm_load_module(struct js *js, esm_module_t *mod) { return img_val; } - FILE *fp = fopen(mod->resolved_path, "rb"); - if (!fp) { - mod->is_loading = false; - return js_mkerr(js, "Cannot open module: %s", mod->resolved_path); - } + char *content = NULL; + size_t size = 0; - fseek(fp, 0, SEEK_END); - long size = ftell(fp); - fseek(fp, 0, SEEK_SET); - - char *content = (char *)malloc(size + 1); - if (!content) { + if (mod->is_url) { + if (mod->url_content) { + content = strdup(mod->url_content); + size = mod->url_content_len; + } else { + char *error = NULL; + content = esm_fetch_url(mod->resolved_path, &size, &error); + if (!content) { + mod->is_loading = false; + jsval_t err = js_mkerr(js, "Cannot fetch module %s: %s", mod->resolved_path, error ? error : "unknown error"); + if (error) free(error); + return err; + } + mod->url_content = strdup(content); + mod->url_content_len = size; + } + } else { + FILE *fp = fopen(mod->resolved_path, "rb"); + if (!fp) { + mod->is_loading = false; + return js_mkerr(js, "Cannot open module: %s", mod->resolved_path); + } + + fseek(fp, 0, SEEK_END); + long fsize = ftell(fp); + fseek(fp, 0, SEEK_SET); + size = (size_t)fsize; + + content = (char *)malloc(size + 1); + if (!content) { + fclose(fp); + mod->is_loading = false; + return js_mkerr(js, "OOM loading module"); + } + + fread(content, 1, size, fp); fclose(fp); - mod->is_loading = false; - return js_mkerr(js, "OOM loading module"); } - - fread(content, 1, size, fp); - fclose(fp); content[size] = '\0'; char *js_code = content; @@ -19243,10 +19277,11 @@ static jsval_t builtin_import(struct js *js, jsval_t *args, int nargs) { jsoff_t spec_len; jsoff_t spec_off = vstr(js, args[0], &spec_len); - const char *specifier = (char *)&js->mem[spec_off]; + char *specifier = strndup((char *)&js->mem[spec_off], spec_len); ant_library_t *lib = find_library(specifier, spec_len); if (lib) { + free(specifier); jsval_t lib_obj = lib->init_fn(js); if (is_err(lib_obj)) return builtin_Promise_reject(js, &lib_obj, 1); jsval_t promise_args[] = { lib_obj }; @@ -19254,9 +19289,16 @@ static jsval_t builtin_import(struct js *js, jsval_t *args, int nargs) { } const char *base_path = js->filename ? js->filename : "."; - char *resolved_path = esm_resolve_path(specifier, base_path); + char *resolved_path = NULL; + + if (esm_is_url(specifier) || esm_is_url(base_path)) { + resolved_path = esm_resolve_url(specifier, base_path); + } else resolved_path = esm_resolve_path(specifier, base_path); + if (!resolved_path) { - return js_mkerr(js, "Cannot resolve module: %.*s", (int)spec_len, specifier); + jsval_t err = js_mkerr(js, "Cannot resolve module: %s", specifier); + free(specifier); + return err; } esm_module_t *mod = esm_find_module(resolved_path); @@ -19264,12 +19306,13 @@ static jsval_t builtin_import(struct js *js, jsval_t *args, int nargs) { mod = esm_create_module(specifier, resolved_path); if (!mod) { free(resolved_path); - return js_mkerr(js, "Cannot create module"); + free(specifier); return js_mkerr(js, "Cannot create module"); } } jsval_t ns = esm_load_module(js, mod); free(resolved_path); + free(specifier); if (is_err(ns)) return builtin_Promise_reject(js, &ns, 1); @@ -19284,12 +19327,24 @@ static jsval_t builtin_import_meta_resolve(struct js *js, jsval_t *args, int nar jsoff_t spec_len; jsoff_t spec_off = vstr(js, args[0], &spec_len); - const char *specifier = (char *)&js->mem[spec_off]; + char *specifier = strndup((char *)&js->mem[spec_off], spec_len); const char *base_path = js->filename ? js->filename : "."; - char *resolved_path = esm_resolve_path(specifier, base_path); + char *resolved_path = NULL; + + if (esm_is_url(specifier) || esm_is_url(base_path)) { + resolved_path = esm_resolve_url(specifier, base_path); + } else resolved_path = esm_resolve_path(specifier, base_path); + if (!resolved_path) { - return js_mkerr(js, "Cannot resolve module: %.*s", (int)spec_len, specifier); + jsval_t err = js_mkerr(js, "Cannot resolve module: %s", specifier); + free(specifier); return err; + } + free(specifier); + + if (esm_is_url(resolved_path)) { + jsval_t result = js_mkstr(js, resolved_path, strlen(resolved_path)); + free(resolved_path); return result; } size_t url_len = strlen(resolved_path) + 8; @@ -19314,26 +19369,46 @@ void js_setup_import_meta(struct js *js, const char *filename) { jsval_t import_meta = mkobj(js, 0); if (is_err(import_meta)) return; - size_t url_len = strlen(filename) + 8; - char *url = malloc(url_len); - if (url) { - snprintf(url, url_len, "file://%s", filename); - jsval_t url_val = js_mkstr(js, url, strlen(url)); + bool is_url = esm_is_url(filename); + + if (is_url) { + jsval_t url_val = js_mkstr(js, filename, strlen(filename)); if (!is_err(url_val)) setprop(js, import_meta, js_mkstr(js, "url", 3), url_val); - free(url); + } else { + size_t url_len = strlen(filename) + 8; + char *url = malloc(url_len); + if (url) { + snprintf(url, url_len, "file://%s", filename); + jsval_t url_val = js_mkstr(js, url, strlen(url)); + if (!is_err(url_val)) setprop(js, import_meta, js_mkstr(js, "url", 3), url_val); free(url); + } } jsval_t filename_val = js_mkstr(js, filename, strlen(filename)); if (!is_err(filename_val)) setprop(js, import_meta, js_mkstr(js, "filename", 8), filename_val); - char *filename_copy = strdup(filename); - if (filename_copy) { - char *dir = dirname(filename_copy); - if (dir) { - jsval_t dirname_val = js_mkstr(js, dir, strlen(dir)); - if (!is_err(dirname_val)) setprop(js, import_meta, js_mkstr(js, "dirname", 7), dirname_val); + if (is_url) { + char *filename_copy = strdup(filename); + if (filename_copy) { + char *last_slash = strrchr(filename_copy, '/'); + char *scheme_end = strstr(filename_copy, "://"); + if (last_slash && scheme_end && last_slash > scheme_end + 2) { + *last_slash = '\0'; + jsval_t dirname_val = js_mkstr(js, filename_copy, strlen(filename_copy)); + if (!is_err(dirname_val)) setprop(js, import_meta, js_mkstr(js, "dirname", 7), dirname_val); + } + free(filename_copy); + } + } else { + char *filename_copy = strdup(filename); + if (filename_copy) { + char *dir = dirname(filename_copy); + if (dir) { + jsval_t dirname_val = js_mkstr(js, dir, strlen(dir)); + if (!is_err(dirname_val)) setprop(js, import_meta, js_mkstr(js, "dirname", 7), dirname_val); + } + free(filename_copy); } - free(filename_copy); } setprop(js, import_meta, js_mkstr(js, "main", 4), js_mktrue()); @@ -19384,25 +19459,30 @@ static jsval_t js_import_stmt(struct js *js) { jsoff_t spec_len; jsoff_t spec_off = vstr(js, spec, &spec_len); - const char *specifier = (char *)&js->mem[spec_off]; - + char *spec_str = strndup((char *)&js->mem[spec_off], spec_len); jsval_t ns = js_mkundef(); - ant_library_t *lib = find_library(specifier, spec_len); + ant_library_t *lib = find_library(spec_str, spec_len); if (lib) { ns = lib->init_fn(js); } else { const char *base_path = js->filename ? js->filename : "."; - char *resolved_path = esm_resolve_path(specifier, base_path); + char *resolved_path = NULL; + + if (esm_is_url(spec_str) || esm_is_url(base_path)) { + resolved_path = esm_resolve_url(spec_str, base_path); + } else resolved_path = esm_resolve_path(spec_str, base_path); + if (!resolved_path) { - return js_mkerr(js, "Cannot resolve module: %.*s", (int)spec_len, specifier); + free(spec_str); + return js_mkerr(js, "Cannot resolve module: %.*s", (int)spec_len, spec_str); } esm_module_t *mod = esm_find_module(resolved_path); if (!mod) { - mod = esm_create_module(specifier, resolved_path); + mod = esm_create_module(spec_str, resolved_path); if (!mod) { free(resolved_path); - return js_mkerr(js, "Cannot create module"); + free(spec_str); return js_mkerr(js, "Cannot create module"); } } @@ -19410,11 +19490,11 @@ static jsval_t js_import_stmt(struct js *js) { JS_SAVE_STATE(js, saved); ns = esm_load_module(js, mod); - JS_RESTORE_STATE(js, saved); free(resolved_path); } + free(spec_str); js->consumed = 1; next(js); js->consumed = 0; if (is_err(ns)) return ns; @@ -19493,25 +19573,31 @@ static jsval_t js_import_stmt(struct js *js) { jsoff_t spec_len; jsoff_t spec_off = vstr(js, spec, &spec_len); - const char *specifier = (char *)&js->mem[spec_off]; + char *spec_str = strndup((char *)&js->mem[spec_off], spec_len); jsval_t ns = js_mkundef(); - ant_library_t *lib = find_library(specifier, spec_len); + ant_library_t *lib = find_library(spec_str, spec_len); if (lib) { ns = lib->init_fn(js); } else { const char *base_path = js->filename ? js->filename : "."; - char *resolved_path = esm_resolve_path(specifier, base_path); + char *resolved_path = NULL; + + if (esm_is_url(spec_str) || esm_is_url(base_path)) { + resolved_path = esm_resolve_url(spec_str, base_path); + } else resolved_path = esm_resolve_path(spec_str, base_path); + if (!resolved_path) { - return js_mkerr(js, "Cannot resolve module: %.*s", (int)spec_len, specifier); + free(spec_str); + return js_mkerr(js, "Cannot resolve module: %s", spec_str); } esm_module_t *mod = esm_find_module(resolved_path); if (!mod) { - mod = esm_create_module(specifier, resolved_path); + mod = esm_create_module(spec_str, resolved_path); if (!mod) { free(resolved_path); - return js_mkerr(js, "Cannot create module"); + free(spec_str); return js_mkerr(js, "Cannot create module"); } } @@ -19519,11 +19605,11 @@ static jsval_t js_import_stmt(struct js *js) { JS_SAVE_STATE(js, saved); ns = esm_load_module(js, mod); - JS_RESTORE_STATE(js, saved); free(resolved_path); } + free(spec_str); js->consumed = 1; next(js); js->consumed = 0; if (is_err(ns)) return ns; @@ -19599,23 +19685,31 @@ static jsval_t js_import_stmt(struct js *js) { jsoff_t spec_len; jsoff_t spec_off = vstr(js, spec, &spec_len); - const char *specifier = (char *)&js->mem[spec_off]; + char *spec_str = strndup((char *)&js->mem[spec_off], spec_len); jsval_t ns = js_mkundef(); - ant_library_t *lib = find_library(specifier, spec_len); + ant_library_t *lib = find_library(spec_str, spec_len); if (lib) { ns = lib->init_fn(js); } else { const char *base_path = js->filename ? js->filename : "."; - char *resolved_path = esm_resolve_path(specifier, base_path); - if (!resolved_path) return js_mkerr(js, "Cannot resolve module: %.*s", (int)spec_len, specifier); + char *resolved_path = NULL; + + if (esm_is_url(spec_str) || esm_is_url(base_path)) { + resolved_path = esm_resolve_url(spec_str, base_path); + } else resolved_path = esm_resolve_path(spec_str, base_path); + + if (!resolved_path) { + free(spec_str); + return js_mkerr(js, "Cannot resolve module: %s", spec_str); + } esm_module_t *mod = esm_find_module(resolved_path); if (!mod) { - mod = esm_create_module(specifier, resolved_path); + mod = esm_create_module(spec_str, resolved_path); if (!mod) { free(resolved_path); - return js_mkerr(js, "Cannot create module"); + free(spec_str); return js_mkerr(js, "Cannot create module"); } } @@ -19633,6 +19727,7 @@ static jsval_t js_import_stmt(struct js *js) { free(resolved_path); } + free(spec_str); js->consumed = 1; next(js); @@ -19730,6 +19825,85 @@ static jsval_t js_export_stmt(struct js *js) { return cls; } + if (next(js) == TOK_MUL) { + js->consumed = 1; + + const char *alias_name = NULL; + size_t alias_len = 0; + + if (next(js) == TOK_AS) { + js->consumed = 1; + EXPECT(TOK_IDENTIFIER, ); + alias_name = &js->code[js->toff]; + alias_len = js->tlen; + js->consumed = 1; + } + + EXPECT(TOK_FROM, ); + EXPECT(TOK_STRING, ); + + jsval_t spec = js_str_literal(js); + jsoff_t spec_len; + jsoff_t spec_off = vstr(js, spec, &spec_len); + char *spec_str = strndup((char *)&js->mem[spec_off], spec_len); + + jsval_t ns = js_mkundef(); + ant_library_t *lib = find_library(spec_str, spec_len); + if (lib) { + ns = lib->init_fn(js); + } else { + const char *base_path = js->filename ? js->filename : "."; + char *resolved_path = NULL; + + if (esm_is_url(spec_str) || esm_is_url(base_path)) { + resolved_path = esm_resolve_url(spec_str, base_path); + } else resolved_path = esm_resolve_path(spec_str, base_path); + + if (!resolved_path) { + free(spec_str); + return js_mkerr(js, "Cannot resolve module: %s", spec_str); + } + + esm_module_t *mod = esm_find_module(resolved_path); + if (!mod) { + mod = esm_create_module(spec_str, resolved_path); + if (!mod) { + free(resolved_path); + free(spec_str); + return js_mkerr(js, "Cannot create module"); + } + } + + js_parse_state_t saved; + JS_SAVE_STATE(js, saved); + + ns = esm_load_module(js, mod); + JS_RESTORE_STATE(js, saved); + + free(resolved_path); + } + free(spec_str); + + js->consumed = 1; next(js); js->consumed = 0; + if (is_err(ns)) return ns; + + if (alias_name) { + setprop(js, js->module_ns, js_mkstr(js, alias_name, alias_len), ns); + } else if (vtype(ns) == T_OBJ) { + js_prop_iter_t iter = js_prop_iter_begin(js, ns); + const char *key; + size_t key_len; + jsval_t value; + + while (js_prop_iter_next(&iter, &key, &key_len, &value)) { + setprop(js, js->module_ns, js_mkstr(js, key, key_len), resolveprop(js, value)); + } + js_prop_iter_end(&iter); + } + + return js_mkundef(); + } + if (next(js) == TOK_LBRACE) { js->consumed = 1; diff --git a/src/esm/remote.c b/src/esm/remote.c new file mode 100644 index 0000000..690626c --- /dev/null +++ b/src/esm/remote.c @@ -0,0 +1,372 @@ +#include "utils.h" +#include "esm/remote.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#define mkdir_p(path) _mkdir(path) +#define PATH_SEP '\\' +#else +#include +#include +#define mkdir_p(path) mkdir(path, 0755) +#define PATH_SEP '/' +#endif + +typedef struct { + char *data; + size_t size; + size_t capacity; + int status_code; + int completed; + int failed; + char *error_msg; + tlsuv_http_t http_client; + tlsuv_http_req_t *http_req; +} esm_url_fetch_t; + +bool esm_is_url(const char *spec) { + return (strncmp(spec, "http://", 7) == 0 || strncmp(spec, "https://", 8) == 0); +} + +static int is_path_sep(char c) { + return c == '/' || c == '\\'; +} + +static void esm_url_fetch_close_cb(tlsuv_http_t *client) { + esm_url_fetch_t *ctx = (esm_url_fetch_t *)client->data; + ctx->completed = 1; +} + +static const char *esm_get_home_dir(void) { +#ifdef _WIN32 + const char *home = getenv("USERPROFILE"); + if (home) return home; + + const char *drive = getenv("HOMEDRIVE"); + const char *path = getenv("HOMEPATH"); + if (drive && path) { + static char win_home[MAX_PATH]; + snprintf(win_home, sizeof(win_home), "%s%s", drive, path); + return win_home; + } +#else + const char *home = getenv("HOME"); + if (home) return home; +#endif + return NULL; +} + +static void esm_url_fetch_body_cb(tlsuv_http_req_t *http_req, char *body, ssize_t len) { + esm_url_fetch_t *ctx = (esm_url_fetch_t *)http_req->data; + + if (len == UV_EOF) { + tlsuv_http_close(&ctx->http_client, esm_url_fetch_close_cb); + return; + } + + if (len < 0) { + ctx->failed = 1; + ctx->error_msg = strdup(uv_strerror((int)len)); + tlsuv_http_close(&ctx->http_client, esm_url_fetch_close_cb); + return; + } + + if (ctx->size + (size_t)len > ctx->capacity) { + size_t new_cap = ctx->capacity * 2; + while (new_cap < ctx->size + (size_t)len) new_cap *= 2; + char *new_data = realloc(ctx->data, new_cap); + if (!new_data) { + ctx->failed = 1; + ctx->error_msg = strdup("Out of memory"); + tlsuv_http_close(&ctx->http_client, esm_url_fetch_close_cb); + return; + } + ctx->data = new_data; + ctx->capacity = new_cap; + } + + memcpy(ctx->data + ctx->size, body, (size_t)len); + ctx->size += (size_t)len; +} + +static void esm_url_fetch_resp_cb(tlsuv_http_resp_t *resp, void *data) { + (void)data; + esm_url_fetch_t *ctx = (esm_url_fetch_t *)resp->req->data; + + if (resp->code < 0) { + ctx->failed = 1; + char err_buf[256]; + snprintf(err_buf, sizeof(err_buf), "%s (code: %d)", uv_strerror(resp->code), resp->code); + ctx->error_msg = strdup(err_buf); + tlsuv_http_close(&ctx->http_client, esm_url_fetch_close_cb); + return; + } + + ctx->status_code = resp->code; + resp->body_cb = esm_url_fetch_body_cb; +} + +static char *esm_get_cache_path(const char *url) { + const char *home = esm_get_home_dir(); + if (!home) home = "."; + + uint64_t hash = hash_key(url, strlen(url)); + + size_t len = strlen(home) + 48; + char *cache_path = malloc(len); + if (!cache_path) return NULL; + +#ifdef _WIN32 + snprintf(cache_path, len, "%s\\.ant\\esm\\%016llx", home, (unsigned long long)hash); +#else + snprintf(cache_path, len, "%s/.ant/esm/%016llx", home, (unsigned long long)hash); +#endif + return cache_path; +} + +static char *esm_read_cache(const char *cache_path, const char *url, size_t *out_len) { + FILE *fp = fopen(cache_path, "rb"); + if (!fp) return NULL; + + fseek(fp, 0, SEEK_END); + long size = ftell(fp); + fseek(fp, 0, SEEK_SET); + + char *content = malloc(size + 1); + if (!content) { + fclose(fp); + return NULL; + } + + fread(content, 1, size, fp); + fclose(fp); + content[size] = '\0'; + + char *newline = strchr(content, '\n'); + if (!newline) { + free(content); + return NULL; + } + + size_t url_len = newline - content; + if (strncmp(content, url, url_len) != 0 || strlen(url) != url_len) { + free(content); + return NULL; + } + + char *body = newline + 1; + size_t body_len = size - (body - content); + + char *result = malloc(body_len + 1); + if (!result) { + free(content); + return NULL; + } + + memcpy(result, body, body_len); + result[body_len] = '\0'; + free(content); + + if (out_len) *out_len = body_len; + return result; +} + +static void esm_mkdir_recursive(char *path) { + for (char *p = path + 1; *p; p++) { +#ifdef _WIN32 + if (p == path + 2 && path[1] == ':') continue; +#endif + if (is_path_sep(*p)) { + *p = '\0'; + mkdir_p(path); + *p = PATH_SEP; + } + } + mkdir_p(path); +} + +static void esm_write_cache(const char *cache_path, const char *url, const char *content, size_t len) { + char *dir = strdup(cache_path); + if (!dir) return; + + for (char *p = dir + strlen(dir) - 1; p > dir; p--) { + if (is_path_sep(*p)) { *p = '\0'; break; } + } + + esm_mkdir_recursive(dir); + free(dir); + + FILE *fp = fopen(cache_path, "wb"); + if (!fp) return; + + fprintf(fp, "%s\n", url); + fwrite(content, 1, len, fp); + fclose(fp); +} + +char *esm_fetch_url(const char *url, size_t *out_len, char **out_error) { + char *cache_path = esm_get_cache_path(url); + if (cache_path) { + char *cached = esm_read_cache(cache_path, url, out_len); + if (cached) { free(cache_path); return cached; } + } + + uv_loop_t *loop = uv_default_loop(); + esm_url_fetch_t ctx = {0}; + + ctx.capacity = 16384; + ctx.data = malloc(ctx.capacity); + if (!ctx.data) { + if (out_error) *out_error = strdup("Out of memory"); + return NULL; + } + + const char *scheme_end = strstr(url, "://"); + if (!scheme_end) { + free(ctx.data); + if (out_error) *out_error = strdup("Invalid URL: no scheme"); + return NULL; + } + + const char *host_start = scheme_end + 3; + const char *path_start = strchr(host_start, '/'); + const char *at_in_host = NULL; + + for (const char *p = host_start; p < (path_start ? path_start : host_start + strlen(host_start)); p++) { + if (*p == '@') at_in_host = p; + } + if (at_in_host) host_start = at_in_host + 1; + + size_t scheme_len = scheme_end - url; + size_t host_len = path_start ? (size_t)(path_start - host_start) : strlen(host_start); + const char *path = path_start ? path_start : "/"; + + char *host_url = calloc(1, scheme_len + 3 + host_len + 1); + snprintf(host_url, scheme_len + 3 + host_len + 1, "%.*s://%.*s", (int)scheme_len, url, (int)host_len, host_start); + int rc = tlsuv_http_init(loop, &ctx.http_client, host_url); + free(host_url); + + if (rc != 0) { + free(ctx.data); + if (out_error) *out_error = strdup("Failed to initialize HTTP client"); + return NULL; + } + + ctx.http_client.data = &ctx; + ctx.http_req = tlsuv_http_req(&ctx.http_client, "GET", path, esm_url_fetch_resp_cb, &ctx); + + if (!ctx.http_req) { + free(ctx.data); + tlsuv_http_close(&ctx.http_client, NULL); + if (out_error) *out_error = strdup("Failed to create HTTP request"); + return NULL; + } + + ctx.http_req->data = &ctx; + while (!ctx.completed) uv_run(loop, UV_RUN_ONCE); + + if (ctx.failed || ctx.status_code < 200 || ctx.status_code >= 400) { + if (out_error) { + if (ctx.error_msg) { + *out_error = ctx.error_msg; + ctx.error_msg = NULL; + } else { + char err_buf[64]; + snprintf(err_buf, sizeof(err_buf), "HTTP error: %d", ctx.status_code); + *out_error = strdup(err_buf); + } + } + free(ctx.data); + if (ctx.error_msg) free(ctx.error_msg); + if (cache_path) free(cache_path); + return NULL; + } + + ctx.data[ctx.size] = '\0'; + if (out_len) *out_len = ctx.size; + if (ctx.error_msg) free(ctx.error_msg); + + if (cache_path) { + esm_write_cache(cache_path, url, ctx.data, ctx.size); + free(cache_path); + } + + return ctx.data; +} + +char *esm_resolve_url(const char *specifier, const char *base_url) { + if (esm_is_url(specifier)) { + return strdup(specifier); + } + + if (specifier[0] == '/') { + const char *scheme_end = strstr(base_url, "://"); + if (!scheme_end) return NULL; + + const char *host_start = scheme_end + 3; + const char *path_start = strchr(host_start, '/'); + + const char *at_in_host = NULL; + for (const char *p = host_start; p < (path_start ? path_start : host_start + strlen(host_start)); p++) { + if (*p == '@') at_in_host = p; + } + if (at_in_host) host_start = at_in_host + 1; + + size_t scheme_len = scheme_end - base_url; + size_t host_len = path_start ? (size_t)(path_start - host_start) : strlen(host_start); + + size_t len = scheme_len + 3 + host_len + strlen(specifier) + 1; + char *result = malloc(len); + if (!result) return NULL; + + snprintf( + result, len, "%.*s://%.*s%s", + (int)scheme_len, base_url, + (int)host_len, host_start, + specifier + ); + + return result; + } + + if (specifier[0] == '.' && (specifier[1] == '/' || (specifier[1] == '.' && specifier[2] == '/'))) { + char *base_copy = strdup(base_url); + if (!base_copy) return NULL; + + char *last_slash = strrchr(base_copy, '/'); + char *scheme_end = strstr(base_copy, "://"); + if (scheme_end && last_slash > scheme_end + 2) { + *last_slash = '\0'; + } + + const char *spec = specifier; + while (strncmp(spec, "../", 3) == 0) { + spec += 3; + char *prev_slash = strrchr(base_copy, '/'); + if (prev_slash && prev_slash > scheme_end + 2) { + *prev_slash = '\0'; + } + } + + if (strncmp(spec, "./", 2) == 0) spec += 2; + size_t len = strlen(base_copy) + strlen(spec) + 2; + char *result = malloc(len); + if (!result) { free(base_copy); return NULL; } + + snprintf(result, len, "%s/%s", base_copy, spec); + free(base_copy); + + return result; + } + + return strdup(specifier); +} diff --git a/src/main.c b/src/main.c index 7e512e8..52c1b7e 100644 --- a/src/main.c +++ b/src/main.c @@ -12,6 +12,7 @@ #include "utils.h" #include "runtime.h" #include "snapshot.h" +#include "esm/remote.h" #include "modules/builtin.h" #include "modules/buffer.h" @@ -72,41 +73,58 @@ static void eval_code(struct js *js, struct arg_str *eval, struct arg_lit *print js_run_event_loop(js); } -static int execute_module(struct js *js, const char *filename) { - char *filename_copy = strdup(filename); - char *dir = dirname(filename_copy); - - js_set(js, js_glob(js), "__dirname", js_mkstr(js, dir, strlen(dir))); - js_set(js, js_glob(js), "__filename", js_mkstr(js, filename, strlen(filename))); - - free(filename_copy); - +static char *read_file(const char *filename, size_t *len) { FILE *fp = fopen(filename, "rb"); - if (fp == NULL) { - fprintf(stderr, "Error: Could not open file '%s'\n", filename); - return EXIT_FAILURE; - } - + if (!fp) return NULL; + fseek(fp, 0, SEEK_END); - long file_size = ftell(fp); + long size = ftell(fp); fseek(fp, 0, SEEK_SET); - - char *buffer = malloc(file_size + 1); - if (buffer == NULL) { - fprintf(stderr, "Error: Memory allocation failed\n"); + + char *buffer = malloc(size + 1); + if (!buffer) { fclose(fp); - return EXIT_FAILURE; + return NULL; } - - size_t len = fread(buffer, 1, file_size, fp); + + *len = fread(buffer, 1, size, fp); fclose(fp); - buffer[len] = '\0'; + buffer[*len] = '\0'; - char abs_path[4096]; - const char *use_path = NULL; - if (realpath(filename, abs_path) != NULL) { - use_path = abs_path; - } else use_path = filename; + return buffer; +} + +static int execute_module(struct js *js, const char *filename) { + char *buffer = NULL; + size_t len = 0; + const char *use_path = filename; + + if (esm_is_url(filename)) { + char *error = NULL; + buffer = esm_fetch_url(filename, &len, &error); + + if (!buffer) { + fprintf(stderr, "Error: Could not fetch '%s': %s\n", filename, error ? error : "unknown error"); + free(error); + return EXIT_FAILURE; + } + + js_set(js, js_glob(js), "__dirname", js_mkundef()); + } else { + buffer = read_file(filename, &len); + if (!buffer) { + fprintf(stderr, "Error: Could not open file '%s'\n", filename); + return EXIT_FAILURE; + } + + char *file_path = strdup(filename); + char *dir = dirname(file_path); + js_set(js, js_glob(js), "__dirname", js_mkstr(js, dir, strlen(dir))); + free(file_path); + + char abs_path[4096]; + if (realpath(filename, abs_path)) use_path = abs_path; + } char *js_code = buffer; size_t js_len = len; @@ -122,6 +140,8 @@ static int execute_module(struct js *js, const char *filename) { } js_set_filename(js, use_path); + js_set(js, js_glob(js), "__filename", js_mkstr(js, filename, strlen(filename))); + js_setup_import_meta(js, use_path); js_mkscope(js);