getModuleSystem.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. const { promises: fsPromises } = require('fs');
  2. const path = require('path');
  3. /** @type {Map<string, string | undefined>} */
  4. let packageJsonTypeMap = new Map();
  5. /**
  6. * Infers the current active module system from loader context and options.
  7. * @this {import('webpack').loader.LoaderContext}
  8. * @param {import('webpack').ModuleFilenameHelpers} ModuleFilenameHelpers Webpack's module filename helpers.
  9. * @param {import('../types').NormalizedLoaderOptions} options The normalized loader options.
  10. * @return {Promise<'esm' | 'cjs'>} The inferred module system.
  11. */
  12. async function getModuleSystem(ModuleFilenameHelpers, options) {
  13. // Check loader options -
  14. // if `esModule` is set we don't have to do extra guess work.
  15. switch (typeof options.esModule) {
  16. case 'boolean': {
  17. return options.esModule ? 'esm' : 'cjs';
  18. }
  19. case 'object': {
  20. if (
  21. options.esModule.include &&
  22. ModuleFilenameHelpers.matchPart(this.resourcePath, options.esModule.include)
  23. ) {
  24. return 'esm';
  25. }
  26. if (
  27. options.esModule.exclude &&
  28. ModuleFilenameHelpers.matchPart(this.resourcePath, options.esModule.exclude)
  29. ) {
  30. return 'cjs';
  31. }
  32. break;
  33. }
  34. default: // Do nothing
  35. }
  36. // Check current resource's extension
  37. if (/\.mjs$/.test(this.resourcePath)) return 'esm';
  38. if (/\.cjs$/.test(this.resourcePath)) return 'cjs';
  39. if (typeof this.addMissingDependency !== 'function') {
  40. // This is Webpack 4 which does not support `import.meta`.
  41. // We assume `.js` files are CommonJS because the output cannot be ESM anyway.
  42. return 'cjs';
  43. }
  44. // We will assume CommonJS if we cannot determine otherwise
  45. let packageJsonType = '';
  46. // We begin our search for relevant `package.json` files,
  47. // at the directory of the resource being loaded.
  48. // These paths should already be resolved,
  49. // but we resolve them again to ensure we are dealing with an aboslute path.
  50. const resourceContext = path.dirname(this.resourcePath);
  51. let searchPath = resourceContext;
  52. let previousSearchPath = '';
  53. // We start our search just above the root context of the webpack compilation
  54. const stopPath = path.dirname(this.rootContext);
  55. // If the module context is a resolved symlink outside the `rootContext` path,
  56. // then we will never find the `stopPath` - so we also halt when we hit the root.
  57. // Note that there is a potential that the wrong `package.json` is found in some pathalogical cases,
  58. // such as a folder that is conceptually a package + does not have an ancestor `package.json`,
  59. // but there exists a `package.json` higher up.
  60. // This might happen if you have a folder of utility JS files that you symlink but did not organize as a package.
  61. // We consider this an unsupported edge case for now.
  62. while (searchPath !== stopPath && searchPath !== previousSearchPath) {
  63. // If we have already determined the `package.json` type for this path we can stop searching.
  64. // We do however still need to cache the found value,
  65. // from the `resourcePath` folder up to the matching `searchPath`,
  66. // to avoid retracing these steps when processing sibling resources.
  67. if (packageJsonTypeMap.has(searchPath)) {
  68. packageJsonType = packageJsonTypeMap.get(searchPath);
  69. let currentPath = resourceContext;
  70. while (currentPath !== searchPath) {
  71. // We set the found type at least level from `resourcePath` folder up to the matching `searchPath`
  72. packageJsonTypeMap.set(currentPath, packageJsonType);
  73. currentPath = path.dirname(currentPath);
  74. }
  75. break;
  76. }
  77. let packageJsonPath = path.join(searchPath, 'package.json');
  78. try {
  79. const packageSource = await fsPromises.readFile(packageJsonPath, 'utf-8');
  80. try {
  81. const packageObject = JSON.parse(packageSource);
  82. // Any package.json is sufficient as long as it can be parsed.
  83. // If it does not explicitly have a `type: "module"` it will be assumed to be CommonJS.
  84. packageJsonType = typeof packageObject.type === 'string' ? packageObject.type : '';
  85. packageJsonTypeMap.set(searchPath, packageJsonType);
  86. // We set the type in the cache for all paths from the `resourcePath` folder,
  87. // up to the matching `searchPath` to avoid retracing these steps when processing sibling resources.
  88. let currentPath = resourceContext;
  89. while (currentPath !== searchPath) {
  90. packageJsonTypeMap.set(currentPath, packageJsonType);
  91. currentPath = path.dirname(currentPath);
  92. }
  93. } catch (e) {
  94. // `package.json` exists but could not be parsed.
  95. // We track it as a dependency so we can reload if this file changes.
  96. }
  97. this.addDependency(packageJsonPath);
  98. break;
  99. } catch (e) {
  100. // `package.json` does not exist.
  101. // We track it as a missing dependency so we can reload if this file is added.
  102. this.addMissingDependency(packageJsonPath);
  103. }
  104. // Try again at the next level up
  105. previousSearchPath = searchPath;
  106. searchPath = path.dirname(searchPath);
  107. }
  108. // Check `package.json` for the `type` field -
  109. // fallback to use `cjs` for anything ambiguous.
  110. return packageJsonType === 'module' ? 'esm' : 'cjs';
  111. }
  112. module.exports = getModuleSystem;