| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent d0447c9 commit 97a437e
9 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -3708,6 +3708,15 @@ When set to 1, the [module compile cache][] can be reused across different direc | |||
| 3708 | 3708 | locations as long as the module layout relative to the cache directory remains the same, | |
| 3709 | 3709 | and by any user (the cache subdirectory is not suffixed with the creating user's uid). | |
| 3710 | 3710 | ||
| 3711 | + ### `NODE_COMPILE_CACHE_READONLY=1` | ||
| 3712 | + | ||
| 3713 | + <!-- YAML | ||
| 3714 | + added: REPLACEME | ||
| 3715 | + --> | ||
| 3716 | + | ||
| 3717 | + When set to 1, the [module compile cache][] only reads existing entries from | ||
| 3718 | + its directory: nothing is written to it and it is not created if missing. | ||
| 3719 | + | ||
| 3711 | 3720 | ### `NODE_DEBUG=module[,…]` | |
| 3712 | 3721 | ||
| 3713 | 3722 | <!-- YAML | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -427,6 +427,15 @@ There are two ways to enable the portable mode: | |||
| 427 | 427 | ||
| 428 | 428 | 2. Setting the environment variable: [`NODE_COMPILE_CACHE_PORTABLE=1`][] | |
| 429 | 429 | ||
| 430 | + ### Read-only compile cache | ||
| 431 | + | ||
| 432 | + A cache that was generated ahead of time, for example at build time to be | ||
| 433 | + shipped inside an application package, can be enabled with `readOnly: true` | ||
| 434 | + (or [`NODE_COMPILE_CACHE_READONLY=1`][]). Node.js then loads whatever entries | ||
| 435 | + the directory holds and never writes to it: modules without a usable entry are | ||
| 436 | + compiled as usual but not persisted, [`module.flushCompileCache()`][] is a | ||
| 437 | + no-op, and the directory is not created if it is missing. | ||
| 438 | + | ||
| 430 | 439 | ### Limitations of the compile cache | |
| 431 | 440 | ||
| 432 | 441 | Currently when using the compile cache with [V8 JavaScript code coverage][], the | |
@@ -498,6 +507,9 @@ The following constants are returned as the `status` field in the object returne | |||
| 498 | 507 | <!-- YAML | |
| 499 | 508 | added: v22.8.0 | |
| 500 | 509 | changes: | |
| 510 | + - version: REPLACEME | ||
| 511 | + pr-url: https://github.com/nodejs/node/pull/65302 | ||
| 512 | + description: Add the `readOnly` option. | ||
| 501 | 513 | - version: v25.4.0 | |
| 502 | 514 | pr-url: https://github.com/nodejs/node/pull/60971 | |
| 503 | 515 | description: This feature is no longer experimental. | |
@@ -522,6 +534,11 @@ changes: | |||
| 522 | 534 | the cache can be reused even if the project directory is moved. This is a best-effort | |
| 523 | 535 | feature. If not specified, it will depend on whether the environment variable | |
| 524 | 536 | [`NODE_COMPILE_CACHE_PORTABLE=1`][] is set. | |
| 537 | + * `readOnly` {boolean} Optional. If `true`, existing cache entries in `directory` are | ||
| 538 | + used but nothing is ever written to it, and the directory is not created when it does | ||
| 539 | + not exist (enabling then fails). Meant for caches generated ahead of time and shipped | ||
| 540 | + with an application. If not specified, it will depend on whether the environment | ||
| 541 | + variable [`NODE_COMPILE_CACHE_READONLY=1`][] is set. | ||
| 525 | 542 | * Returns: {Object} | |
| 526 | 543 | * `status` {integer} One of the [`module.constants.compileCacheStatus`][] | |
| 527 | 544 | * `message` {string|undefined} If Node.js cannot enable the compile cache, this contains | |
@@ -2047,6 +2064,7 @@ returned object contains the following keys: | |||
| 2047 | 2064 | [`--require`]: cli.md#-r---require-module | |
| 2048 | 2065 | [`NODE_COMPILE_CACHE=dir`]: cli.md#node_compile_cachedir | |
| 2049 | 2066 | [`NODE_COMPILE_CACHE_PORTABLE=1`]: cli.md#node_compile_cache_portable1 | |
| 2067 | + [`NODE_COMPILE_CACHE_READONLY=1`]: cli.md#node_compile_cache_readonly1 | ||
| 2050 | 2068 | [`NODE_DISABLE_COMPILE_CACHE=1`]: cli.md#node_disable_compile_cache1 | |
| 2051 | 2069 | [`NODE_V8_COVERAGE=dir`]: cli.md#node_v8_coveragedir | |
| 2052 | 2070 | [`SourceMap`]: #class-modulesourcemap | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1862,6 +1862,10 @@ When set to 1, the module compile cache can be reused across different directory | |||
| 1862 | 1862 | locations as long as the module layout relative to the cache directory remains the same, | |
| 1863 | 1863 | and by any user (the cache subdirectory is not suffixed with the creating user's uid). | |
| 1864 | 1864 | . | |
| 1865 | + .It Ev NODE_COMPILE_CACHE_READONLY Ar 1 | ||
| 1866 | + When set to 1, the module compile cache only reads existing entries from | ||
| 1867 | + its directory: nothing is written to it and it is not created if missing. | ||
| 1868 | + . | ||
| 1865 | 1869 | .It Ev NODE_DEBUG Ar module[,…] | |
| 1866 | 1870 | \fB','\fR-separated list of core modules that should print debug information. | |
| 1867 | 1871 | . | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -466,21 +466,25 @@ function stringify(body) { | |||
| 466 | 466 | * after this method is called. | |
| 467 | 467 | * This method accepts either: | |
| 468 | 468 | * - A string: path to the cache directory. | |
| 469 | - * - An options object `{directory?: string, portable?: boolean}`: | ||
| 469 | + * - An options object `{directory?: string, portable?: boolean, readOnly?: boolean}`: | ||
| 470 | 470 | * - `directory`: A string path to the cache directory. | |
| 471 | 471 | * - `portable`: If `portable` is true, the cache directory will be considered relative. | |
| 472 | 472 | * Defaults to `NODE_COMPILE_CACHE_PORTABLE === '1'`. | |
| 473 | + * - `readOnly`: If `readOnly` is true, existing cache entries are used but nothing is | ||
| 474 | + * written, and the cache directory is not created. Defaults to | ||
| 475 | + * `NODE_COMPILE_CACHE_READONLY === '1'`. | ||
| 473 | 476 | * If cache directory is undefined, it defaults to the `NODE_COMPILE_CACHE` environment variable. | |
| 474 | 477 | * If `NODE_COMPILE_CACHE` isn't set, it defaults to `path.join(os.tmpdir(), 'node-compile-cache')`. | |
| 475 | - * @param {string | { directory?: string, portable?: boolean } | undefined} options | ||
| 478 | + * @param {string | { directory?: string, portable?: boolean, readOnly?: boolean } | undefined} options | ||
| 476 | 479 | * @returns {{status: number, message?: string, directory?: string}} | |
| 477 | 480 | */ | |
| 478 | 481 | function enableCompileCache(options) { | |
| 479 | 482 | let portable; | |
| 483 | + let readOnly; | ||
| 480 | 484 | let directory; | |
| 481 | 485 | ||
| 482 | 486 | if (typeof options === 'object' && options !== null) { | |
| 483 | - ({ directory, portable } = options); | ||
| 487 | + ({ directory, portable, readOnly } = options); | ||
| 484 | 488 | } else { | |
| 485 | 489 | directory = options; | |
| 486 | 490 | } | |
@@ -490,7 +494,10 @@ function enableCompileCache(options) { | |||
| 490 | 494 | if (portable === undefined) { | |
| 491 | 495 | portable = process.env.NODE_COMPILE_CACHE_PORTABLE === '1'; | |
| 492 | 496 | } | |
| 493 | - const nativeResult = _enableCompileCache(directory, portable); | ||
| 497 | + if (readOnly === undefined) { | ||
| 498 | + readOnly = process.env.NODE_COMPILE_CACHE_READONLY === '1'; | ||
| 499 | + } | ||
| 500 | + const nativeResult = _enableCompileCache(directory, portable, readOnly); | ||
| 494 | 501 | const result = { status: nativeResult[0] }; | |
| 495 | 502 | if (nativeResult[1]) { | |
| 496 | 503 | result.message = nativeResult[1]; | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -257,7 +257,8 @@ CompileCacheEntry* CompileCacheHandler::GetOrInsert(Local<String> code, | |||
| 257 | 257 | // If the portable cache is enabled and it seems possible to compute the | |
| 258 | 258 | // relative position from an absolute path, we use the relative position | |
| 259 | 259 | // in the cache key. | |
| 260 | - if (portable_ == EnableOption::PORTABLE && IsAbsoluteFilePath(file_path)) { | ||
| 260 | + if (HasOption(portable_, EnableOption::PORTABLE) && | ||
| 261 | + IsAbsoluteFilePath(file_path)) { | ||
| 261 | 262 | // Normalize the path to ensure it is consistent. | |
| 262 | 263 | std::string normalized_file_path = NormalizeFileURLOrPath(env, file_path); | |
| 263 | 264 | if (normalized_file_path.empty()) { | |
@@ -332,6 +333,10 @@ void CompileCacheHandler::MaybeSaveImpl(CompileCacheEntry* entry, | |||
| 332 | 333 | Debug("keeping the in-memory entry\n"); | |
| 333 | 334 | return; | |
| 334 | 335 | } | |
| 336 | + if (read_only_) { | ||
| 337 | + Debug("read-only, not serializing\n"); | ||
| 338 | + return; | ||
| 339 | + } | ||
| 335 | 340 | Debug("%s the in-memory entry\n", | |
| 336 | 341 | entry->cache == nullptr ? "initializing" : "refreshing"); | |
| 337 | 342 | ||
@@ -356,6 +361,9 @@ void CompileCacheHandler::MaybeSave(CompileCacheEntry* entry, | |||
| 356 | 361 | ||
| 357 | 362 | void CompileCacheHandler::MaybeSave(CompileCacheEntry* entry, | |
| 358 | 363 | std::string_view transpiled) { | |
| 364 | + if (read_only_) { | ||
| 365 | + return; | ||
| 366 | + } | ||
| 359 | 367 | CHECK(entry->type == CachedCodeType::kStrippedTypeScript); | |
| 360 | 368 | Debug("[compile cache] saving transpilation cache for %s %s\n", | |
| 361 | 369 | entry->type_name(), | |
@@ -388,6 +396,10 @@ void CompileCacheHandler::MaybeSave(CompileCacheEntry* entry, | |||
| 388 | 396 | */ | |
| 389 | 397 | void CompileCacheHandler::Persist() { | |
| 390 | 398 | DCHECK(!compile_cache_dir_.empty()); | |
| 399 | + if (read_only_) { | ||
| 400 | + Debug("[compile cache] read-only, skipping persistence\n"); | ||
| 401 | + return; | ||
| 402 | + } | ||
| 391 | 403 | ||
| 392 | 404 | // TODO(joyeecheung): do this using a separate event loop to utilize the | |
| 393 | 405 | // libuv thread pool and do the file system operations concurrently. | |
@@ -554,10 +566,11 @@ CompileCacheEnableResult CompileCacheHandler::Enable(Environment* env, | |||
| 554 | 566 | cache_tag, | |
| 555 | 567 | cache_dir_with_tag); | |
| 556 | 568 | ||
| 557 | - if (!env->permission()->is_granted( | ||
| 558 | - env, | ||
| 559 | - permission::PermissionScope::kFileSystemWrite, | ||
| 560 | - cache_dir_with_tag)) [[unlikely]] { | ||
| 569 | + const bool read_only = HasOption(option, EnableOption::READ_ONLY); | ||
| 570 | + if (!read_only && !env->permission()->is_granted( | ||
| 571 | + env, | ||
| 572 | + permission::PermissionScope::kFileSystemWrite, | ||
| 573 | + cache_dir_with_tag)) [[unlikely]] { | ||
| 561 | 574 | result.message = "Skipping compile cache because write permission for " + | |
| 562 | 575 | cache_dir_with_tag + " is not granted"; | |
| 563 | 576 | result.status = CompileCacheEnableStatus::FAILED; | |
@@ -574,25 +587,46 @@ CompileCacheEnableResult CompileCacheHandler::Enable(Environment* env, | |||
| 574 | 587 | return result; | |
| 575 | 588 | } | |
| 576 | 589 | ||
| 577 | - fs::FSReqWrapSync req_wrap; | ||
| 578 | - int err = fs::MKDirpSync( | ||
| 579 | - nullptr, &(req_wrap.req), cache_dir_with_tag, 0777, nullptr); | ||
| 580 | - if (is_debug_) { | ||
| 581 | - Debug("[compile cache] creating cache directory %s...%s\n", | ||
| 590 | + if (read_only) { | ||
| 591 | + // A read-only cache is used as found and never created: without the | ||
| 592 | + // directory there is nothing to read. | ||
| 593 | + uv_fs_t stat_req; | ||
| 594 | + int err = | ||
| 595 | + uv_fs_stat(nullptr, &stat_req, cache_dir_with_tag.c_str(), nullptr); | ||
| 596 | + // libuv normalizes st_mode across platforms; MSVC has no S_ISDIR macro. | ||
| 597 | + bool is_dir = err == 0 && (stat_req.statbuf.st_mode & S_IFMT) == S_IFDIR; | ||
| 598 | + uv_fs_req_cleanup(&stat_req); | ||
| 599 | + Debug("[compile cache] read-only cache directory %s...%s\n", | ||
| 582 | 600 | cache_dir_with_tag, | |
| 583 | - err < 0 ? uv_strerror(err) : "success"); | ||
| 584 | - } | ||
| 585 | - if (err != 0 && err != UV_EEXIST) { | ||
| 586 | - result.message = | ||
| 587 | - "Cannot create cache directory: " + std::string(uv_strerror(err)); | ||
| 588 | - result.status = CompileCacheEnableStatus::FAILED; | ||
| 589 | - return result; | ||
| 601 | + is_dir ? "found" : "not found"); | ||
| 602 | + if (!is_dir) { | ||
| 603 | + result.message = | ||
| 604 | + "Cache directory does not exist (read-only): " + cache_dir_with_tag; | ||
| 605 | + result.status = CompileCacheEnableStatus::FAILED; | ||
| 606 | + return result; | ||
| 607 | + } | ||
| 608 | + } else { | ||
| 609 | + fs::FSReqWrapSync req_wrap; | ||
| 610 | + int err = fs::MKDirpSync( | ||
| 611 | + nullptr, &(req_wrap.req), cache_dir_with_tag, 0777, nullptr); | ||
| 612 | + if (is_debug_) { | ||
| 613 | + Debug("[compile cache] creating cache directory %s...%s\n", | ||
| 614 | + cache_dir_with_tag, | ||
| 615 | + err < 0 ? uv_strerror(err) : "success"); | ||
| 616 | + } | ||
| 617 | + if (err != 0 && err != UV_EEXIST) { | ||
| 618 | + result.message = | ||
| 619 | + "Cannot create cache directory: " + std::string(uv_strerror(err)); | ||
| 620 | + result.status = CompileCacheEnableStatus::FAILED; | ||
| 621 | + return result; | ||
| 622 | + } | ||
| 590 | 623 | } | |
| 591 | 624 | ||
| 592 | 625 | result.cache_directory = absolute_cache_dir_base; | |
| 593 | 626 | compile_cache_dir_ = cache_dir_with_tag; | |
| 594 | 627 | portable_ = option; | |
| 595 | - if (option == EnableOption::PORTABLE) { | ||
| 628 | + read_only_ = read_only; | ||
| 629 | + if (HasOption(option, EnableOption::PORTABLE)) { | ||
| 596 | 630 | normalized_compile_cache_dir_ = | |
| 597 | 631 | NormalizeFileURLOrPath(env, compile_cache_dir_); | |
| 598 | 632 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -60,7 +60,22 @@ struct CompileCacheEnableResult { | |||
| 60 | 60 | std::string message; // Set in case of failure. | |
| 61 | 61 | }; | |
| 62 | 62 | ||
| 63 | - enum class EnableOption : uint8_t { DEFAULT, PORTABLE }; | ||
| 63 | + enum class EnableOption : uint8_t { | ||
| 64 | + DEFAULT = 0, | ||
| 65 | + PORTABLE = 1 << 0, | ||
| 66 | + // Only read existing cache entries: nothing is compiled into the in-memory | ||
| 67 | + // store for persisting, nothing is written to disk, and the cache directory | ||
| 68 | + // is not created if it does not exist. | ||
| 69 | + READ_ONLY = 1 << 1, | ||
| 70 | + }; | ||
| 71 | + | ||
| 72 | + inline constexpr EnableOption operator|(EnableOption a, EnableOption b) { | ||
| 73 | + return static_cast<EnableOption>(static_cast<uint8_t>(a) | | ||
| 74 | + static_cast<uint8_t>(b)); | ||
| 75 | + } | ||
| 76 | + inline constexpr bool HasOption(EnableOption value, EnableOption flag) { | ||
| 77 | + return (static_cast<uint8_t>(value) & static_cast<uint8_t>(flag)) != 0; | ||
| 78 | + } | ||
| 64 | 79 | ||
| 65 | 80 | class CompileCacheHandler { | |
| 66 | 81 | public: | |
@@ -82,6 +97,7 @@ class CompileCacheHandler { | |||
| 82 | 97 | bool rejected); | |
| 83 | 98 | void MaybeSave(CompileCacheEntry* entry, std::string_view transpiled); | |
| 84 | 99 | std::string_view cache_dir() { return compile_cache_dir_; } | |
| 100 | + bool read_only() const { return read_only_; } | ||
| 85 | 101 | ||
| 86 | 102 | private: | |
| 87 | 103 | void ReadCacheFile(CompileCacheEntry* entry); | |
@@ -107,6 +123,7 @@ class CompileCacheHandler { | |||
| 107 | 123 | std::string compile_cache_dir_; | |
| 108 | 124 | std::string normalized_compile_cache_dir_; | |
| 109 | 125 | EnableOption portable_ = EnableOption::DEFAULT; | |
| 126 | + bool read_only_ = false; | ||
| 110 | 127 | std::unordered_map<uint32_t, std::unique_ptr<CompileCacheEntry>> | |
| 111 | 128 | compiler_cache_store_; | |
| 112 | 129 | }; | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1195,8 +1195,14 @@ void Environment::InitializeCompileCache() { | |||
| 1195 | 1195 | DebugCategory::COMPILE_CACHE, | |
| 1196 | 1196 | "[compile cache] using relative path\n"); | |
| 1197 | 1197 | } | |
| 1198 | - EnableCompileCache(dir_from_env, | ||
| 1199 | - portable ? EnableOption::PORTABLE : EnableOption::DEFAULT); | ||
| 1198 | + std::string read_only_env; | ||
| 1199 | + bool read_only = credentials::SafeGetenv( | ||
| 1200 | + "NODE_COMPILE_CACHE_READONLY", &read_only_env, this) && | ||
| 1201 | + read_only_env == "1"; | ||
| 1202 | + EnableOption option = EnableOption::DEFAULT; | ||
| 1203 | + if (portable) option = option | EnableOption::PORTABLE; | ||
| 1204 | + if (read_only) option = option | EnableOption::READ_ONLY; | ||
| 1205 | + EnableCompileCache(dir_from_env, option); | ||
| 1200 | 1206 | } | |
| 1201 | 1207 | ||
| 1202 | 1208 | CompileCacheEnableResult Environment::EnableCompileCache( | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -524,7 +524,10 @@ void EnableCompileCache(const FunctionCallbackInfo<Value>& args) { | |||
| 524 | 524 | ||
| 525 | 525 | EnableOption option = EnableOption::DEFAULT; | |
| 526 | 526 | if (args.Length() > 1 && args[1]->IsTrue()) { | |
| 527 | - option = EnableOption::PORTABLE; | ||
| 527 | + option = option | EnableOption::PORTABLE; | ||
| 528 | + } | ||
| 529 | + if (args.Length() > 2 && args[2]->IsTrue()) { | ||
| 530 | + option = option | EnableOption::READ_ONLY; | ||
| 528 | 531 | } | |
| 529 | 532 | ||
| 530 | 533 | Utf8Value value(isolate, args[0]); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,65 @@ | |||
| 1 | + 'use strict'; | ||
| 2 | + | ||
| 3 | + // This tests module.enableCompileCache({ directory, readOnly: true }): existing | ||
| 4 | + // entries are read, nothing is written, and a missing directory is not created. | ||
| 5 | + | ||
| 6 | + const common = require('../common'); | ||
| 7 | + const { spawnSyncAndAssert } = require('../common/child_process'); | ||
| 8 | + const assert = require('assert'); | ||
| 9 | + const fs = require('fs'); | ||
| 10 | + const path = require('path'); | ||
| 11 | + const tmpdir = require('../common/tmpdir'); | ||
| 12 | + const fixtures = require('../common/fixtures'); | ||
| 13 | + | ||
| 14 | + tmpdir.refresh(); | ||
| 15 | + const wrapper = fixtures.path('compile-cache-wrapper-options.js'); | ||
| 16 | + const target = path.join(tmpdir.path, 'target.js'); | ||
| 17 | + fs.writeFileSync(target, 'module.exports = 1;'); | ||
| 18 | + const other = path.join(tmpdir.path, 'other.js'); | ||
| 19 | + fs.writeFileSync(other, 'module.exports = 2;'); | ||
| 20 | + const directory = path.join(tmpdir.path, 'cache'); | ||
| 21 | + const list = () => fs.readdirSync(directory, { recursive: true }).sort(); | ||
| 22 | + const run = (options, extraEnv, requires, check) => spawnSyncAndAssert( | ||
| 23 | + process.execPath, | ||
| 24 | + [...requires.flatMap((r) => ['-r', r]), target], | ||
| 25 | + { | ||
| 26 | + env: { | ||
| 27 | + ...process.env, | ||
| 28 | + NODE_DEBUG_NATIVE: 'COMPILE_CACHE', | ||
| 29 | + NODE_TEST_COMPILE_CACHE_OPTIONS: JSON.stringify(options), | ||
| 30 | + ...extraEnv, | ||
| 31 | + }, | ||
| 32 | + }, | ||
| 33 | + check); | ||
| 34 | + | ||
| 35 | + // Read-only against a directory that does not exist: enabling fails and | ||
| 36 | + // nothing is created. | ||
| 37 | + run({ directory, readOnly: true }, {}, [wrapper], { | ||
| 38 | + stderr: /read-only cache directory .*\.\.\.not found/, | ||
| 39 | + }); | ||
| 40 | + assert(!fs.existsSync(directory)); | ||
| 41 | + | ||
| 42 | + // A normal run generates the cache for target.js. | ||
| 43 | + run({ directory }, {}, [wrapper], { | ||
| 44 | + stderr: /writing cache for .*target\.js.*success/, | ||
| 45 | + }); | ||
| 46 | + const generated = list(); | ||
| 47 | + assert.notStrictEqual(generated.length, 0); | ||
| 48 | + | ||
| 49 | + // Read-only against it: target.js's entry is accepted, other.js is compiled | ||
| 50 | + // but not written, and persistence is skipped. | ||
| 51 | + run({ directory, readOnly: true }, {}, [wrapper, other], { | ||
| 52 | + stderr: common.mustCall((output) => { | ||
| 53 | + assert.match(output, /cache for .*target\.js was accepted/); | ||
| 54 | + assert.match(output, /read-only, skipping persistence/); | ||
| 55 | + assert.doesNotMatch(output, /writing cache for/); | ||
| 56 | + return true; | ||
| 57 | + }), | ||
| 58 | + }); | ||
| 59 | + assert.deepStrictEqual(list(), generated); | ||
| 60 | + | ||
| 61 | + // The environment variable form. | ||
| 62 | + run({ directory }, { NODE_COMPILE_CACHE_READONLY: '1' }, [wrapper, other], { | ||
| 63 | + stderr: /read-only, skipping persistence/, | ||
| 64 | + }); | ||
| 65 | + assert.deepStrictEqual(list(), generated); | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments