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 <webadmin@geoffreybooth.com>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Joyee Cheung
2025-01-25 03:30:27 +01:00
committed by GitHub
parent f07300cfa3
commit 4a5d2c7538
9 changed files with 846 additions and 16 deletions

View File

@@ -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;
}
/**

View File

@@ -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<T> 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<int>(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],

View File

@@ -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<v8::Module> mod,
bool rejected);
void MaybeSave(CompileCacheEntry* entry, std::string_view transpiled);
std::string_view cache_dir() { return compile_cache_dir_; }
private:

View File

@@ -1,6 +1,7 @@
#include "node_modules.h"
#include <cstdio>
#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<Value>& args) {
.ToLocalChecked());
}
void GetCompileCacheEntry(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
CHECK(args[0]->IsString()); // TODO(joyeecheung): accept buffer.
CHECK(args[1]->IsString());
CHECK(args[2]->IsUint32());
Local<Context> context = isolate->GetCurrentContext();
Environment* env = Environment::GetCurrent(context);
if (!env->use_compile_cache()) {
return;
}
Local<String> source = args[0].As<String>();
Local<String> filename = args[1].As<String>();
CachedCodeType type =
static_cast<CachedCodeType>(args[2].As<v8::Uint32>()->Value());
auto* cache_entry =
env->compile_cache_handler()->GetOrInsert(source, filename, type);
if (cache_entry == nullptr) {
return;
}
v8::LocalVector<v8::Name> names(isolate,
{FIXED_ONE_BYTE_STRING(isolate, "external")});
v8::LocalVector<v8::Value> 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<const char*>(cache_entry->cache->data),
cache_entry->cache->length);
Local<Value> 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<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> 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<CompileCacheEntry*>(args[0].As<External>()->Value());
Utf8Value utf8(isolate, args[1].As<String>());
env->compile_cache_handler()->MaybeSave(cache_entry, utf8.ToStringView());
}
void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
Local<ObjectTemplate> 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<Object> target,
@@ -530,12 +605,31 @@ void BindingData::CreatePerContextProperties(Local<Object> 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<Name> cached_code_type_keys(isolate);
LocalVector<Value> 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

View File

@@ -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;
}
});
}

View File

@@ -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;
}
});
}

View File

@@ -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;
}
});

View File

@@ -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;
}
});

View File

@@ -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;
}
});