|
5 | 5 | ArrayPrototypePush, |
6 | 6 | ArrayPrototypeSplice, |
7 | 7 | FunctionPrototypeCall, |
| 8 | + JSONParse, |
| 9 | + StringPrototypeEndsWith, |
| 10 | + StringPrototypeLastIndexOf, |
8 | 11 | StringPrototypeStartsWith, |
9 | 12 | } = primordials; |
10 | 13 |
|
@@ -336,17 +339,61 @@ function findVFSForWatch(filename) { |
336 | 339 | return null; |
337 | 340 | } |
338 | 341 |
|
| 342 | +/** |
| 343 | + * Checks whether any active VFS could potentially handle the given path. |
| 344 | + * Used for early exit on non-VFS paths to avoid unnecessary JS walks. |
| 345 | + * @param {string} filePath The absolute file path to check |
| 346 | + * @returns {boolean} |
| 347 | + */ |
| 348 | +function anyVFSCouldHandle(filePath) { |
| 349 | + const normalized = normalizeVFSPath(filePath); |
| 350 | + for (let i = 0; i < activeVFSList.length; i++) { |
| 351 | + if (activeVFSList[i].shouldHandle(normalized)) { |
| 352 | + return true; |
| 353 | + } |
| 354 | + } |
| 355 | + return false; |
| 356 | +} |
| 357 | + |
| 358 | +/** |
| 359 | + * Parse VFS package.json content into the flat PackageConfig format |
| 360 | + * returned by read() (not the nested DeserializedPackageConfig format). |
| 361 | + * @param {string} jsonPath Path to the package.json |
| 362 | + * @param {string|Buffer} content Raw file content |
| 363 | + * @returns {object} PackageConfig with { name, main, type, exports, imports, exists, pjsonPath } |
| 364 | + */ |
| 365 | +function parseVFSPackageJSON(jsonPath, content) { |
| 366 | + const str = typeof content === 'string' ? content : content.toString('utf8'); |
| 367 | + const parsed = JSONParse(str); |
| 368 | + let type = 'none'; |
| 369 | + if (parsed.type === 'commonjs' || parsed.type === 'module') { |
| 370 | + type = parsed.type; |
| 371 | + } |
| 372 | + return { |
| 373 | + __proto__: null, |
| 374 | + ...(parsed.name != null && { name: parsed.name }), |
| 375 | + ...(parsed.main != null && { main: parsed.main }), |
| 376 | + type, |
| 377 | + ...(parsed.exports != null && { exports: parsed.exports }), |
| 378 | + ...(parsed.imports != null && { imports: parsed.imports }), |
| 379 | + exists: true, |
| 380 | + pjsonPath: jsonPath, |
| 381 | + }; |
| 382 | +} |
| 383 | + |
339 | 384 | /** |
340 | 385 | * Determine module format from file extension. |
341 | | - * Uses the shared extensionFormatMap, falling back to commonjs for .js |
342 | | - * and unknown extensions since VFS does not check package.json "type". |
| 386 | + * Uses the shared extensionFormatMap. For .js files, checks the package.json |
| 387 | + * "type" field via getPackageType (which is now VFS-aware). |
343 | 388 | * @param {string} filePath The file path |
344 | 389 | * @returns {string} The format ('module', 'commonjs', 'json', etc.) |
345 | 390 | */ |
346 | 391 | function getFormatFromExtension(filePath) { |
347 | 392 | const ext = extname(filePath); |
348 | 393 | if (ext === '.js') { |
349 | | - // TODO: Check package.json "type" field for proper detection |
| 394 | + const { getPackageType } = require('internal/modules/package_json_reader'); |
| 395 | + const type = getPackageType(pathToFileURL(filePath)); |
| 396 | + if (type === 'module') return 'module'; |
350 | 397 | return 'commonjs'; |
351 | 398 | } |
352 | 399 | return extensionFormatMap[ext] ?? 'commonjs'; |
@@ -487,7 +534,16 @@ function installModuleHooks() { |
487 | 534 | setCustomToRealPath, |
488 | 535 | setCustomReadFileSync, |
489 | 536 | setCustomInternalModuleStat, |
| 537 | + internalModuleStat, |
490 | 538 | } = require('internal/modules/helpers'); |
| 539 | + const { |
| 540 | + read: readPackageJSON, |
| 541 | + getPackageScopeConfig, |
| 542 | + setCustomRead, |
| 543 | + setCustomGetPackageScopeConfig, |
| 544 | + setCustomGetPackageType, |
| 545 | + setCustomGetNearestParentPackageJSON, |
| 546 | + } = require('internal/modules/package_json_reader'); |
491 | 547 |
|
492 | 548 | // Save original Module._stat |
493 | 549 | originalStat = Module._stat; |
@@ -539,6 +595,150 @@ function installModuleHooks() { |
539 | 595 | return defaultFn(path); |
540 | 596 | }); |
541 | 597 |
|
| 598 | + // Set VFS-aware package.json read override. |
| 599 | + // The C++ readPackageJSON binding reads from disk via libuv, bypassing VFS. |
| 600 | + // This override intercepts package.json reads for paths under VFS mounts. |
| 601 | + setCustomRead(function vfsAwareRead(jsonPath, opts, defaultFn) { |
| 602 | + if (!anyVFSCouldHandle(jsonPath)) { |
| 603 | + return defaultFn(jsonPath, opts); |
| 604 | + } |
| 605 | + // Check if the file exists in VFS via stat (avoids ENOENT throw from findVFSForRead) |
| 606 | + const statResult = findVFSForStat(jsonPath); |
| 607 | + if (statResult !== null) { |
| 608 | + if (statResult.result === 0) { |
| 609 | + // File exists -read and parse it |
| 610 | + const vfsResult = findVFSForRead(jsonPath, 'utf8'); |
| 611 | + if (vfsResult !== null) { |
| 612 | + return parseVFSPackageJSON(jsonPath, vfsResult.content); |
| 613 | + } |
| 614 | + } |
| 615 | + // Path is handled by VFS but file doesn't exist -return not-found |
| 616 | + return { |
| 617 | + __proto__: null, |
| 618 | + exists: false, |
| 619 | + pjsonPath: jsonPath, |
| 620 | + type: 'none', |
| 621 | + }; |
| 622 | + } |
| 623 | + return defaultFn(jsonPath, opts); |
| 624 | + }); |
| 625 | + |
| 626 | + // Set VFS-aware getPackageScopeConfig override. |
| 627 | + // The C++ implementation walks upward from a file URL to find the nearest |
| 628 | + // package.json. This JS reimplementation does the same walk for VFS paths. |
| 629 | + setCustomGetPackageScopeConfig(function vfsAwareGetPackageScopeConfig(resolved, defaultFn) { |
| 630 | + let resolvedPath; |
| 631 | + try { |
| 632 | + resolvedPath = fileURLToPath(`${resolved}`); |
| 633 | + } catch { |
| 634 | + return defaultFn(resolved); |
| 635 | + } |
| 636 | + |
| 637 | + if (!anyVFSCouldHandle(resolvedPath)) { |
| 638 | + return defaultFn(resolved); |
| 639 | + } |
| 640 | + |
| 641 | + // Walk upward from the resolved path, checking for package.json at each level. |
| 642 | + // This mirrors the C++ GetPackageScopeConfig logic. |
| 643 | + let currentDir = dirname(resolvedPath); |
| 644 | + let lastDir; |
| 645 | + |
| 646 | + while (currentDir !== lastDir) { |
| 647 | + // Stop at node_modules boundaries |
| 648 | + if (StringPrototypeEndsWith(currentDir, '/node_modules') || |
| 649 | + StringPrototypeEndsWith(currentDir, '\\node_modules')) { |
| 650 | + break; |
| 651 | + } |
| 652 | + |
| 653 | + const pjsonPath = resolve(currentDir, 'package.json'); |
| 654 | + const result = readPackageJSON(pjsonPath); |
| 655 | + if (result.exists) { |
| 656 | + return result; |
| 657 | + } |
| 658 | + |
| 659 | + lastDir = currentDir; |
| 660 | + currentDir = dirname(currentDir); |
| 661 | + } |
| 662 | + |
| 663 | + // No package.json found -return not-found result |
| 664 | + return { |
| 665 | + __proto__: null, |
| 666 | + pjsonPath: resolve(dirname(resolvedPath), 'package.json'), |
| 667 | + exists: false, |
| 668 | + type: 'none', |
| 669 | + }; |
| 670 | + }); |
| 671 | + |
| 672 | + // Set VFS-aware getPackageType override. |
| 673 | + // Delegates to getPackageScopeConfig (which is already toggled). |
| 674 | + setCustomGetPackageType(function vfsAwareGetPackageType(url, defaultFn) { |
| 675 | + let urlPath; |
| 676 | + try { |
| 677 | + urlPath = fileURLToPath(`${url}`); |
| 678 | + } catch { |
| 679 | + return defaultFn(url); |
| 680 | + } |
| 681 | + |
| 682 | + if (!anyVFSCouldHandle(urlPath)) { |
| 683 | + return defaultFn(url); |
| 684 | + } |
| 685 | + |
| 686 | + const config = getPackageScopeConfig(url); |
| 687 | + return config.type ?? 'none'; |
| 688 | + }); |
| 689 | + |
| 690 | + // Set VFS-aware getNearestParentPackageJSON override. |
| 691 | + // The C++ TraverseParent walks upward from a file path (not URL) to find |
| 692 | + // the nearest package.json. Returns DeserializedPackageConfig format: |
| 693 | + // { data: { name, main, type, exports, imports }, exists, path }. |
| 694 | + setCustomGetNearestParentPackageJSON(function vfsAwareGetNearestParent(checkPath, defaultFn) { |
| 695 | + if (!anyVFSCouldHandle(checkPath)) { |
| 696 | + return defaultFn(checkPath); |
| 697 | + } |
| 698 | + |
| 699 | + // Walk upward from dirname(checkPath) |
| 700 | + let currentDir = dirname(checkPath); |
| 701 | + let lastDir; |
| 702 | + |
| 703 | + while (currentDir !== lastDir) { |
| 704 | + // Stop at node_modules boundaries (matches C++ TraverseParent) |
| 705 | + const basename = currentDir.substring( |
| 706 | + StringPrototypeLastIndexOf(currentDir, '/') + 1, |
| 707 | + ); |
| 708 | + if (basename === 'node_modules') { |
| 709 | + return undefined; |
| 710 | + } |
| 711 | + |
| 712 | + const pjsonPath = resolve(currentDir, 'package.json'); |
| 713 | + |
| 714 | + // Check if the package.json file exists via stat |
| 715 | + const stat = internalModuleStat(pjsonPath); |
| 716 | + if (stat === 0) { |
| 717 | + // File exists -read and parse it |
| 718 | + const flat = readPackageJSON(pjsonPath); |
| 719 | + if (flat.exists) { |
| 720 | + // Convert flat PackageConfig to nested DeserializedPackageConfig |
| 721 | + const data = { __proto__: null, type: flat.type ?? 'none' }; |
| 722 | + if (flat.name != null) data.name = flat.name; |
| 723 | + if (flat.main != null) data.main = flat.main; |
| 724 | + if (flat.exports != null) data.exports = flat.exports; |
| 725 | + if (flat.imports != null) data.imports = flat.imports; |
| 726 | + |
| 727 | + return { |
| 728 | + data, |
| 729 | + exists: true, |
| 730 | + path: pjsonPath, |
| 731 | + }; |
| 732 | + } |
| 733 | + } |
| 734 | + |
| 735 | + lastDir = currentDir; |
| 736 | + currentDir = dirname(currentDir); |
| 737 | + } |
| 738 | + |
| 739 | + return undefined; |
| 740 | + }); |
| 741 | + |
542 | 742 | // Register ESM hooks using Module.registerHooks |
543 | 743 | Module.registerHooks({ |
544 | 744 | resolve: vfsResolveHook, |
|
0 commit comments