From b46e85efe8ab4a4eeb93bc85962a19316a612863 Mon Sep 17 00:00:00 2001 From: theMackabu Date: Sun, 29 Mar 2026 02:04:55 -0700 Subject: [PATCH] add node:http --- examples/demo/server.cjs | 2 + include/esm/builtin_bundle.h | 21 ++ include/modules/http_metadata.h | 8 + meson.build | 1 + meson/builtins/meson.build | 19 ++ meson/meson.build | 1 + sources.json | 36 ++- src/ant.c | 86 ++++--- src/builtins/node/http.cjs | 409 ++++++++++++++++++++++++++++++++ src/esm/builtin_bundle.c | 24 ++ src/esm/loader.c | 141 +++++++++-- src/http/http1_parser.c | 6 + src/main.c | 2 + src/modules/http_metadata.c | 120 ++++++++++ src/tools/gen_builtin_bundle.js | 130 ++++++++++ 15 files changed, 948 insertions(+), 58 deletions(-) create mode 100644 include/esm/builtin_bundle.h create mode 100644 include/modules/http_metadata.h create mode 100644 meson/builtins/meson.build create mode 100644 src/builtins/node/http.cjs create mode 100644 src/esm/builtin_bundle.c create mode 100644 src/modules/http_metadata.c create mode 100644 src/tools/gen_builtin_bundle.js diff --git a/examples/demo/server.cjs b/examples/demo/server.cjs index 2188615..0123ebf 100644 --- a/examples/demo/server.cjs +++ b/examples/demo/server.cjs @@ -1,3 +1,5 @@ +console.log('started on http://localhost:3000'); + require('node:http') .createServer((_req, res) => res.end('ant!')) .listen(3000); diff --git a/include/esm/builtin_bundle.h b/include/esm/builtin_bundle.h new file mode 100644 index 0000000..4cd90b4 --- /dev/null +++ b/include/esm/builtin_bundle.h @@ -0,0 +1,21 @@ +#ifndef ESM_BUILTIN_BUNDLE_H +#define ESM_BUILTIN_BUNDLE_H + +#include "esm/loader.h" + +#include +#include +#include + +typedef struct { + const char *specifier; + const char *source_name; + const uint8_t *code; + size_t code_len; + ant_module_format_t format; +} ant_builtin_bundle_entry_t; + +bool esm_is_builtin_specifier(const char *specifier); +const ant_builtin_bundle_entry_t *esm_lookup_builtin_bundle(const char *specifier, size_t spec_len); + +#endif diff --git a/include/modules/http_metadata.h b/include/modules/http_metadata.h new file mode 100644 index 0000000..c11707b --- /dev/null +++ b/include/modules/http_metadata.h @@ -0,0 +1,8 @@ +#ifndef ANT_INTERNAL_HTTP_METADATA_MODULE_H +#define ANT_INTERNAL_HTTP_METADATA_MODULE_H + +#include "types.h" + +ant_value_t internal_http_metadata_library(ant_t *js); + +#endif diff --git a/meson.build b/meson.build index 32539d0..2a08830 100644 --- a/meson.build +++ b/meson.build @@ -39,6 +39,7 @@ lib_sources = files(run_command( libant = static_library( 'ant', lib_sources + [ + builtin_bundle_h, snapshot_h, messages_h, theme_h diff --git a/meson/builtins/meson.build b/meson/builtins/meson.build new file mode 100644 index 0000000..069af9f --- /dev/null +++ b/meson/builtins/meson.build @@ -0,0 +1,19 @@ +node = find_program('node', required: true) + +builtin_files = files(run_command( + python, files('../sources.py'), src_root / 'sources.json', 'builtins', src_root, + check: true +).stdout().strip().split()) + +builtin_bundle_h = custom_target( + 'builtin_bundle', + input: builtin_files, + output: 'builtin_bundle_data.h', + command: [ + node, + src_root / 'src' / 'tools' / 'gen_builtin_bundle.js', + src_root / 'src' / 'builtins', + '@OUTPUT@', + '@INPUT@', + ], +) diff --git a/meson/meson.build b/meson/meson.build index eb228f6..36b6a37 100644 --- a/meson/meson.build +++ b/meson/meson.build @@ -31,3 +31,4 @@ endif subdir('deps') subdir('version') subdir('snapshot') +subdir('builtins') diff --git a/sources.json b/sources.json index 00480f8..97345b7 100644 --- a/sources.json +++ b/sources.json @@ -1,14 +1,46 @@ { "engine": { - "patterns": ["src/*.c", "src/gc/*.c", "src/highlight/*.c", "src/esm/*.c", "src/cli/*.c", "src/modules/*.c", "src/streams/*.c", "src/silver/*.c", "src/http/*.c", "src/net/*.c"], + "patterns": [ + "src/*.c", + "src/gc/*.c", + "src/highlight/*.c", + "src/esm/*.c", + "src/cli/*.c", + "src/modules/*.c", + "src/streams/*.c", + "src/silver/*.c", + "src/http/*.c", + "src/net/*.c" + ], "exclude": ["src/main.c", "src/watch.c"] }, "library": { - "patterns": ["src/*.c", "src/gc/*.c", "src/highlight/*.c", "src/esm/*.c", "src/modules/*.c", "src/streams/*.c", "src/silver/*.c", "src/http/*.c", "src/net/*.c"], + "patterns": [ + "src/*.c", + "src/gc/*.c", + "src/highlight/*.c", + "src/esm/*.c", + "src/modules/*.c", + "src/streams/*.c", + "src/silver/*.c", + "src/http/*.c", + "src/net/*.c" + ], "exclude": ["src/main.c", "src/watch.c"] }, "core": { "patterns": ["src/core/**/*.ts"], "exclude": ["src/core/index.ts"] + }, + "builtins": { + "patterns": [ + "src/builtins/**/*.cts", + "src/builtins/**/*.mts", + "src/builtins/**/*.ts", + "src/builtins/**/*.cjs", + "src/builtins/**/*.mjs", + "src/builtins/**/*.js" + ], + "exclude": [] } } diff --git a/src/ant.c b/src/ant.c index d2d89df..06b9b59 100644 --- a/src/ant.c +++ b/src/ant.c @@ -24,6 +24,7 @@ #include "esm/remote.h" #include "esm/loader.h" +#include "esm/builtin_bundle.h" #include "silver/lexer.h" #include "silver/compiler.h" @@ -11208,12 +11209,52 @@ static ant_value_t builtin_import_meta_resolve(ant_t *js, ant_value_t *args, int ant_value_t import_meta = js_get_current_import_meta(js); if (vtype(import_meta) == T_OBJ) { - ant_value_t filename = js_get(js, import_meta, "filename"); - if (vtype(filename) == T_STR) { - ant_offset_t n = 0; ant_offset_t off = vstr(js, filename, &n); - return js_esm_resolve_specifier(js, args[0], (const char *)(uintptr_t)(off)); - } - } return js_esm_resolve_specifier(js, args[0], NULL); + ant_value_t filename = js_get(js, import_meta, "filename"); + + if (vtype(filename) == T_STR) { + ant_offset_t n = 0; ant_offset_t off = vstr(js, filename, &n); + return js_esm_resolve_specifier(js, args[0], (const char *)(uintptr_t)(off)); + }} + + return js_esm_resolve_specifier(js, args[0], NULL); +} + +static inline void js_set_import_meta_special_dirname( + ant_t *js, + ant_value_t import_meta, + const char *filename, + bool is_builtin +) { + char *filename_copy = strdup(filename); + if (!filename_copy) return; + + char *last_slash = strrchr(filename_copy, '/'); + char *scheme_end = strstr(filename_copy, "://"); + + if ((is_builtin && last_slash) || (!is_builtin && last_slash && scheme_end && last_slash > scheme_end + 2)) { + *last_slash = '\0'; + ant_value_t dirname_val = js_mkstr(js, filename_copy, strlen(filename_copy)); + if (!is_err(dirname_val)) js_setprop(js, import_meta, js_mkstr(js, "dirname", 7), dirname_val); + } + + free(filename_copy); +} + +static inline void js_set_import_meta_path_dirname( + ant_t *js, + ant_value_t import_meta, + const char *filename +) { + char *filename_copy = strdup(filename); + if (!filename_copy) return; + + char *dir = dirname(filename_copy); + if (dir) { + ant_value_t dirname_val = js_mkstr(js, dir, strlen(dir)); + if (!is_err(dirname_val)) js_setprop(js, import_meta, js_mkstr(js, "dirname", 7), dirname_val); + } + + free(filename_copy); } ant_value_t js_create_import_meta(ant_t *js, const char *filename, bool is_main) { @@ -11221,41 +11262,26 @@ ant_value_t js_create_import_meta(ant_t *js, const char *filename, bool is_main) ant_value_t import_meta = mkobj(js, 0); if (is_err(import_meta)) return import_meta; + bool is_url = esm_is_url(filename); + bool is_builtin = esm_is_builtin_specifier(filename); - ant_value_t url_val = is_url ? js_mkstr(js, filename, strlen(filename)) : js_esm_make_file_url(js, filename); + ant_value_t url_val = (is_url || is_builtin) + ? js_mkstr(js, filename, strlen(filename)) + : js_esm_make_file_url(js, filename); + if (!is_err(url_val)) js_setprop(js, import_meta, js_mkstr(js, "url", 3), url_val); ant_value_t filename_val = js_mkstr(js, filename, strlen(filename)); if (!is_err(filename_val)) js_setprop(js, import_meta, js_mkstr(js, "filename", 8), filename_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'; - ant_value_t dirname_val = js_mkstr(js, filename_copy, strlen(filename_copy)); - if (!is_err(dirname_val)) js_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) { - ant_value_t dirname_val = js_mkstr(js, dir, strlen(dir)); - if (!is_err(dirname_val)) js_setprop(js, import_meta, js_mkstr(js, "dirname", 7), dirname_val); - } - free(filename_copy); - } - } + if (is_url || is_builtin) js_set_import_meta_special_dirname(js, import_meta, filename, is_builtin); + else js_set_import_meta_path_dirname(js, import_meta, filename); js_setprop(js, import_meta, js_mkstr(js, "main", 4), is_main ? js_true : js_false); ant_value_t resolve_fn = js_mkfun(builtin_import_meta_resolve); js_setprop(js, import_meta, js_mkstr(js, "resolve", 7), resolve_fn); + return import_meta; } diff --git a/src/builtins/node/http.cjs b/src/builtins/node/http.cjs new file mode 100644 index 0000000..3389a3e --- /dev/null +++ b/src/builtins/node/http.cjs @@ -0,0 +1,409 @@ +const net = require('node:net'); +const httpMetadata = require('ant:internal/http_metadata'); +const httpParser = require('ant:internal/http_parser'); +const httpWriter = require('ant:internal/http_writer'); + +const METHODS = httpMetadata.METHODS; +const STATUS_CODES = httpMetadata.STATUS_CODES; + +function createHeadersObject() { + return Object.create(null); +} + +function normalizeHeaderName(name) { + return String(name).toLowerCase(); +} + +function appendHeaderValue(headers, key, value) { + if (key === 'set-cookie') { + if (!headers[key]) headers[key] = []; + headers[key].push(value); + return; + } + + if (headers[key] === undefined) { + headers[key] = value; + return; + } + + if (key === 'cookie') headers[key] += '; ' + value; + else headers[key] += ', ' + value; +} + +function buildHeaders(rawHeaders) { + const headers = createHeadersObject(); + let i = 0; + + for (i = 0; i + 1 < rawHeaders.length; i += 2) { + appendHeaderValue(headers, normalizeHeaderName(rawHeaders[i]), String(rawHeaders[i + 1])); + } + + return headers; +} + +function bufferFrom(value, encoding) { + if (value === undefined || value === null) return Buffer.alloc(0); + if (Buffer.isBuffer(value)) return value; + if (value && typeof value === 'object' && typeof value.byteLength === 'number') return Buffer.from(value); + if (typeof value === 'string') return Buffer.from(value, encoding || 'utf8'); + return Buffer.from(String(value), encoding || 'utf8'); +} + +function appendRawHeader(rawHeaders, name, value) { + if (Array.isArray(value)) { + value.forEach(item => rawHeaders.push(name, String(item))); + return; + } + + rawHeaders.push(name, String(value)); +} + +function hasResponseBody(method, statusCode) { + if (method === 'HEAD') return false; + if (statusCode === 204 || statusCode === 304) return false; + return true; +} + +function applyHeaderObject(target, headers) { + if (!headers) return; + + if (Array.isArray(headers)) { + for (let i = 0; i + 1 < headers.length; i += 2) { + target.setHeader(headers[i], headers[i + 1]); + } + return; + } + + Object.keys(headers).forEach(name => { + target.setHeader(name, headers[name]); + }); +} + +function makeSocketState(server, socket) { + return { + server, + socket, + buffered: Buffer.alloc(0), + activeResponse: null, + closed: false + }; +} + +function appendSocketChunk(buffered, chunk) { + const next = bufferFrom(chunk); + if (buffered.length === 0) return next; + return Buffer.concat([buffered, next]); +} + +function handleClientError(server, socket, error) { + if (server.listenerCount('clientError') > 0) { + server.emit('clientError', error, socket); + return; + } + + if (socket && !socket.destroyed) { + socket.end(httpWriter.writeBasicResponse(400, 'Bad Request', 'text/plain;charset=UTF-8', 'Bad Request', false)); + } +} + +class IncomingMessage extends EventEmitter { + constructor(socket, parsed) { + super(); + this.socket = socket; + this.connection = socket; + this.method = parsed.method; + this.url = parsed.target; + this.headers = buildHeaders(parsed.rawHeaders || []); + this.rawHeaders = (parsed.rawHeaders || []).slice(); + this.httpVersion = parsed.httpVersion; + this.httpVersionMajor = parsed.httpVersionMajor; + this.httpVersionMinor = parsed.httpVersionMinor; + this._keepAlive = !!parsed.keepAlive; + this.complete = false; + this.aborted = false; + this.destroyed = false; + this.readableEnded = false; + this._body = bufferFrom(parsed.body); + } + + _deliverBody() { + if (this.destroyed || this.complete) return; + + if (this._body.length > 0) this.emit('data', this._body); + this.complete = true; + this.readableEnded = true; + this.emit('end'); + this.emit('close'); + } + + destroy(error) { + if (this.destroyed) return this; + this.destroyed = true; + if (error) this.emit('error', error); + if (this.socket && !this.socket.destroyed) this.socket.destroy(error); + return this; + } + + setTimeout(msecs, callback) { + if (this.socket && this.socket.setTimeout) this.socket.setTimeout(msecs, callback); + return this; + } +} + +class ServerResponse extends EventEmitter { + constructor(req, socket, socketState) { + super(); + this.req = req; + this.socket = socket; + this.connection = socket; + this.statusCode = 200; + this.statusMessage = undefined; + this.sendDate = true; + this.strictContentLength = false; + this.headersSent = false; + this.writableEnded = false; + this.writableFinished = false; + this.finished = false; + this._headers = createHeadersObject(); + this._headerNames = createHeadersObject(); + this._socketState = socketState; + this._streaming = false; + } + + setHeader(name, value) { + const headerName = String(name); + const key = normalizeHeaderName(headerName); + this._headers[key] = value; + this._headerNames[key] = headerName; + return this; + } + + getHeader(name) { + return this._headers[normalizeHeaderName(name)]; + } + + getHeaders() { + const copy = createHeadersObject(); + Object.keys(this._headers).forEach(key => { + copy[key] = this._headers[key]; + }); + return copy; + } + + getHeaderNames() { + return Object.keys(this._headers); + } + + hasHeader(name) { + return this._headers[normalizeHeaderName(name)] !== undefined; + } + + removeHeader(name) { + const key = normalizeHeaderName(name); + delete this._headers[key]; + delete this._headerNames[key]; + return this; + } + + writeHead(statusCode, statusMessage, headers) { + this.statusCode = statusCode | 0; + + if (typeof statusMessage === 'string') { + this.statusMessage = statusMessage; + applyHeaderObject(this, headers); + } else { + applyHeaderObject(this, statusMessage); + } + + return this; + } + + _rawHeaders() { + const rawHeaders = []; + + if (this.sendDate && !this.hasHeader('date')) { + this.setHeader('Date', new Date().toUTCString()); + } + + Object.keys(this._headers).forEach(key => { + const name = this._headerNames[key] || key; + appendRawHeader(rawHeaders, name, this._headers[key]); + }); + + return rawHeaders; + } + + _shouldKeepAlive() { + const connection = this.getHeader('connection'); + if (typeof connection === 'string') { + if (connection.toLowerCase() === 'close') return false; + if (connection.toLowerCase() === 'keep-alive') return true; + } + + return !!(this.req && this.req._keepAlive); + } + + _writeHead(bodyIsStream, bodySize) { + if (this.headersSent) return; + + const statusText = this.statusMessage || STATUS_CODES[this.statusCode] || httpWriter.defaultStatusText(this.statusCode); + const head = httpWriter.writeHead(this.statusCode, statusText, this._rawHeaders(), bodyIsStream, bodySize, this._shouldKeepAlive()); + + this.headersSent = true; + this.socket.write(head); + } + + write(chunk, encoding, callback) { + const body = hasResponseBody(this.req && this.req.method, this.statusCode) + ? bufferFrom(chunk, typeof encoding === 'string' ? encoding : undefined) + : Buffer.alloc(0); + + if (typeof encoding === 'function') callback = encoding; + if (!this.headersSent) this._writeHead(true, 0); + this._streaming = true; + + if (body.length > 0) this.socket.write(httpWriter.writeChunk(body)); + if (typeof callback === 'function') callback(); + return true; + } + + end(chunk, encoding, callback) { + let body = Buffer.alloc(0); + let keepAlive = false; + + if (this.writableEnded) return this; + if (typeof encoding === 'function') { + callback = encoding; + encoding = undefined; + } + + if (hasResponseBody(this.req && this.req.method, this.statusCode)) { + body = bufferFrom(chunk, typeof encoding === 'string' ? encoding : undefined); + } + + if (this._streaming) { + if (!this.headersSent) this._writeHead(true, 0); + if (body.length > 0) this.socket.write(httpWriter.writeChunk(body)); + this.socket.write(httpWriter.writeFinalChunk()); + } else { + this._writeHead(false, body.length); + if (body.length > 0) this.socket.write(body); + } + + this.writableEnded = true; + this.writableFinished = true; + this.finished = true; + this.emit('finish'); + this.emit('close'); + + keepAlive = this._shouldKeepAlive(); + this._socketState.activeResponse = null; + if (typeof callback === 'function') callback(); + + if (!keepAlive) this.socket.end(); + else if (this._socketState.buffered.length > 0) this._socketState.server._drainSocket(this.socket, this._socketState); + return this; + } + + setTimeout(msecs, callback) { + if (this.socket && this.socket.setTimeout) this.socket.setTimeout(msecs, callback); + return this; + } +} + +class Server extends net.Server { + constructor(options, requestListener) { + const serverOptions = typeof options === 'function' ? undefined : options; + const onRequest = typeof options === 'function' ? options : requestListener; + + super(serverOptions); + this.timeout = 0; + this.headersTimeout = 60000; + this.requestTimeout = 300000; + this.keepAliveTimeout = 5000; + this.maxHeadersCount = 2000; + + this.on('connection', socket => { + this._attachSocket(socket); + }); + + if (typeof onRequest === 'function') this.on('request', onRequest); + } + + _attachSocket(socket) { + const state = makeSocketState(this, socket); + + if (this.timeout > 0 && socket.setTimeout) { + socket.setTimeout(this.timeout, () => { + this.emit('timeout', socket); + }); + } + + socket.on('data', chunk => { + state.buffered = appendSocketChunk(state.buffered, chunk); + this._drainSocket(socket, state); + }); + + socket.on('error', error => { + handleClientError(this, socket, error); + }); + + socket.on('close', () => { + state.closed = true; + }); + } + + _drainSocket(socket, state) { + while (!state.closed) { + let parsed = null; + + if (state.activeResponse && !state.activeResponse.writableEnded) return; + if (state.buffered.length === 0) return; + + try { + parsed = httpParser.parseRequest(state.buffered); + } catch (error) { + state.closed = true; + handleClientError(this, socket, error); + return; + } + + if (parsed === null) return; + if (!parsed.consumed || parsed.consumed < 0) { + state.closed = true; + handleClientError(this, socket, new Error('Invalid HTTP parser state')); + return; + } + + state.buffered = parsed.consumed < state.buffered.length ? state.buffered.subarray(parsed.consumed) : Buffer.alloc(0); + + const req = new IncomingMessage(socket, parsed); + const res = new ServerResponse(req, socket, state); + state.activeResponse = res; + + this.emit('request', req, res); + req._deliverBody(); + + if (!res.writableEnded) return; + } + } + + setTimeout(msecs, callback) { + this.timeout = msecs | 0; + if (typeof callback === 'function') this.on('timeout', callback); + return this; + } +} + +function createServer(options, requestListener) { + return new Server(options, requestListener); +} + +module.exports = { + METHODS, + STATUS_CODES, + IncomingMessage, + Server, + ServerResponse, + createServer +}; diff --git a/src/esm/builtin_bundle.c b/src/esm/builtin_bundle.c new file mode 100644 index 0000000..210235e --- /dev/null +++ b/src/esm/builtin_bundle.c @@ -0,0 +1,24 @@ +#include + +#include "esm/builtin_bundle.h" +#include "builtin_bundle_data.h" + +bool esm_is_builtin_specifier(const char *specifier) { + if (!specifier) return false; + return + strncmp(specifier, "node:", 5) == 0 || + strncmp(specifier, "ant:", 4) == 0; +} + +const ant_builtin_bundle_entry_t *esm_lookup_builtin_bundle(const char *specifier, size_t spec_len) { + size_t i = 0; + if (!specifier) return NULL; + + for (i = 0; i < ant_builtin_bundle_data_count; i++) { + const ant_builtin_bundle_entry_t *entry = &ant_builtin_bundle_data[i]; + size_t entry_len = strlen(entry->specifier); + if (entry_len == spec_len && memcmp(entry->specifier, specifier, spec_len) == 0) return entry; + } + + return NULL; +} diff --git a/src/esm/loader.c b/src/esm/loader.c index 987bb34..076ed48 100644 --- a/src/esm/loader.c +++ b/src/esm/loader.c @@ -4,6 +4,7 @@ #include "esm/commonjs.h" #include "esm/library.h" #include "esm/remote.h" +#include "esm/builtin_bundle.h" #include "modules/json.h" #include "modules/napi.h" @@ -36,15 +37,21 @@ typedef enum { typedef struct esm_module { char *path; + char *cache_key; char *resolved_path; char *url_content; + size_t url_content_len; + const uint8_t *embedded_code; + size_t embedded_code_len; + ant_value_t namespace_obj; ant_value_t default_export; ant_value_t tla_promise; UT_hash_handle hh; esm_module_kind_t kind; ant_module_format_t format; + bool is_loaded; bool is_loading; bool has_tla; @@ -62,7 +69,9 @@ typedef struct { static esm_module_cache_t global_module_cache = {NULL, 0}; static int esm_dynamic_import_depth = 0; + static char *esm_resolve_node_module(const char *specifier, const char *base_path); +static char *esm_canonicalize_path(const char *path); static char *esm_file_url_to_path(const char *specifier) { if (!specifier || strncmp(specifier, "file:", 5) != 0) return NULL; @@ -75,6 +84,14 @@ static char *esm_file_url_to_path(const char *specifier) { return strdup(p); } +static char *esm_make_cache_key(const char *module_key) { + if (!module_key) return NULL; + if (esm_is_builtin_specifier(module_key)) return strdup(module_key); + if (esm_is_data_url(module_key)) return strdup(module_key); + if (esm_is_url(module_key)) return strdup(module_key); + return esm_canonicalize_path(module_key); +} + static char *esm_get_extension(const char *path) { const char *dot = strrchr(path, '.'); const char *slash = strrchr(path, '/'); @@ -832,51 +849,68 @@ static char *esm_canonicalize_path(const char *path) { return canonical; } -static esm_module_t *esm_find_module(const char *resolved_path) { - char *canonical_path = esm_canonicalize_path(resolved_path); - if (!canonical_path) return NULL; +static esm_module_t *esm_find_module(const char *module_key) { + char *cache_key = esm_make_cache_key(module_key); + if (!cache_key) return NULL; esm_module_t *mod = NULL; - HASH_FIND_STR(global_module_cache.modules, canonical_path, mod); + HASH_FIND_STR(global_module_cache.modules, cache_key, mod); - free(canonical_path); + free(cache_key); return mod; } -static esm_module_t *esm_create_module(const char *path, const char *resolved_path) { - bool is_url = esm_is_url(resolved_path) || esm_is_data_url(resolved_path); - char *canonical_path = is_url ? strdup(resolved_path) : esm_canonicalize_path(resolved_path); - if (!canonical_path) return NULL; +static esm_module_t *esm_create_module( + const char *path, + const char *resolved_path, + const char *module_key, + ant_module_format_t format, + const uint8_t *embedded_code, + size_t embedded_code_len +) { + char *cache_key = esm_make_cache_key(module_key); + if (!cache_key) return NULL; esm_module_t *existing_mod = NULL; - HASH_FIND_STR(global_module_cache.modules, canonical_path, existing_mod); + HASH_FIND_STR(global_module_cache.modules, cache_key, existing_mod); if (existing_mod) { - free(canonical_path); + free(cache_key); return existing_mod; } esm_module_t *mod = (esm_module_t *)malloc(sizeof(esm_module_t)); if (!mod) { - free(canonical_path); + free(cache_key); return NULL; } *mod = (esm_module_t){ .path = strdup(path), - .resolved_path = canonical_path, + .cache_key = cache_key, + .resolved_path = strdup(resolved_path), .namespace_obj = js_mkundef(), .default_export = js_mkundef(), .is_loaded = false, .is_loading = false, .kind = esm_classify_module_kind(resolved_path), - .format = MODULE_EVAL_FORMAT_UNKNOWN, + .format = format, .url_content = NULL, .url_content_len = 0, + .embedded_code = embedded_code, + .embedded_code_len = embedded_code_len, .tla_promise = js_mkundef(), .has_tla = false, }; - HASH_ADD_STR(global_module_cache.modules, resolved_path, mod); + if (!mod->path || !mod->resolved_path) { + free(mod->path); + free(mod->cache_key); + free(mod->resolved_path); + free(mod); + return NULL; + } + + HASH_ADD_STR(global_module_cache.modules, cache_key, mod); global_module_cache.count++; return mod; @@ -887,6 +921,7 @@ void js_esm_cleanup_module_cache(void) { HASH_ITER(hh, global_module_cache.modules, current, tmp) { HASH_DEL(global_module_cache.modules, current); if (current->path) free(current->path); + if (current->cache_key) free(current->cache_key); if (current->resolved_path) free(current->resolved_path); if (current->url_content) free(current->url_content); free(current); @@ -997,7 +1032,15 @@ static ant_value_t esm_load_module(ant_t *js, esm_module_t *mod) { char *content = NULL; size_t size = 0; - if (mod->kind == ESM_MODULE_KIND_URL && esm_is_data_url(mod->resolved_path)) { + if (mod->embedded_code) { + content = (char *)malloc(mod->embedded_code_len + 1); + if (!content) { + mod->is_loading = false; + return js_mkerr(js, "OOM loading bundled module"); + } + memcpy(content, mod->embedded_code, mod->embedded_code_len); + size = mod->embedded_code_len; + } else if (mod->kind == ESM_MODULE_KIND_URL && esm_is_data_url(mod->resolved_path)) { content = esm_parse_data_url(mod->resolved_path, &size); if (!content) { mod->is_loading = false; @@ -1033,22 +1076,21 @@ static ant_value_t esm_load_module(ant_t *js, esm_module_t *mod) { size_t js_len = size; const char *strip_detail = NULL; - + + if (!mod->embedded_code) { int strip_result = strip_typescript_inplace( &content, size, mod->resolved_path, &js_len, &strip_detail ); - + if (strip_result < 0) { ant_value_t err = js_mkerr( js, "TypeScript error: strip failed (%d): %s", strip_result, strip_detail ); - - free(content); + free(content); mod->is_loading = false; - return err; - } + }} char *js_code = content; ant_value_t ns = js_mkobj(js); @@ -1099,10 +1141,25 @@ static ant_value_t esm_load_module(ant_t *js, esm_module_t *mod) { return esm_complete_namespace_module(js, mod, ns); } -static ant_value_t esm_get_or_load(ant_t *js, const char *specifier, const char *resolved_path) { - esm_module_t *mod = esm_find_module(resolved_path); +static ant_value_t esm_get_or_load( + ant_t *js, + const char *specifier, + const char *resolved_path, + const char *module_key, + ant_module_format_t format, + const uint8_t *embedded_code, + size_t embedded_code_len +) { + esm_module_t *mod = esm_find_module(module_key); if (!mod) { - mod = esm_create_module(specifier, resolved_path); + mod = esm_create_module( + specifier, + resolved_path, + module_key, + format, + embedded_code, + embedded_code_len + ); if (!mod) return js_mkerr(js, "Cannot create module"); } return esm_load_module(js, mod); @@ -1119,6 +1176,7 @@ ant_value_t js_esm_import_sync_cstr_from( size_t spec_len, const char *base_path ) { + const ant_builtin_bundle_entry_t *bundle = NULL; char *spec_copy = strndup(specifier, spec_len); if (!spec_copy) return js_mkerr(js, "oom"); @@ -1129,6 +1187,21 @@ ant_value_t js_esm_import_sync_cstr_from( spec_len = strlen(spec_copy); } + bundle = esm_lookup_builtin_bundle(spec_copy, spec_len); + if (bundle) { + ant_value_t ns = esm_get_or_load( + js, + spec_copy, + bundle->source_name, + bundle->specifier, + bundle->format, + bundle->code, + bundle->code_len + ); + free(spec_copy); + return ns; + } + bool loaded = false; ant_value_t lib = js_esm_load_registered_library(js, spec_copy, spec_len, &loaded); if (loaded) { @@ -1144,7 +1217,15 @@ ant_value_t js_esm_import_sync_cstr_from( return err; } - ant_value_t ns = esm_get_or_load(js, spec_copy, resolved_path); + ant_value_t ns = esm_get_or_load( + js, + spec_copy, + resolved_path, + resolved_path, + MODULE_EVAL_FORMAT_UNKNOWN, + NULL, + 0 + ); free(resolved_path); free(spec_copy); return ns; @@ -1209,6 +1290,7 @@ HASH_ITER(hh, global_module_cache.modules, mod, tmp) { }} ant_value_t js_esm_resolve_specifier(ant_t *js, ant_value_t specifier, const char *base_path) { + const ant_builtin_bundle_entry_t *bundle = NULL; if (vtype(specifier) != T_STR) { return js_mkerr(js, "import.meta.resolve() requires a string specifier"); } @@ -1219,6 +1301,13 @@ ant_value_t js_esm_resolve_specifier(ant_t *js, ant_value_t specifier, const cha char *spec_copy = strndup(spec_str, (size_t)spec_len); if (!spec_copy) return js_mkerr(js, "oom"); + bundle = esm_lookup_builtin_bundle(spec_copy, (size_t)spec_len); + if (bundle) { + ant_value_t result = js_mkstr(js, bundle->specifier, strlen(bundle->specifier)); + free(spec_copy); + return result; + } + if (!base_path || !base_path[0]) base_path = esm_default_base_path(js); char *resolved_path = esm_resolve(spec_copy, base_path, esm_resolve_path); free(spec_copy); diff --git a/src/http/http1_parser.c b/src/http/http1_parser.c index 0e1d67c..09fd1cb 100644 --- a/src/http/http1_parser.c +++ b/src/http/http1_parser.c @@ -150,6 +150,9 @@ ant_http1_parse_result_t ant_http1_parse_request( return ANT_HTTP1_PARSE_INCOMPLETE; } + if (out->consumed_len == 0) ctx.req.consumed_len = len; + else ctx.req.consumed_len = out->consumed_len; + ctx.req.method = ant_http1_buffer_take(&ctx.method, NULL); ctx.req.target = ant_http1_buffer_take(&ctx.target, NULL); ctx.req.body = (uint8_t *)ant_http1_buffer_take(&ctx.body, &ctx.req.body_len); @@ -256,6 +259,9 @@ ant_http1_parse_result_t ant_http1_conn_parser_execute( if (!cp->ctx.message_complete) return ANT_HTTP1_PARSE_INCOMPLETE; + + if (consumed_out && *consumed_out == 0) *consumed_out = len; + cp->ctx.req.consumed_len = consumed_out ? *consumed_out : len; cp->ctx.req.method = ant_http1_buffer_take(&cp->ctx.method, NULL); cp->ctx.req.target = ant_http1_buffer_take(&cp->ctx.target, NULL); diff --git a/src/main.c b/src/main.c index e4117e2..528f8ab 100644 --- a/src/main.c +++ b/src/main.c @@ -70,6 +70,7 @@ #include "modules/util.h" #include "modules/async_hooks.h" #include "modules/net.h" +#include "modules/http_metadata.h" #include "modules/http_parser.h" #include "modules/http_writer.h" #include "modules/dns.h" @@ -616,6 +617,7 @@ int main(int argc, char *argv[]) { ant_register_library(internal_http_parser_library, "ant:internal/http_parser", NULL); ant_register_library(internal_http_writer_library, "ant:internal/http_writer", NULL); + ant_register_library(internal_http_metadata_library, "ant:internal/http_metadata", NULL); ant_standard_library("util", util_library); ant_standard_library("net", net_library); diff --git a/src/modules/http_metadata.c b/src/modules/http_metadata.c new file mode 100644 index 0000000..3f7b551 --- /dev/null +++ b/src/modules/http_metadata.c @@ -0,0 +1,120 @@ +#include // IWYU pragma: keep + +#include +#include + +#include "ant.h" +#include "modules/http_metadata.h" + +static const char *const http_methods[] = { + "ACL", + "BIND", + "CHECKOUT", + "CONNECT", + "COPY", + "DELETE", + "GET", + "HEAD", + "LINK", + "LOCK", + "M-SEARCH", + "MERGE", + "MKACTIVITY", + "MKCALENDAR", + "MKCOL", + "MOVE", + "NOTIFY", + "OPTIONS", + "PATCH", + "POST", + "PROPFIND", + "PROPPATCH", + "PURGE", + "PUT", + "QUERY", + "REBIND", + "REPORT", + "SEARCH", + "SOURCE", + "SUBSCRIBE", + "TRACE", + "UNBIND", + "UNLINK", + "UNLOCK", + "UNSUBSCRIBE" +}; + +typedef struct { + int code; + const char *text; +} http_status_entry_t; + +static const http_status_entry_t http_status_codes[] = { + {200, "OK"}, + {201, "Created"}, + {202, "Accepted"}, + {204, "No Content"}, + {301, "Moved Permanently"}, + {302, "Found"}, + {303, "See Other"}, + {304, "Not Modified"}, + {307, "Temporary Redirect"}, + {308, "Permanent Redirect"}, + {400, "Bad Request"}, + {401, "Unauthorized"}, + {403, "Forbidden"}, + {404, "Not Found"}, + {405, "Method Not Allowed"}, + {408, "Request Timeout"}, + {409, "Conflict"}, + {410, "Gone"}, + {413, "Payload Too Large"}, + {415, "Unsupported Media Type"}, + {421, "Misdirected Request"}, + {429, "Too Many Requests"}, + {500, "Internal Server Error"}, + {501, "Not Implemented"}, + {502, "Bad Gateway"}, + {503, "Service Unavailable"}, + {504, "Gateway Timeout"} +}; + +static ant_value_t http_metadata_make_methods(ant_t *js) { + ant_value_t methods = js_mkarr(js); + size_t count = sizeof(http_methods) / sizeof(http_methods[0]); + + for (size_t i = 0; i < count; i++) { + js_arr_push(js, methods, js_mkstr(js, http_methods[i], strlen(http_methods[i]))); + } + + return methods; +} + +static ant_value_t http_metadata_make_status_codes(ant_t *js) { + ant_value_t status_codes = js_mkobj(js); + + size_t count = sizeof(http_status_codes) / sizeof(http_status_codes[0]); + char code_buf[16]; + + for (size_t i = 0; i < count; i++) { + int code = http_status_codes[i].code; + size_t code_len = (size_t)snprintf(code_buf, sizeof(code_buf), "%d", code); + + js_setprop( + js, status_codes, + js_mkstr(js, code_buf, code_len), + js_mkstr(js, http_status_codes[i].text, strlen(http_status_codes[i].text)) + ); + } + + return status_codes; +} + +ant_value_t internal_http_metadata_library(ant_t *js) { + ant_value_t lib = js_mkobj(js); + + js_set(js, lib, "METHODS", http_metadata_make_methods(js)); + js_set(js, lib, "STATUS_CODES", http_metadata_make_status_codes(js)); + + return lib; +} diff --git a/src/tools/gen_builtin_bundle.js b/src/tools/gen_builtin_bundle.js new file mode 100644 index 0000000..eb327e8 --- /dev/null +++ b/src/tools/gen_builtin_bundle.js @@ -0,0 +1,130 @@ +import * as esbuild from 'esbuild'; +import path from 'node:path'; +import { writeFileSync } from 'node:fs'; + +function cString(input) { + return JSON.stringify(input); +} + +function toSpecifier(rootDir, filePath) { + const relativePath = path.relative(rootDir, filePath).replaceAll('\\', '/'); + const withoutExt = relativePath.replace(/\.(cts|mts|ts|cjs|mjs|js)$/u, ''); + + if (withoutExt.startsWith('node/')) { + return `node:${withoutExt.slice('node/'.length)}`; + } + + if (withoutExt.startsWith('ant/')) { + return `ant:${withoutExt.slice('ant/'.length)}`; + } + + throw new Error(`Unsupported builtin module path: ${relativePath}`); +} + +function toFormat(filePath) { + if (/\.(cts|cjs)$/u.test(filePath)) return 'MODULE_EVAL_FORMAT_CJS'; + if (/\.(mts|mjs)$/u.test(filePath)) return 'MODULE_EVAL_FORMAT_ESM'; + return 'MODULE_EVAL_FORMAT_UNKNOWN'; +} + +async function bundleBuiltin(entryPath, format) { + const output = await esbuild.build({ + entryPoints: [entryPath], + bundle: true, + write: false, + minify: true, + platform: 'neutral', + format: format === 'MODULE_EVAL_FORMAT_ESM' ? 'esm' : 'cjs', + target: ['es2020'], + plugins: [ + { + name: 'builtin-externals', + setup(build) { + build.onResolve({ filter: /^(node:|ant:)/ }, args => ({ + path: args.path, + external: true + })); + } + } + ] + }); + + if (output.outputFiles.length !== 1) { + throw new Error(`Expected exactly one bundled output for ${entryPath}`); + } + + return output.outputFiles[0].contents; +} + +function generateHeader(rootDir, bundles) { + const lines = []; + + lines.push('/* Auto-generated builtin bundle data. DO NOT EDIT. */'); + lines.push(''); + lines.push('#ifndef ANT_BUILTIN_BUNDLE_DATA_H'); + lines.push('#define ANT_BUILTIN_BUNDLE_DATA_H'); + lines.push(''); + lines.push('#include '); + lines.push('#include '); + lines.push(''); + + bundles.forEach((bundle, index) => { + const byteLines = []; + for (let i = 0; i < bundle.bytes.length; i += 16) { + byteLines.push(' ' + Array.from(bundle.bytes.slice(i, i + 16)).join(', ')); + } + + lines.push(`/* ${bundle.specifier} <- ${path.relative(rootDir, bundle.entryPath).replaceAll('\\', '/')} */`); + lines.push(`static const uint8_t ant_builtin_bundle_${index}[] = {`); + lines.push(byteLines.join(',\n')); + lines.push('};'); + lines.push(''); + }); + + lines.push('static const ant_builtin_bundle_entry_t ant_builtin_bundle_data[] = {'); + bundles.forEach((bundle, index) => { + lines.push( + ` { ${cString(bundle.specifier)}, ${cString(bundle.specifier)}, ant_builtin_bundle_${index}, sizeof(ant_builtin_bundle_${index}), ${bundle.format} },` + ); + }); + lines.push('};'); + lines.push(''); + lines.push('static const size_t ant_builtin_bundle_data_count ='); + lines.push(' sizeof(ant_builtin_bundle_data) / sizeof(ant_builtin_bundle_data[0]);'); + lines.push(''); + lines.push('#endif'); + + return lines.join('\n') + '\n'; +} + +async function main() { + const args = process.argv.slice(2); + if (args.length < 3) { + console.error(`Usage: ${process.argv[1]} `); + process.exit(1); + } + + const [builtinsRoot, outputFile, ...entryFiles] = args; + const bundles = []; + + for (const entryPath of entryFiles) { + const specifier = toSpecifier(builtinsRoot, entryPath); + const format = toFormat(entryPath); + const bytes = await bundleBuiltin(entryPath, format); + bundles.push({ entryPath, specifier, format, bytes }); + } + + const header = generateHeader(builtinsRoot, bundles); + const totalBundledBytes = bundles.reduce((sum, bundle) => sum + bundle.bytes.length, 0); + + writeFileSync(outputFile, header); + + console.log(`builtin bundle generated successfully: ${outputFile}`); + console.log(` bundled size: ${totalBundledBytes} bytes`); + console.log(` modules: ${bundles.length}`); +} + +main().catch(error => { + console.error(error); + process.exit(1); +}); -- 2.51.2