File size: 5,298 Bytes
cc651f6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
const { promises: fsPromises } = require('fs');
const path = require('path');

/** @type {Map<string, string | undefined>} */
let packageJsonTypeMap = new Map();

/**
 * Infers the current active module system from loader context and options.
 * @this {import('webpack').loader.LoaderContext}
 * @param {import('webpack').ModuleFilenameHelpers} ModuleFilenameHelpers Webpack's module filename helpers.
 * @param {import('../types').NormalizedLoaderOptions} options The normalized loader options.
 * @return {Promise<'esm' | 'cjs'>} The inferred module system.
 */
async function getModuleSystem(ModuleFilenameHelpers, options) {
  // Check loader options -
  // if `esModule` is set we don't have to do extra guess work.
  switch (typeof options.esModule) {
    case 'boolean': {
      return options.esModule ? 'esm' : 'cjs';
    }
    case 'object': {
      if (
        options.esModule.include &&
        ModuleFilenameHelpers.matchPart(this.resourcePath, options.esModule.include)
      ) {
        return 'esm';
      }
      if (
        options.esModule.exclude &&
        ModuleFilenameHelpers.matchPart(this.resourcePath, options.esModule.exclude)
      ) {
        return 'cjs';
      }

      break;
    }
    default: // Do nothing
  }

  // Check current resource's extension
  if (/\.mjs$/.test(this.resourcePath)) return 'esm';
  if (/\.cjs$/.test(this.resourcePath)) return 'cjs';

  if (typeof this.addMissingDependency !== 'function') {
    // This is Webpack 4 which does not support `import.meta`.
    // We assume `.js` files are CommonJS because the output cannot be ESM anyway.
    return 'cjs';
  }

  // We will assume CommonJS if we cannot determine otherwise
  let packageJsonType = '';

  // We begin our search for relevant `package.json` files,
  // at the directory of the resource being loaded.
  // These paths should already be resolved,
  // but we resolve them again to ensure we are dealing with an aboslute path.
  const resourceContext = path.dirname(this.resourcePath);
  let searchPath = resourceContext;
  let previousSearchPath = '';
  // We start our search just above the root context of the webpack compilation
  const stopPath = path.dirname(this.rootContext);

  // If the module context is a resolved symlink outside the `rootContext` path,
  // then we will never find the `stopPath` - so we also halt when we hit the root.
  // Note that there is a potential that the wrong `package.json` is found in some pathalogical cases,
  // such as a folder that is conceptually a package + does not have an ancestor `package.json`,
  // but there exists a `package.json` higher up.
  // This might happen if you have a folder of utility JS files that you symlink but did not organize as a package.
  // We consider this an unsupported edge case for now.
  while (searchPath !== stopPath && searchPath !== previousSearchPath) {
    // If we have already determined the `package.json` type for this path we can stop searching.
    // We do however still need to cache the found value,
    // from the `resourcePath` folder up to the matching `searchPath`,
    // to avoid retracing these steps when processing sibling resources.
    if (packageJsonTypeMap.has(searchPath)) {
      packageJsonType = packageJsonTypeMap.get(searchPath);

      let currentPath = resourceContext;
      while (currentPath !== searchPath) {
        // We set the found type at least level from `resourcePath` folder up to the matching `searchPath`
        packageJsonTypeMap.set(currentPath, packageJsonType);
        currentPath = path.dirname(currentPath);
      }
      break;
    }

    let packageJsonPath = path.join(searchPath, 'package.json');
    try {
      const packageSource = await fsPromises.readFile(packageJsonPath, 'utf-8');
      try {
        const packageObject = JSON.parse(packageSource);

        // Any package.json is sufficient as long as it can be parsed.
        // If it does not explicitly have a `type: "module"` it will be assumed to be CommonJS.
        packageJsonType = typeof packageObject.type === 'string' ? packageObject.type : '';
        packageJsonTypeMap.set(searchPath, packageJsonType);

        // We set the type in the cache for all paths from the `resourcePath` folder,
        // up to the matching `searchPath` to avoid retracing these steps when processing sibling resources.
        let currentPath = resourceContext;
        while (currentPath !== searchPath) {
          packageJsonTypeMap.set(currentPath, packageJsonType);
          currentPath = path.dirname(currentPath);
        }
      } catch (e) {
        // `package.json` exists but could not be parsed.
        // We track it as a dependency so we can reload if this file changes.
      }

      this.addDependency(packageJsonPath);
      break;
    } catch (e) {
      // `package.json` does not exist.
      // We track it as a missing dependency so we can reload if this file is added.
      this.addMissingDependency(packageJsonPath);
    }

    // Try again at the next level up
    previousSearchPath = searchPath;
    searchPath = path.dirname(searchPath);
  }

  // Check `package.json` for the `type` field -
  // fallback to use `cjs` for anything ambiguous.
  return packageJsonType === 'module' ? 'esm' : 'cjs';
}

module.exports = getModuleSystem;