From 4a5d2c7538b412eea84a0f41544784b1b8ed7c8c Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Sat, 25 Jan 2025 03:30:27 +0100 Subject: [PATCH] module: integrate TypeScript into compile cache This integrates TypeScript into the compile cache by caching the transpilation (either type-stripping or transforming) output in addition to the V8 code cache that's generated from the transpilation output. Locally this speeds up loading with type stripping of `benchmark/fixtures/strip-types-benchmark.ts` by ~65% and loading with type transforms of `fixtures/transform-types-benchmark.ts` by ~128%. When comparing loading .ts and loading pre-transpiled .js on-disk with the compile cache enabled, previously .ts loaded 46% slower with type-stripping and 66% slower with transforms compared to loading .js files directly. After this patch, .ts loads 12% slower with type-stripping and 22% slower with transforms compared to .js. (Note that the numbers are based on microbenchmark fixtures and do not necessarily represent real-world workloads, though with bigger real-world files, the speed up should be more significant). PR-URL: https://github.com/nodejs/node/pull/56629 Fixes: https://github.com/nodejs/node/issues/54741 Reviewed-By: Geoffrey Booth Reviewed-By: Marco Ippolito Reviewed-By: James M Snell --- lib/internal/modules/typescript.js | 63 ++++++- src/compile_cache.cc | 65 ++++++- src/compile_cache.h | 15 +- src/node_modules.cc | 96 ++++++++++ .../test-compile-cache-typescript-commonjs.js | 166 +++++++++++++++++ .../test-compile-cache-typescript-esm.js | 167 ++++++++++++++++++ ...est-compile-cache-typescript-strip-miss.js | 104 +++++++++++ ...mpile-cache-typescript-strip-sourcemaps.js | 59 +++++++ ...test-compile-cache-typescript-transform.js | 127 +++++++++++++ 9 files changed, 846 insertions(+), 16 deletions(-) create mode 100644 test/parallel/test-compile-cache-typescript-commonjs.js create mode 100644 test/parallel/test-compile-cache-typescript-esm.js create mode 100644 test/parallel/test-compile-cache-typescript-strip-miss.js create mode 100644 test/parallel/test-compile-cache-typescript-strip-sourcemaps.js create mode 100644 test/parallel/test-compile-cache-typescript-transform.js diff --git a/lib/internal/modules/typescript.js b/lib/internal/modules/typescript.js index 6abfc707657b92..17bbc6ba944432 100644 --- a/lib/internal/modules/typescript.js +++ b/lib/internal/modules/typescript.js @@ -22,6 +22,11 @@ const { const { getOptionValue } = require('internal/options'); const assert = require('internal/assert'); const { Buffer } = require('buffer'); +const { + getCompileCacheEntry, + saveCompileCacheEntry, + cachedCodeTypes: { kStrippedTypeScript, kTransformedTypeScript, kTransformedTypeScriptWithSourceMaps }, +} = internalBinding('modules'); /** * The TypeScript parsing mode, either 'strip-only' or 'transform'. @@ -105,11 +110,19 @@ function stripTypeScriptTypes(code, options = kEmptyObject) { }); } +/** + * @typedef {'strip-only' | 'transform'} TypeScriptMode + * @typedef {object} TypeScriptOptions + * @property {TypeScriptMode} mode Mode. + * @property {boolean} sourceMap Whether to generate source maps. + * @property {string|undefined} filename Filename. + */ + /** * Processes TypeScript code by stripping types or transforming. * Handles source maps if needed. * @param {string} code TypeScript code to process. - * @param {object} options The configuration object. + * @param {TypeScriptOptions} options The configuration object. * @returns {string} The processed code. */ function processTypeScriptCode(code, options) { @@ -126,6 +139,20 @@ function processTypeScriptCode(code, options) { return transformedCode; } +/** + * Get the type enum used for compile cache. + * @param {TypeScriptMode} mode Mode of transpilation. + * @param {boolean} sourceMap Whether source maps are enabled. + * @returns {number} + */ +function getCachedCodeType(mode, sourceMap) { + if (mode === 'transform') { + if (sourceMap) { return kTransformedTypeScriptWithSourceMaps; } + return kTransformedTypeScript; + } + return kStrippedTypeScript; +} + /** * Performs type-stripping to TypeScript source code internally. * It is used by internal loaders. @@ -142,12 +169,40 @@ function stripTypeScriptModuleTypes(source, filename, emitWarning = true) { if (isUnderNodeModules(filename)) { throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename); } + const sourceMap = getOptionValue('--enable-source-maps'); + + const mode = getTypeScriptParsingMode(); + + // Instead of caching the compile cache status, just go into C++ to fetch it, + // as checking process.env equally involves calling into C++ anyway, and + // the compile cache can be enabled dynamically. + const type = getCachedCodeType(mode, sourceMap); + // Get a compile cache entry into the native compile cache store, + // keyed by the filename. If the cache can already be loaded on disk, + // cached.transpiled contains the cached string. Otherwise we should do + // the transpilation and save it in the native store later using + // saveCompileCacheEntry(). + const cached = (filename ? getCompileCacheEntry(source, filename, type) : undefined); + if (cached?.transpiled) { // TODO(joyeecheung): return Buffer here. + return cached.transpiled; + } + const options = { - mode: getTypeScriptParsingMode(), - sourceMap: getOptionValue('--enable-source-maps'), + mode, + sourceMap, filename, }; - return processTypeScriptCode(source, options); + + const transpiled = processTypeScriptCode(source, options); + if (cached) { + // cached.external contains a pointer to the native cache entry. + // The cached object would be unreachable once it's out of scope, + // but the pointer inside cached.external would stay around for reuse until + // environment shutdown or when the cache is manually flushed + // to disk. Unwrap it in JS before passing into C++ since it's faster. + saveCompileCacheEntry(cached.external, transpiled); + } + return transpiled; } /** diff --git a/src/compile_cache.cc b/src/compile_cache.cc index 50697bcfe1671d..f13797e5f50288 100644 --- a/src/compile_cache.cc +++ b/src/compile_cache.cc @@ -77,10 +77,27 @@ v8::ScriptCompiler::CachedData* CompileCacheEntry::CopyCache() const { // See comments in CompileCacheHandler::Persist(). constexpr uint32_t kCacheMagicNumber = 0x8adfdbb2; +const char* CompileCacheEntry::type_name() const { + switch (type) { + case CachedCodeType::kCommonJS: + return "CommonJS"; + case CachedCodeType::kESM: + return "ESM"; + case CachedCodeType::kStrippedTypeScript: + return "StrippedTypeScript"; + case CachedCodeType::kTransformedTypeScript: + return "TransformedTypeScript"; + case CachedCodeType::kTransformedTypeScriptWithSourceMaps: + return "TransformedTypeScriptWithSourceMaps"; + default: + UNREACHABLE(); + } +} + void CompileCacheHandler::ReadCacheFile(CompileCacheEntry* entry) { Debug("[compile cache] reading cache from %s for %s %s...", entry->cache_filename, - entry->type == CachedCodeType::kCommonJS ? "CommonJS" : "ESM", + entry->type_name(), entry->source_filename); uv_fs_t req; @@ -256,7 +273,8 @@ void CompileCacheHandler::MaybeSaveImpl(CompileCacheEntry* entry, v8::Local func_or_mod, bool rejected) { DCHECK_NOT_NULL(entry); - Debug("[compile cache] cache for %s was %s, ", + Debug("[compile cache] V8 code cache for %s %s was %s, ", + entry->type_name(), entry->source_filename, rejected ? "rejected" : (entry->cache == nullptr) ? "not initialized" @@ -287,6 +305,25 @@ void CompileCacheHandler::MaybeSave(CompileCacheEntry* entry, MaybeSaveImpl(entry, func, rejected); } +void CompileCacheHandler::MaybeSave(CompileCacheEntry* entry, + std::string_view transpiled) { + CHECK(entry->type == CachedCodeType::kStrippedTypeScript || + entry->type == CachedCodeType::kTransformedTypeScript || + entry->type == CachedCodeType::kTransformedTypeScriptWithSourceMaps); + Debug("[compile cache] saving transpilation cache for %s %s\n", + entry->type_name(), + entry->source_filename); + + // TODO(joyeecheung): it's weird to copy it again here. Convert the v8::String + // directly into buffer held by v8::ScriptCompiler::CachedData here. + int cache_size = static_cast(transpiled.size()); + uint8_t* data = new uint8_t[cache_size]; + memcpy(data, transpiled.data(), cache_size); + entry->cache.reset(new v8::ScriptCompiler::CachedData( + data, cache_size, v8::ScriptCompiler::CachedData::BufferOwned)); + entry->refreshed = true; +} + /** * Persist the compile cache accumulated in memory to disk. * @@ -316,18 +353,25 @@ void CompileCacheHandler::Persist() { // incur a negligible overhead from thread synchronization. for (auto& pair : compiler_cache_store_) { auto* entry = pair.second.get(); + const char* type_name = entry->type_name(); if (entry->cache == nullptr) { - Debug("[compile cache] skip %s because the cache was not initialized\n", + Debug("[compile cache] skip persisting %s %s because the cache was not " + "initialized\n", + type_name, entry->source_filename); continue; } if (entry->refreshed == false) { - Debug("[compile cache] skip %s because cache was the same\n", - entry->source_filename); + Debug( + "[compile cache] skip persisting %s %s because cache was the same\n", + type_name, + entry->source_filename); continue; } if (entry->persisted == true) { - Debug("[compile cache] skip %s because cache was already persisted\n", + Debug("[compile cache] skip persisting %s %s because cache was already " + "persisted\n", + type_name, entry->source_filename); continue; } @@ -363,8 +407,9 @@ void CompileCacheHandler::Persist() { auto cleanup_mkstemp = OnScopeLeave([&mkstemp_req]() { uv_fs_req_cleanup(&mkstemp_req); }); std::string cache_filename_tmp = entry->cache_filename + ".XXXXXX"; - Debug("[compile cache] Creating temporary file for cache of %s...", - entry->source_filename); + Debug("[compile cache] Creating temporary file for cache of %s (%s)...", + entry->source_filename, + type_name); int err = uv_fs_mkstemp( nullptr, &mkstemp_req, cache_filename_tmp.c_str(), nullptr); if (err < 0) { @@ -372,8 +417,10 @@ void CompileCacheHandler::Persist() { continue; } Debug(" -> %s\n", mkstemp_req.path); - Debug("[compile cache] writing cache for %s to temporary file %s [%d %d %d " + Debug("[compile cache] writing cache for %s %s to temporary file %s [%d " + "%d %d " "%d %d]...", + type_name, entry->source_filename, mkstemp_req.path, headers[kMagicNumberOffset], diff --git a/src/compile_cache.h b/src/compile_cache.h index a7bb58c4a0be95..72910084e18bca 100644 --- a/src/compile_cache.h +++ b/src/compile_cache.h @@ -13,10 +13,17 @@ namespace node { class Environment; -// TODO(joyeecheung): move it into a CacheHandler class. +#define CACHED_CODE_TYPES(V) \ + V(kCommonJS, 0) \ + V(kESM, 1) \ + V(kStrippedTypeScript, 2) \ + V(kTransformedTypeScript, 3) \ + V(kTransformedTypeScriptWithSourceMaps, 4) + enum class CachedCodeType : uint8_t { - kCommonJS = 0, - kESM, +#define V(type, value) type = value, + CACHED_CODE_TYPES(V) +#undef V }; struct CompileCacheEntry { @@ -34,6 +41,7 @@ struct CompileCacheEntry { // Copy the cache into a new store for V8 to consume. Caller takes // ownership. v8::ScriptCompiler::CachedData* CopyCache() const; + const char* type_name() const; }; #define COMPILE_CACHE_STATUS(V) \ @@ -70,6 +78,7 @@ class CompileCacheHandler { void MaybeSave(CompileCacheEntry* entry, v8::Local mod, bool rejected); + void MaybeSave(CompileCacheEntry* entry, std::string_view transpiled); std::string_view cache_dir() { return compile_cache_dir_; } private: diff --git a/src/node_modules.cc b/src/node_modules.cc index 4b522a91323c9f..85c8e21cf026ff 100644 --- a/src/node_modules.cc +++ b/src/node_modules.cc @@ -1,6 +1,7 @@ #include "node_modules.h" #include #include "base_object-inl.h" +#include "compile_cache.h" #include "node_errors.h" #include "node_external_reference.h" #include "node_url.h" @@ -21,12 +22,16 @@ namespace modules { using v8::Array; using v8::Context; +using v8::External; using v8::FunctionCallbackInfo; using v8::HandleScope; +using v8::Integer; using v8::Isolate; using v8::Local; using v8::LocalVector; +using v8::Name; using v8::NewStringType; +using v8::Null; using v8::Object; using v8::ObjectTemplate; using v8::Primitive; @@ -498,6 +503,74 @@ void GetCompileCacheDir(const FunctionCallbackInfo& args) { .ToLocalChecked()); } +void GetCompileCacheEntry(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + CHECK(args[0]->IsString()); // TODO(joyeecheung): accept buffer. + CHECK(args[1]->IsString()); + CHECK(args[2]->IsUint32()); + Local context = isolate->GetCurrentContext(); + Environment* env = Environment::GetCurrent(context); + if (!env->use_compile_cache()) { + return; + } + Local source = args[0].As(); + Local filename = args[1].As(); + CachedCodeType type = + static_cast(args[2].As()->Value()); + auto* cache_entry = + env->compile_cache_handler()->GetOrInsert(source, filename, type); + if (cache_entry == nullptr) { + return; + } + + v8::LocalVector names(isolate, + {FIXED_ONE_BYTE_STRING(isolate, "external")}); + v8::LocalVector values(isolate, + {v8::External::New(isolate, cache_entry)}); + if (cache_entry->cache != nullptr) { + Debug(env, + DebugCategory::COMPILE_CACHE, + "[compile cache] retrieving transpile cache for %s %s...", + cache_entry->type_name(), + cache_entry->source_filename); + + std::string_view cache( + reinterpret_cast(cache_entry->cache->data), + cache_entry->cache->length); + Local transpiled; + // TODO(joyeecheung): convert with simdutf and into external strings + if (!ToV8Value(context, cache).ToLocal(&transpiled)) { + Debug(env, DebugCategory::COMPILE_CACHE, "failed\n"); + return; + } else { + Debug(env, DebugCategory::COMPILE_CACHE, "success\n"); + } + names.push_back(FIXED_ONE_BYTE_STRING(isolate, "transpiled")); + values.push_back(transpiled); + } else { + Debug(env, + DebugCategory::COMPILE_CACHE, + "[compile cache] no transpile cache for %s %s\n", + cache_entry->type_name(), + cache_entry->source_filename); + } + args.GetReturnValue().Set(Object::New( + isolate, v8::Null(isolate), names.data(), values.data(), names.size())); +} + +void SaveCompileCacheEntry(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + Local context = isolate->GetCurrentContext(); + Environment* env = Environment::GetCurrent(context); + DCHECK(env->use_compile_cache()); + CHECK(args[0]->IsExternal()); + CHECK(args[1]->IsString()); // TODO(joyeecheung): accept buffer. + auto* cache_entry = + static_cast(args[0].As()->Value()); + Utf8Value utf8(isolate, args[1].As()); + env->compile_cache_handler()->MaybeSave(cache_entry, utf8.ToStringView()); +} + void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data, Local target) { Isolate* isolate = isolate_data->isolate(); @@ -514,6 +587,8 @@ void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data, SetMethod(isolate, target, "enableCompileCache", EnableCompileCache); SetMethod(isolate, target, "getCompileCacheDir", GetCompileCacheDir); SetMethod(isolate, target, "flushCompileCache", FlushCompileCache); + SetMethod(isolate, target, "getCompileCacheEntry", GetCompileCacheEntry); + SetMethod(isolate, target, "saveCompileCacheEntry", SaveCompileCacheEntry); } void BindingData::CreatePerContextProperties(Local target, @@ -530,12 +605,31 @@ void BindingData::CreatePerContextProperties(Local target, compile_cache_status_values.push_back( \ FIXED_ONE_BYTE_STRING(isolate, #status)); COMPILE_CACHE_STATUS(V) +#undef V USE(target->Set(context, FIXED_ONE_BYTE_STRING(isolate, "compileCacheStatus"), Array::New(isolate, compile_cache_status_values.data(), compile_cache_status_values.size()))); + + LocalVector cached_code_type_keys(isolate); + LocalVector cached_code_type_values(isolate); + +#define V(type, value) \ + cached_code_type_keys.push_back(FIXED_ONE_BYTE_STRING(isolate, #type)); \ + cached_code_type_values.push_back(Integer::New(isolate, value)); \ + DCHECK_EQ(value, cached_code_type_values.size() - 1); + CACHED_CODE_TYPES(V) +#undef V + + USE(target->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "cachedCodeTypes"), + Object::New(isolate, + Null(isolate), + cached_code_type_keys.data(), + cached_code_type_values.data(), + cached_code_type_keys.size()))); } void BindingData::RegisterExternalReferences( @@ -547,6 +641,8 @@ void BindingData::RegisterExternalReferences( registry->Register(EnableCompileCache); registry->Register(GetCompileCacheDir); registry->Register(FlushCompileCache); + registry->Register(GetCompileCacheEntry); + registry->Register(SaveCompileCacheEntry); } } // namespace modules diff --git a/test/parallel/test-compile-cache-typescript-commonjs.js b/test/parallel/test-compile-cache-typescript-commonjs.js new file mode 100644 index 00000000000000..b6c4581ed47be3 --- /dev/null +++ b/test/parallel/test-compile-cache-typescript-commonjs.js @@ -0,0 +1,166 @@ +'use strict'; + +// This tests NODE_COMPILE_CACHE works for CommonJS with types. + +require('../common'); +const { spawnSyncAndAssert } = require('../common/child_process'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); + +// Check cache for .ts files that would be run as CommonJS. +{ + tmpdir.refresh(); + const dir = tmpdir.resolve('.compile_cache_dir'); + const script = fixtures.path('typescript', 'ts', 'test-commonjs-parsing.ts'); + + spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /saving transpilation cache for StrippedTypeScript .*test-commonjs-parsing\.ts/); + assert.match(output, /writing cache for StrippedTypeScript .*test-commonjs-parsing\.ts.*success/); + assert.match(output, /writing cache for CommonJS .*test-commonjs-parsing\.ts.*success/); + return true; + } + }); + + spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-commonjs-parsing\.ts.*success/); + assert.match(output, /reading cache from .* for CommonJS .*test-commonjs-parsing\.ts.*success/); + assert.match(output, /skip persisting StrippedTypeScript .*test-commonjs-parsing\.ts because cache was the same/); + assert.match(output, /V8 code cache for CommonJS .*test-commonjs-parsing\.ts was accepted, keeping the in-memory entry/); + assert.match(output, /skip persisting CommonJS .*test-commonjs-parsing\.ts because cache was the same/); + return true; + } + }); +} + +// Check cache for .cts files that require .cts files. +{ + tmpdir.refresh(); + const dir = tmpdir.resolve('.compile_cache_dir'); + const script = fixtures.path('typescript', 'cts', 'test-require-commonjs.cts'); + + spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /writing cache for StrippedTypeScript .*test-require-commonjs\.cts.*success/); + assert.match(output, /writing cache for StrippedTypeScript .*test-cts-export-foo\.cts.*success/); + assert.match(output, /writing cache for CommonJS .*test-require-commonjs\.cts.*success/); + assert.match(output, /writing cache for CommonJS .*test-cts-export-foo\.cts.*success/); + return true; + } + }); + + spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-require-commonjs\.cts.*success/); + assert.match(output, /skip persisting StrippedTypeScript .*test-require-commonjs\.cts because cache was the same/); + assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-cts-export-foo\.cts.*success/); + assert.match(output, /skip persisting StrippedTypeScript .*test-cts-export-foo\.cts because cache was the same/); + + assert.match(output, /V8 code cache for CommonJS .*test-require-commonjs\.cts was accepted, keeping the in-memory entry/); + assert.match(output, /skip persisting CommonJS .*test-require-commonjs\.cts because cache was the same/); + assert.match(output, /V8 code cache for CommonJS .*test-cts-export-foo\.cts was accepted, keeping the in-memory entry/); + assert.match(output, /skip persisting CommonJS .*test-cts-export-foo\.cts because cache was the same/); + return true; + } + }); +} + +// Check cache for .cts files that require .mts files. +{ + tmpdir.refresh(); + const dir = tmpdir.resolve('.compile_cache_dir'); + const script = fixtures.path('typescript', 'cts', 'test-require-mts-module.cts'); + + spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /writing cache for StrippedTypeScript .*test-require-mts-module\.cts.*success/); + assert.match(output, /writing cache for StrippedTypeScript .*test-mts-export-foo\.mts.*success/); + assert.match(output, /writing cache for CommonJS .*test-require-mts-module\.cts.*success/); + assert.match(output, /writing cache for ESM .*test-mts-export-foo\.mts.*success/); + return true; + } + }); + + spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-require-mts-module\.cts.*success/); + assert.match(output, /skip persisting StrippedTypeScript .*test-require-mts-module\.cts because cache was the same/); + assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-mts-export-foo\.mts.*success/); + assert.match(output, /skip persisting StrippedTypeScript .*test-mts-export-foo\.mts because cache was the same/); + + assert.match(output, /V8 code cache for CommonJS .*test-require-mts-module\.cts was accepted, keeping the in-memory entry/); + assert.match(output, /skip persisting CommonJS .*test-require-mts-module\.cts because cache was the same/); + assert.match(output, /V8 code cache for ESM .*test-mts-export-foo\.mts was accepted, keeping the in-memory entry/); + assert.match(output, /skip persisting ESM .*test-mts-export-foo\.mts because cache was the same/); + return true; + } + }); +} diff --git a/test/parallel/test-compile-cache-typescript-esm.js b/test/parallel/test-compile-cache-typescript-esm.js new file mode 100644 index 00000000000000..cec7b814da6679 --- /dev/null +++ b/test/parallel/test-compile-cache-typescript-esm.js @@ -0,0 +1,167 @@ +'use strict'; + +// This tests NODE_COMPILE_CACHE works for ESM with types. + +require('../common'); +const { spawnSyncAndAssert } = require('../common/child_process'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); + +// Check cache for .ts files that would be run as ESM. +{ + tmpdir.refresh(); + const dir = tmpdir.resolve('.compile_cache_dir'); + const script = fixtures.path('typescript', 'ts', 'test-module-typescript.ts'); + + spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /saving transpilation cache for StrippedTypeScript .*test-module-typescript\.ts/); + assert.match(output, /writing cache for StrippedTypeScript .*test-module-typescript\.ts.*success/); + assert.match(output, /writing cache for ESM .*test-module-typescript\.ts.*success/); + return true; + } + }); + + spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-module-typescript\.ts.*success/); + assert.match(output, /reading cache from .* for ESM .*test-module-typescript\.ts.*success/); + assert.match(output, /skip persisting StrippedTypeScript .*test-module-typescript\.ts because cache was the same/); + assert.match(output, /V8 code cache for ESM .*test-module-typescript\.ts was accepted, keeping the in-memory entry/); + assert.match(output, /skip persisting ESM .*test-module-typescript\.ts because cache was the same/); + return true; + } + }); +} + +// Check cache for .mts files that import .mts files. +{ + tmpdir.refresh(); + const dir = tmpdir.resolve('.compile_cache_dir'); + const script = fixtures.path('typescript', 'mts', 'test-import-module.mts'); + + spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /writing cache for StrippedTypeScript .*test-import-module\.mts.*success/); + assert.match(output, /writing cache for StrippedTypeScript .*test-mts-export-foo\.mts.*success/); + assert.match(output, /writing cache for ESM .*test-import-module\.mts.*success/); + assert.match(output, /writing cache for ESM .*test-mts-export-foo\.mts.*success/); + return true; + } + }); + + spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-import-module\.mts.*success/); + assert.match(output, /skip persisting StrippedTypeScript .*test-import-module\.mts because cache was the same/); + assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-mts-export-foo\.mts.*success/); + assert.match(output, /skip persisting StrippedTypeScript .*test-mts-export-foo\.mts because cache was the same/); + + assert.match(output, /V8 code cache for ESM .*test-import-module\.mts was accepted, keeping the in-memory entry/); + assert.match(output, /skip persisting ESM .*test-import-module\.mts because cache was the same/); + assert.match(output, /V8 code cache for ESM .*test-mts-export-foo\.mts was accepted, keeping the in-memory entry/); + assert.match(output, /skip persisting ESM .*test-mts-export-foo\.mts because cache was the same/); + return true; + } + }); +} + + +// Check cache for .mts files that import .cts files. +{ + tmpdir.refresh(); + const dir = tmpdir.resolve('.compile_cache_dir'); + const script = fixtures.path('typescript', 'mts', 'test-import-commonjs.mts'); + + spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /writing cache for StrippedTypeScript .*test-import-commonjs\.mts.*success/); + assert.match(output, /writing cache for StrippedTypeScript .*test-cts-export-foo\.cts.*success/); + assert.match(output, /writing cache for ESM .*test-import-commonjs\.mts.*success/); + assert.match(output, /writing cache for CommonJS .*test-cts-export-foo\.cts.*success/); + return true; + } + }); + + spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-import-commonjs\.mts.*success/); + assert.match(output, /skip persisting StrippedTypeScript .*test-import-commonjs\.mts because cache was the same/); + assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-cts-export-foo\.cts.*success/); + assert.match(output, /skip persisting StrippedTypeScript .*test-cts-export-foo\.cts because cache was the same/); + + assert.match(output, /V8 code cache for ESM .*test-import-commonjs\.mts was accepted, keeping the in-memory entry/); + assert.match(output, /skip persisting ESM .*test-import-commonjs\.mts because cache was the same/); + assert.match(output, /V8 code cache for CommonJS .*test-cts-export-foo\.cts was accepted, keeping the in-memory entry/); + assert.match(output, /skip persisting CommonJS .*test-cts-export-foo\.cts because cache was the same/); + return true; + } + }); +} diff --git a/test/parallel/test-compile-cache-typescript-strip-miss.js b/test/parallel/test-compile-cache-typescript-strip-miss.js new file mode 100644 index 00000000000000..5d37a377f002e4 --- /dev/null +++ b/test/parallel/test-compile-cache-typescript-strip-miss.js @@ -0,0 +1,104 @@ +'use strict'; + +// This tests NODE_COMPILE_CACHE can handle cache invalidation +// between strip-only TypeScript and transformed TypeScript. + +require('../common'); +const { spawnSyncAndAssert } = require('../common/child_process'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); + +tmpdir.refresh(); +const dir = tmpdir.resolve('.compile_cache_dir'); +const script = fixtures.path('typescript', 'ts', 'test-typescript.ts'); + +spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /saving transpilation cache for StrippedTypeScript .*test-typescript\.ts/); + assert.match(output, /writing cache for StrippedTypeScript .*test-typescript\.ts.*success/); + assert.match(output, /writing cache for CommonJS .*test-typescript\.ts.*success/); + return true; + } + }); + +// Reloading with transform should miss the cache generated without transform. +spawnSyncAndAssert( + process.execPath, + ['--experimental-transform-types', script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + // Both the transpile cache and the code cache should be missed. + assert.match(output, /no transpile cache for TransformedTypeScriptWithSourceMaps .*test-typescript\.ts/); + assert.match(output, /reading cache from .* for CommonJS .*test-typescript\.ts.*mismatch/); + // New cache with source map should be generated. + assert.match(output, /writing cache for TransformedTypeScriptWithSourceMaps .*test-typescript\.ts.*success/); + assert.match(output, /writing cache for CommonJS .*test-typescript\.ts.*success/); + return true; + } + }); + +// Reloading with transform should hit the cache generated with transform. +spawnSyncAndAssert( + process.execPath, + ['--experimental-transform-types', script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /retrieving transpile cache for TransformedTypeScriptWithSourceMaps .*test-typescript\.ts.*success/); + assert.match(output, /reading cache from .* for CommonJS .*test-typescript\.ts.*success/); + assert.match(output, /skip persisting TransformedTypeScriptWithSourceMaps .*test-typescript\.ts because cache was the same/); + assert.match(output, /V8 code cache for CommonJS .*test-typescript\.ts was accepted, keeping the in-memory entry/); + assert.match(output, /skip persisting CommonJS .*test-typescript\.ts because cache was the same/); + return true; + } + }); + +// Reloading without transform should hit the co-existing transpile cache generated without transform, +// but miss the code cache generated with transform. +spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-typescript\.ts.*success/); + assert.match(output, /reading cache from .* for CommonJS .*test-typescript\.ts.*mismatch/); + assert.match(output, /skip persisting StrippedTypeScript .*test-typescript\.ts because cache was the same/); + assert.match(output, /writing cache for CommonJS .*test-typescript\.ts.*success/); + return true; + } + }); diff --git a/test/parallel/test-compile-cache-typescript-strip-sourcemaps.js b/test/parallel/test-compile-cache-typescript-strip-sourcemaps.js new file mode 100644 index 00000000000000..da5e350496f005 --- /dev/null +++ b/test/parallel/test-compile-cache-typescript-strip-sourcemaps.js @@ -0,0 +1,59 @@ +'use strict'; + +// This tests NODE_COMPILE_CACHE can be used for type stripping and ignores +// --enable-source-maps as there's no difference in the code generated. + +require('../common'); +const { spawnSyncAndAssert } = require('../common/child_process'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); + +tmpdir.refresh(); +const dir = tmpdir.resolve('.compile_cache_dir'); +const script = fixtures.path('typescript', 'ts', 'test-typescript.ts'); + +spawnSyncAndAssert( + process.execPath, + [script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /saving transpilation cache for StrippedTypeScript .*test-typescript\.ts/); + assert.match(output, /writing cache for StrippedTypeScript .*test-typescript\.ts.*success/); + assert.match(output, /writing cache for CommonJS .*test-typescript\.ts.*success/); + return true; + } + }); + +// Reloading with source maps should hit the cache generated without source maps, because for +// type stripping, only sourceURL is added regardless of whether source map is enabled. +spawnSyncAndAssert( + process.execPath, + ['--enable-source-maps', script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + // Both the transpile cache and the code cache should be missed. + assert.match(output, /retrieving transpile cache for StrippedTypeScript .*test-typescript\.ts.*success/); + assert.match(output, /reading cache from .* for CommonJS .*test-typescript\.ts.*success/); + assert.match(output, /skip persisting StrippedTypeScript .*test-typescript\.ts because cache was the same/); + assert.match(output, /V8 code cache for CommonJS .*test-typescript\.ts was accepted, keeping the in-memory entry/); + assert.match(output, /skip persisting CommonJS .*test-typescript\.ts because cache was the same/); + return true; + } + }); diff --git a/test/parallel/test-compile-cache-typescript-transform.js b/test/parallel/test-compile-cache-typescript-transform.js new file mode 100644 index 00000000000000..41eb67b203baa1 --- /dev/null +++ b/test/parallel/test-compile-cache-typescript-transform.js @@ -0,0 +1,127 @@ +'use strict'; + +// This tests NODE_COMPILE_CACHE works with --experimental-transform-types. + +require('../common'); +const { spawnSyncAndAssert } = require('../common/child_process'); +const assert = require('assert'); +const tmpdir = require('../common/tmpdir'); +const fixtures = require('../common/fixtures'); + + +tmpdir.refresh(); +const dir = tmpdir.resolve('.compile_cache_dir'); +const script = fixtures.path('typescript', 'ts', 'transformation', 'test-enum.ts'); + +// Check --experimental-transform-types which enables source maps by default. +spawnSyncAndAssert( + process.execPath, + ['--experimental-transform-types', script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /saving transpilation cache for TransformedTypeScriptWithSourceMaps .*test-enum\.ts/); + assert.match(output, /writing cache for TransformedTypeScriptWithSourceMaps .*test-enum\.ts.*success/); + assert.match(output, /writing cache for CommonJS .*test-enum\.ts.*success/); + return true; + } + }); + +spawnSyncAndAssert( + process.execPath, + ['--experimental-transform-types', script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /retrieving transpile cache for TransformedTypeScriptWithSourceMaps .*test-enum\.ts.*success/); + assert.match(output, /reading cache from .* for CommonJS .*test-enum\.ts.*success/); + assert.match(output, /skip persisting TransformedTypeScriptWithSourceMaps .*test-enum\.ts because cache was the same/); + assert.match(output, /V8 code cache for CommonJS .*test-enum\.ts was accepted, keeping the in-memory entry/); + assert.match(output, /skip persisting CommonJS .*test-enum\.ts because cache was the same/); + return true; + } + }); + +// Reloading without source maps should miss the cache generated with source maps. +spawnSyncAndAssert( + process.execPath, + ['--experimental-transform-types', '--no-enable-source-maps', script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + // Both the transpile cache and the code cache should be missed. + assert.match(output, /no transpile cache for TransformedTypeScript .*test-enum\.ts/); + assert.match(output, /reading cache from .* for CommonJS .*test-enum\.ts.*mismatch/); + // New cache without source map should be generated. + assert.match(output, /writing cache for TransformedTypeScript .*test-enum\.ts.*success/); + assert.match(output, /writing cache for CommonJS .*test-enum\.ts.*success/); + return true; + } + }); + +// Reloading without source maps again should hit the cache generated without source maps. +spawnSyncAndAssert( + process.execPath, + ['--experimental-transform-types', '--no-enable-source-maps', script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /retrieving transpile cache for TransformedTypeScript .*test-enum\.ts.*success/); + assert.match(output, /reading cache from .* for CommonJS .*test-enum\.ts.*success/); + assert.match(output, /skip persisting TransformedTypeScript .*test-enum\.ts because cache was the same/); + assert.match(output, /V8 code cache for CommonJS .*test-enum\.ts was accepted, keeping the in-memory entry/); + assert.match(output, /skip persisting CommonJS .*test-enum\.ts because cache was the same/); + return true; + } + }); + +// Reloading with source maps again should hit the co-existing transpile cache with source +// maps, but miss the code cache generated without source maps. +spawnSyncAndAssert( + process.execPath, + ['--experimental-transform-types', script], + { + env: { + ...process.env, + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', + NODE_COMPILE_CACHE: dir + }, + cwd: tmpdir.path + }, + { + stderr(output) { + assert.match(output, /retrieving transpile cache for TransformedTypeScriptWithSourceMaps .*test-enum\.ts.*success/); + assert.match(output, /reading cache from .* for CommonJS .*test-enum\.ts.*mismatch/); + assert.match(output, /skip persisting TransformedTypeScriptWithSourceMaps .*test-enum\.ts because cache was the same/); + assert.match(output, /writing cache for CommonJS .*test-enum\.ts.*success/); + return true; + } + });