From d23f3635bd7f89b62a6f05ed8dce657e91a02f2f Mon Sep 17 00:00:00 2001 From: Marco Ippolito Date: Wed, 18 Mar 2026 15:52:10 +0100 Subject: [PATCH 1/2] build: add ts support in core modules --- configure.py | 3 +- node.gyp | 87 ++++++- node.gypi | 8 +- src/node_builtins.cc | 14 +- src/node_builtins.h | 8 + tools/js2c.cc | 137 ++++++++--- tools/typescript_transpiler.cc | 412 +++++++++++++++++++++++++++++++++ tools/typescript_transpiler.h | 34 +++ 8 files changed, 670 insertions(+), 33 deletions(-) create mode 100644 tools/typescript_transpiler.cc create mode 100644 tools/typescript_transpiler.h diff --git a/configure.py b/configure.py index 4a6d43cb789e05..89a201ae578096 100755 --- a/configure.py +++ b/configure.py @@ -1765,7 +1765,8 @@ def gcc_version_ge(version_checked): return True def configure_node_lib_files(o): - o['variables']['node_library_files'] = SearchFiles('lib', 'js') + o['variables']['node_library_files'] = SearchFiles('lib', 'js') + \ + SearchFiles('lib', 'ts') def configure_node_cctest_sources(o): o['variables']['node_cctest_sources'] = [ 'src/node_snapshot_stub.cc' ] + \ diff --git a/node.gyp b/node.gyp index ed699e0d4c03f1..4a244c66135850 100644 --- a/node.gyp +++ b/node.gyp @@ -462,11 +462,59 @@ ], 'node_mksnapshot_exec': '<(PRODUCT_DIR)/<(EXECUTABLE_PREFIX)node_mksnapshot<(EXECUTABLE_SUFFIX)', 'node_js2c_exec': '<(PRODUCT_DIR)/<(EXECUTABLE_PREFIX)node_js2c<(EXECUTABLE_SUFFIX)', + 'node_js2c_snapshot_empty_object_path': '<(PRODUCT_DIR)/obj.host/mksnapshot/deps/v8/src/snapshot/snapshot-empty.o', + 'node_js2c_embedded_empty_object_path': '<(PRODUCT_DIR)/obj.host/mksnapshot/deps/v8/src/snapshot/embedded/embedded-empty.o', 'conditions': [ ['GENERATOR == "ninja"', { - 'node_text_start_object_path': 'src/large_pages/node_text_start.node_text_start.o' + 'node_text_start_object_path': 'src/large_pages/node_text_start.node_text_start.o', + 'conditions': [ + ['want_separate_host_toolset', { + 'conditions': [ + ['OS=="win"', { + 'node_js2c_snapshot_empty_object_path': '<(PRODUCT_DIR)/obj.host/deps/v8/src/snapshot/mksnapshot.snapshot-empty.obj', + 'node_js2c_embedded_empty_object_path': '<(PRODUCT_DIR)/obj.host/deps/v8/src/snapshot/embedded/mksnapshot.embedded-empty.obj', + }, { + 'node_js2c_snapshot_empty_object_path': '<(PRODUCT_DIR)/obj.host/deps/v8/src/snapshot/mksnapshot.snapshot-empty.o', + 'node_js2c_embedded_empty_object_path': '<(PRODUCT_DIR)/obj.host/deps/v8/src/snapshot/embedded/mksnapshot.embedded-empty.o', + }], + ], + }, { + 'conditions': [ + ['OS=="win"', { + 'node_js2c_snapshot_empty_object_path': '<(PRODUCT_DIR)/obj/deps/v8/src/snapshot/mksnapshot.snapshot-empty.obj', + 'node_js2c_embedded_empty_object_path': '<(PRODUCT_DIR)/obj/deps/v8/src/snapshot/embedded/mksnapshot.embedded-empty.obj', + }, { + 'node_js2c_snapshot_empty_object_path': '<(PRODUCT_DIR)/obj/deps/v8/src/snapshot/mksnapshot.snapshot-empty.o', + 'node_js2c_embedded_empty_object_path': '<(PRODUCT_DIR)/obj/deps/v8/src/snapshot/embedded/mksnapshot.embedded-empty.o', + }], + ], + }], + ], }, { - 'node_text_start_object_path': 'node_text_start/src/large_pages/node_text_start.o' + 'node_text_start_object_path': 'node_text_start/src/large_pages/node_text_start.o', + 'conditions': [ + ['GENERATOR == "msvs"', { + 'conditions': [ + ['want_separate_host_toolset', { + 'node_js2c_snapshot_empty_object_path': '$(OutDir)obj\\mksnapshot_host\\deps\\v8\\src\\snapshot\\snapshot-empty.obj', + 'node_js2c_embedded_empty_object_path': '$(OutDir)obj\\mksnapshot_host\\deps\\v8\\src\\snapshot\\embedded\\embedded-empty.obj', + }, { + 'node_js2c_snapshot_empty_object_path': '$(OutDir)obj\\mksnapshot\\deps\\v8\\src\\snapshot\\snapshot-empty.obj', + 'node_js2c_embedded_empty_object_path': '$(OutDir)obj\\mksnapshot\\deps\\v8\\src\\snapshot\\embedded\\embedded-empty.obj', + }], + ], + }, { + 'conditions': [ + ['want_separate_host_toolset', { + 'node_js2c_snapshot_empty_object_path': '<(PRODUCT_DIR)/obj.host/mksnapshot/deps/v8/src/snapshot/snapshot-empty.o', + 'node_js2c_embedded_empty_object_path': '<(PRODUCT_DIR)/obj.host/mksnapshot/deps/v8/src/snapshot/embedded/embedded-empty.o', + }, { + 'node_js2c_snapshot_empty_object_path': '<(PRODUCT_DIR)/obj.target/mksnapshot/deps/v8/src/snapshot/snapshot-empty.o', + 'node_js2c_embedded_empty_object_path': '<(PRODUCT_DIR)/obj.target/mksnapshot/deps/v8/src/snapshot/embedded/embedded-empty.o', + }], + ], + }], + ], }], [ 'node_shared=="true"', { 'node_target_type%': 'shared_library', @@ -1557,12 +1605,27 @@ 'target_name': 'node_js2c', 'type': 'executable', 'toolsets': ['host'], + 'dependencies': [ + 'tools/v8_gypfiles/v8.gyp:v8_base_without_compiler', + 'tools/v8_gypfiles/v8.gyp:v8_compiler_for_mksnapshot', + 'tools/v8_gypfiles/v8.gyp:v8_init', + 'tools/v8_gypfiles/v8.gyp:v8_libbase', + 'tools/v8_gypfiles/v8.gyp:v8_libplatform', + 'tools/v8_gypfiles/v8.gyp:v8_maybe_icu', + 'tools/v8_gypfiles/v8.gyp:v8_pch', + 'tools/v8_gypfiles/v8.gyp:fp16', + 'tools/v8_gypfiles/abseil.gyp:abseil', + ], 'include_dirs': [ + 'deps/v8', + 'deps/v8/include', 'tools', 'src', ], 'sources': [ 'tools/js2c.cc', + 'tools/typescript_transpiler.cc', + 'tools/typescript_transpiler.h', 'tools/executable_wrapper.h', 'src/embedded_data.h', 'src/embedded_data.cc', @@ -1570,6 +1633,26 @@ 'src/builtin_info.cc', ], 'conditions': [ + [ 'want_separate_host_toolset', { + 'dependencies': [ 'tools/v8_gypfiles/v8.gyp:mksnapshot#host' ], + }, { + 'dependencies': [ 'tools/v8_gypfiles/v8.gyp:mksnapshot' ], + }], + [ 'GENERATOR=="msvs"', { + 'msvs_settings': { + 'VCLinkerTool': { + 'AdditionalDependencies': [ + '<(node_js2c_snapshot_empty_object_path)', + '<(node_js2c_embedded_empty_object_path)', + ], + }, + }, + }, { + 'libraries': [ + '<(node_js2c_snapshot_empty_object_path)', + '<(node_js2c_embedded_empty_object_path)', + ], + }], [ 'OS=="mac"', { 'libraries': [ '-framework CoreFoundation -framework Security' ], }], diff --git a/node.gypi b/node.gypi index 64250ed19837e4..c71f12579189f8 100644 --- a/node.gypi +++ b/node.gypi @@ -95,9 +95,15 @@ }], [ 'node_use_bundled_v8=="true"', { 'dependencies': [ - 'tools/v8_gypfiles/v8.gyp:v8_snapshot', 'tools/v8_gypfiles/v8.gyp:v8_libplatform', ], + 'conditions': [ + [ '_target_name!="node_js2c"', { + 'dependencies': [ + 'tools/v8_gypfiles/v8.gyp:v8_snapshot', + ], + }], + ], }], [ 'node_use_v8_platform=="true"', { 'defines': [ diff --git a/src/node_builtins.cc b/src/node_builtins.cc index b02252540d1f3b..1385b34ab1bb98 100644 --- a/src/node_builtins.cc +++ b/src/node_builtins.cc @@ -178,10 +178,12 @@ BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const { } #ifdef NODE_BUILTIN_MODULES_PATH -static std::string OnDiskFileName(const char* id) { +std::string BuiltinLoader::OnDiskFileName(const char* id) const { std::string filename = NODE_BUILTIN_MODULES_PATH; filename += "/"; + // Save the original id for source map lookup before any pointer arithmetic. + const char* original_id = id; if (strncmp(id, "internal/deps", strlen("internal/deps")) == 0) { id += strlen("internal/"); } else { @@ -192,7 +194,15 @@ static std::string OnDiskFileName(const char* id) { // V8 tools scripts are .mjs files. filename += ".mjs"; } else { - filename += ".js"; + // Use the pre-built source map to determine the file extension. + // This avoids a filesystem probe on every module load in dev mode. + auto source = source_.read(); + auto it = source->find(original_id); + if (it != source->end() && it->second.is_typescript) { + filename += ".ts"; + } else { + filename += ".js"; + } } return filename; diff --git a/src/node_builtins.h b/src/node_builtins.h index e4af1f42f4442b..c69c57fdca4ac5 100644 --- a/src/node_builtins.h +++ b/src/node_builtins.h @@ -73,6 +73,7 @@ struct BuiltinSource { std::string id; UnionBytes source; BuiltinSourceType type; + bool is_typescript = false; }; using BuiltinSourceMap = std::map; @@ -203,6 +204,13 @@ class NODE_EXTERN_PRIVATE BuiltinLoader { const BuiltinSource* AddExternalizedBuiltin(const char* id, const char* filename); +#ifdef NODE_BUILTIN_MODULES_PATH + // Returns the on-disk path for a builtin module id. + // Uses the pre-built source map to determine the file extension rather + // than probing the filesystem. + std::string OnDiskFileName(const char* id) const; +#endif + ThreadsafeCopyOnWrite source_; const UnionBytes config_; diff --git a/tools/js2c.cc b/tools/js2c.cc index 2cb09f8e1d7ba6..c242d937821b86 100644 --- a/tools/js2c.cc +++ b/tools/js2c.cc @@ -8,11 +8,13 @@ #include #include #include +#include #include #include "builtin_info.h" #include "embedded_data.h" #include "executable_wrapper.h" #include "simdutf.h" +#include "typescript_transpiler.h" #include "uv.h" #if defined(_WIN32) @@ -131,13 +133,14 @@ bool SearchFiles(const std::string& dir, constexpr std::string_view kMjsSuffix = ".mjs"; constexpr std::string_view kJsSuffix = ".js"; +constexpr std::string_view kTsSuffix = ".ts"; constexpr std::string_view kGypiSuffix = ".gypi"; constexpr std::string_view depsPrefix = "deps/"; constexpr std::string_view libPrefix = "lib/"; constexpr std::string_view HasAllowedExtensions( const std::string_view filename) { - for (const auto& ext : {kGypiSuffix, kJsSuffix, kMjsSuffix}) { + for (const auto& ext : {kGypiSuffix, kJsSuffix, kMjsSuffix, kTsSuffix}) { if (filename.ends_with(ext)) { return ext; } @@ -329,11 +332,13 @@ std::string GetFileId(const std::string& filename) { size_t end = filename.size(); size_t start = 0; std::string prefix; - // Strip .mjs and .js suffix + // Strip .mjs, .js and .ts suffix if (filename.ends_with(kMjsSuffix)) { end -= kMjsSuffix.size(); } else if (filename.ends_with(kJsSuffix)) { end -= kJsSuffix.size(); + } else if (filename.ends_with(kTsSuffix)) { + end -= kTsSuffix.size(); } // deps/acorn/acorn/dist/acorn.js -> internal/deps/acorn/acorn/dist/acorn @@ -670,6 +675,7 @@ Fragment GetDefinition(const std::string& var, const std::vector& code) { } int AddModule(const std::string& filename, + TypeScriptTranspiler* transpiler, Fragments* definitions, Fragments* initializers, Fragments* registrations) { @@ -684,6 +690,21 @@ int AddModule(const std::string& filename, if (error != 0) { return error; } + + if (filename.ends_with(kTsSuffix)) { + std::vector transpiled; + if (transpiler->Strip(std::string_view(code.data(), code.size()), + filename, + &transpiled) != 0) { + fprintf(stderr, + "Failed to transpile TypeScript file %s: %s\n", + filename.c_str(), + std::string(transpiler->LastError()).c_str()); + return 1; + } + code = std::move(transpiled); + } + std::string file_id = GetFileId(filename); std::string var = GetVariableName(file_id); @@ -696,18 +717,20 @@ int AddModule(const std::string& filename, // {"internal/deps/v8/tools/tickprocessor-driver", // BuiltinSource{UnionBytes(&fs_resource), // BuiltinSourceType::kSourceTextModule}}, - Fragment& init_buf = initializers->emplace_back(Fragment(512, 0)); + Fragment& init_buf = initializers->emplace_back(Fragment(640, 0)); int init_size = snprintf(init_buf.data(), init_buf.size(), " {\"%s\"," " BuiltinSource{" " \"%s\"," " UnionBytes(&%s_resource)," - " BuiltinSourceType::%s} },", + " BuiltinSourceType::%s," + " %s} },", file_id.c_str(), file_id.c_str(), var.c_str(), - source_type.c_str()); + source_type.c_str(), + filename.ends_with(kTsSuffix) ? "true" : "false"); init_buf.resize(init_size); // Registrations: @@ -826,23 +849,39 @@ int AddGypi(const std::string& var, int JS2C(const FileList& js_files, const FileList& mjs_files, + const FileList& ts_files, const std::string& config, + const char* argv0, const std::string& dest) { + TypeScriptTranspiler transpiler; Fragments definitions; - definitions.reserve(js_files.size() + mjs_files.size() + 1); + definitions.reserve(js_files.size() + mjs_files.size() + ts_files.size() + 1); Fragments initializers; - initializers.reserve(js_files.size() + mjs_files.size()); + initializers.reserve(js_files.size() + mjs_files.size() + ts_files.size()); Fragments registrations; - registrations.reserve(js_files.size() + mjs_files.size() + 1); + registrations.reserve(js_files.size() + mjs_files.size() + ts_files.size() + + 1); - for (const auto& filename : js_files) { - int r = AddModule(filename, &definitions, &initializers, ®istrations); - if (r != 0) { - return r; - } + if (!ts_files.empty() && transpiler.Initialize(argv0) != 0) { + fprintf(stderr, + "Failed to initialize TypeScript transpiler: %s\n", + std::string(transpiler.LastError()).c_str()); + return 1; } - for (const auto& filename : mjs_files) { - int r = AddModule(filename, &definitions, &initializers, ®istrations); + + auto add_modules = [&](const FileList& files) { + for (const auto& filename : files) { + int r = AddModule( + filename, &transpiler, &definitions, &initializers, ®istrations); + if (r != 0) { + return r; + } + } + return 0; + }; + + for (const auto* files : {&js_files, &mjs_files, &ts_files}) { + int r = add_modules(*files); if (r != 0) { return r; } @@ -910,7 +949,8 @@ int Main(int argc, char* argv[]) { const std::string& file = args[i]; if (IsDirectory(file, &error)) { if (!SearchFiles(file, &file_map, kJsSuffix) || - !SearchFiles(file, &file_map, kMjsSuffix)) { + !SearchFiles(file, &file_map, kMjsSuffix) || + !SearchFiles(file, &file_map, kTsSuffix)) { return 1; } } else if (error != 0) { @@ -927,8 +967,15 @@ int Main(int argc, char* argv[]) { } } - // Should have exactly 3 types: `.js`, `.mjs` and `.gypi`. - assert(file_map.size() == 3); + // Should have at most 4 extension types: `.js`, `.mjs`, `.ts` and `.gypi`. + // Any other extension indicates an unexpected file slipped into the inputs. + if (file_map.size() > 4) { + fprintf(stderr, + "Unexpected file types in inputs (expected .js, .mjs, .ts, " + ".gypi only)\n"); + return 1; + } + auto gypi_it = file_map.find(".gypi"); // Currently config.gypi is the only `.gypi` file allowed if (gypi_it == file_map.end() || gypi_it->second.size() != 1 || @@ -940,19 +987,55 @@ int Main(int argc, char* argv[]) { } auto js_it = file_map.find(".js"); auto mjs_it = file_map.find(".mjs"); - assert(js_it != file_map.end() && mjs_it != file_map.end()); + auto ts_it = file_map.find(".ts"); + if (mjs_it != file_map.end()) { + auto it = std::find(mjs_it->second.begin(), + mjs_it->second.end(), + "lib/eslint.config_partial.mjs"); + if (it != mjs_it->second.end()) { + mjs_it->second.erase(it); + } + } - auto it = std::find(mjs_it->second.begin(), - mjs_it->second.end(), - "lib/eslint.config_partial.mjs"); - if (it != mjs_it->second.end()) { - mjs_it->second.erase(it); + if (js_it != file_map.end()) { + std::sort(js_it->second.begin(), js_it->second.end()); + } + if (mjs_it != file_map.end()) { + std::sort(mjs_it->second.begin(), mjs_it->second.end()); + } + if (ts_it != file_map.end()) { + std::sort(ts_it->second.begin(), ts_it->second.end()); } - std::sort(js_it->second.begin(), js_it->second.end()); - std::sort(mjs_it->second.begin(), mjs_it->second.end()); + static const FileList empty_list; + const FileList& js_files = + js_it == file_map.end() ? empty_list : js_it->second; + const FileList& mjs_files = + mjs_it == file_map.end() ? empty_list : mjs_it->second; + const FileList& ts_files = + ts_it == file_map.end() ? empty_list : ts_it->second; + + // Detect duplicate module IDs across .js/.mjs and .ts file lists. + // GetFileId strips the extension, so lib/foo.ts and lib/foo.js would both + // resolve to the same ID "foo", causing a silent registration conflict. + if (!ts_files.empty()) { + std::unordered_set known_ids; + for (const auto& f : js_files) known_ids.insert(GetFileId(f)); + for (const auto& f : mjs_files) known_ids.insert(GetFileId(f)); + for (const auto& f : ts_files) { + std::string id = GetFileId(f); + if (known_ids.count(id) != 0) { + fprintf(stderr, + "Duplicate module ID '%s': a .ts file and a .js/.mjs file " + "both resolve to the same module ID\n", + id.c_str()); + return 1; + } + } + } - return JS2C(js_it->second, mjs_it->second, gypi_it->second[0], output); + return JS2C( + js_files, mjs_files, ts_files, gypi_it->second[0], argv[0], output); } } // namespace js2c } // namespace node diff --git a/tools/typescript_transpiler.cc b/tools/typescript_transpiler.cc new file mode 100644 index 00000000000000..d138605861489a --- /dev/null +++ b/tools/typescript_transpiler.cc @@ -0,0 +1,412 @@ +#include "typescript_transpiler.h" + +#include +#include +#include +#include + +#include "libplatform/libplatform.h" +#include "uv.h" +#include "v8.h" + +namespace node { +namespace js2c { + +namespace { + +using v8::ArrayBuffer; +using v8::Context; +using v8::Function; +using v8::Global; +using v8::HandleScope; +using v8::Isolate; +using v8::Local; +using v8::Message; +using v8::MaybeLocal; +using v8::Object; +using v8::Platform; +using v8::Script; +using v8::ScriptCompiler; +using v8::ScriptOrigin; +using v8::String; +using v8::TryCatch; +using v8::V8; +using v8::Value; + +constexpr std::string_view kAmaroPath = "deps/amaro/dist/index.js"; + +constexpr char kBootstrapScript[] = R"JS((function(globalThis) { + function encodeUtf8(input) { + return Uint8Array.from( + unescape(encodeURIComponent(String(input))), + (char) => char.charCodeAt(0)); + } + + function decodeUtf8(input) { + let binary = ''; + const chunkSize = 0x8000; + for (let i = 0; i < input.length; i += chunkSize) { + binary += String.fromCharCode.apply( + String, + input.subarray(i, i + chunkSize)); + } + return decodeURIComponent(escape(binary)); + } + + class TextEncoder { + encode(input = '') { + return encodeUtf8(input); + } + } + + class TextDecoder { + constructor(encoding = 'utf-8', options = {}) { + if (encoding !== 'utf-8' || !options.ignoreBOM) { + throw new Error('Unexpected TextDecoder usage'); + } + } + + decode(input = new Uint8Array(), options = undefined) { + if (!(input instanceof Uint8Array) || options !== undefined) { + throw new Error('Unexpected TextDecoder usage'); + } + return decodeUtf8(input); + } + } + + const Buffer = { + from(value, encoding) { + if (encoding === 'base64') { + return Uint8Array.fromBase64(String(value)); + } + throw new Error(`Unsupported Buffer.from encoding: ${encoding}`); + } + }; + + globalThis.require = (id) => { + if (id === 'util') { + return { TextDecoder, TextEncoder }; + } + if (id === 'node:buffer') { + return { Buffer }; + } + throw new Error(`Unsupported require: ${id}`); + }; + + globalThis.module = { exports: {} }; +})(globalThis); +)JS"; + +MaybeLocal ReadUtf8String(Isolate* isolate, std::string_view source) { + return String::NewFromUtf8(isolate, + source.data(), + v8::NewStringType::kNormal, + static_cast(source.size())); +} + +std::string ToUtf8(Isolate* isolate, Local value) { + String::Utf8Value string(isolate, value); + if (*string == nullptr) { + return {}; + } + return std::string(*string, string.length()); +} + +std::string FormatException(Isolate* isolate, TryCatch* try_catch) { + std::string message = ToUtf8(isolate, try_catch->Exception()); + Local exception_message = try_catch->Message(); + if (exception_message.IsEmpty()) { + return message.empty() ? "Unknown exception" : message; + } + std::string location = ToUtf8(isolate, exception_message->Get()); + if (!location.empty()) { + return location; + } + return message.empty() ? "Unknown exception" : message; +} + +int CloseFile(uv_file file) { + uv_fs_t req; + int err = uv_fs_close(nullptr, &req, file, nullptr); + uv_fs_req_cleanup(&req); + return err; +} + +} // namespace + +class TypeScriptTranspiler::Impl { + public: + Impl() = default; + + ~Impl() { + transform_sync_.Reset(); + context_.Reset(); + if (isolate_ != nullptr) { + isolate_->Dispose(); + } + if (v8_initialized_) { + v8::V8::Dispose(); + } + if (platform_initialized_) { + v8::V8::DisposePlatform(); + } + } + + int Initialize(const char* argv0) { + if (isolate_ != nullptr) { + return 0; + } + + last_error_.clear(); + + V8::InitializeICUDefaultLocation(argv0); + V8::InitializeExternalStartupData(argv0); + platform_ = v8::platform::NewDefaultPlatform(); + V8::InitializePlatform(platform_.get()); + platform_initialized_ = true; + V8::Initialize(); + v8_initialized_ = true; + + allocator_.reset(ArrayBuffer::Allocator::NewDefaultAllocator()); + Isolate::CreateParams create_params; + create_params.array_buffer_allocator = allocator_.get(); + isolate_ = Isolate::New(create_params); + + Isolate::Scope isolate_scope(isolate_); + HandleScope handle_scope(isolate_); + Local context = Context::New(isolate_); + context_.Reset(isolate_, context); + Context::Scope context_scope(context); + + int r = RunScript("js2c-amaro-bootstrap", kBootstrapScript); + if (r != 0) { + return r; + } + + std::string source; + if (!ReadFile(kAmaroPath, &source)) { + last_error_ = "Failed to read " + std::string(kAmaroPath); + return 1; + } + r = RunScript(kAmaroPath, source); + if (r != 0) { + return r; + } + + Local global = context->Global(); + Local module_value; + if (!global->Get(context, ReadOnlyString("module")).ToLocal(&module_value) || + !module_value->IsObject()) { + last_error_ = "Failed to load amaro module"; + return 1; + } + + Local module_object = module_value.As(); + Local exports_value; + if (!module_object->Get(context, ReadOnlyString("exports")).ToLocal(&exports_value) || + !exports_value->IsObject()) { + last_error_ = "Failed to access amaro exports"; + return 1; + } + + Local exports_object = exports_value.As(); + Local transform_value; + if (!exports_object->Get(context, ReadOnlyString("transformSync")).ToLocal(&transform_value) || + !transform_value->IsFunction()) { + last_error_ = "Failed to find amaro transformSync"; + return 1; + } + + transform_sync_.Reset(isolate_, transform_value.As()); + return 0; + } + + int Strip(std::string_view source, + std::string_view filename, + std::vector* output) { + if (isolate_ == nullptr) { + last_error_ = "TypeScript transpiler is not initialized"; + return 1; + } + + last_error_.clear(); + + Isolate::Scope isolate_scope(isolate_); + HandleScope handle_scope(isolate_); + Local context = context_.Get(isolate_); + Context::Scope context_scope(context); + TryCatch try_catch(isolate_); + + Local transform = transform_sync_.Get(isolate_); + Local options = Object::New(isolate_); + if (options->Set(context, + ReadOnlyString("mode"), + ReadOnlyString("strip-only")).IsNothing() || + options->Set(context, + ReadOnlyString("filename"), + StringValue(filename)).IsNothing()) { + last_error_ = "Failed to create amaro options"; + return 1; + } + + Local argv[] = { StringValue(source), options }; + Local result; + if (!transform->Call(context, context->Global(), 2, argv).ToLocal(&result)) { + last_error_ = FormatException(isolate_, &try_catch); + return 1; + } + + if (!result->IsObject()) { + last_error_ = "amaro transformSync returned a non-object result"; + return 1; + } + + Local result_object = result.As(); + Local code_value; + if (!result_object->Get(context, ReadOnlyString("code")).ToLocal(&code_value) || + !code_value->IsString()) { + last_error_ = "amaro transformSync returned no code"; + return 1; + } + + std::string stripped = ToUtf8(isolate_, code_value); + output->assign(stripped.begin(), stripped.end()); + return 0; + } + + std::string_view last_error() const { + return last_error_; + } + + private: + bool ReadFile(std::string_view path, std::string* out) { + std::string path_string(path); + uv_fs_t req; + uv_file file = uv_fs_open(nullptr, &req, path_string.c_str(), O_RDONLY, 0, + nullptr); + if (file < 0) { + uv_fs_req_cleanup(&req); + return false; + } + uv_fs_req_cleanup(&req); + + int err = uv_fs_fstat(nullptr, &req, file, nullptr); + if (err < 0) { + uv_fs_req_cleanup(&req); + CloseFile(file); + return false; + } + if (req.statbuf.st_size < 0) { + uv_fs_req_cleanup(&req); + CloseFile(file); + return false; + } + size_t size = static_cast(req.statbuf.st_size); + uv_fs_req_cleanup(&req); + + out->assign(size, '\0'); + if (out->empty()) { + out->clear(); + return CloseFile(file) >= 0; + } + + size_t offset = 0; + while (offset < out->size()) { + uv_buf_t buf = uv_buf_init(out->data() + offset, out->size() - offset); + err = uv_fs_read(nullptr, &req, file, &buf, 1, offset, nullptr); + uv_fs_req_cleanup(&req); + if (err <= 0) { + CloseFile(file); + return false; + } + offset += static_cast(err); + } + + err = uv_fs_close(nullptr, &req, file, nullptr); + uv_fs_req_cleanup(&req); + if (err < 0) { + return false; + } + + return true; + } + + int RunScript(std::string_view name, std::string_view source) { + HandleScope handle_scope(isolate_); + Local context = context_.Get(isolate_); + Context::Scope context_scope(context); + TryCatch try_catch(isolate_); + + Local source_value; + Local name_value; + if (!ReadUtf8String(isolate_, source).ToLocal(&source_value) || + !ReadUtf8String(isolate_, name).ToLocal(&name_value)) { + last_error_ = "Failed to create V8 strings for " + std::string(name); + return 1; + } + + ScriptOrigin origin(name_value); + ScriptCompiler::Source script_source(source_value, origin); + Local