1'use strict';
2
3const {
4  RegExpPrototypeExec,
5  ObjectPrototypeHasOwnProperty,
6  PromisePrototypeThen,
7  PromiseResolve,
8  StringPrototypeIncludes,
9  StringPrototypeCharCodeAt,
10  StringPrototypeSlice,
11} = primordials;
12const { getOptionValue } = require('internal/options');
13const {
14  extensionFormatMap,
15  getFormatOfExtensionlessFile,
16  getLegacyExtensionFormat,
17  mimeToFormat,
18} = require('internal/modules/esm/formats');
19
20const experimentalNetworkImports =
21  getOptionValue('--experimental-network-imports');
22const experimentalSpecifierResolution =
23  getOptionValue('--experimental-specifier-resolution');
24const defaultTypeFlag = getOptionValue('--experimental-default-type');
25// The next line is where we flip the default to ES modules someday.
26const defaultType = defaultTypeFlag === 'module' ? 'module' : 'commonjs';
27const { getPackageType } = require('internal/modules/esm/resolve');
28const { fileURLToPath } = require('internal/url');
29const { ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes;
30
31const protocolHandlers = {
32  '__proto__': null,
33  'data:': getDataProtocolModuleFormat,
34  'file:': getFileProtocolModuleFormat,
35  'http:': getHttpProtocolModuleFormat,
36  'https:': getHttpProtocolModuleFormat,
37  'node:'() { return 'builtin'; },
38};
39
40/**
41 * @param {URL} parsed
42 * @returns {string | null}
43 */
44function getDataProtocolModuleFormat(parsed) {
45  const { 1: mime } = RegExpPrototypeExec(
46    /^([^/]+\/[^;,]+)(?:[^,]*?)(;base64)?,/,
47    parsed.pathname,
48  ) || [ null, null, null ];
49
50  return mimeToFormat(mime);
51}
52
53const DOT_CODE = 46;
54const SLASH_CODE = 47;
55
56/**
57 * Returns the file extension from a URL. Should give similar result to
58 * `require('node:path').extname(require('node:url').fileURLToPath(url))`
59 * when used with a `file:` URL.
60 * @param {URL} url
61 * @returns {string}
62 */
63function extname(url) {
64  const { pathname } = url;
65  for (let i = pathname.length - 1; i > 0; i--) {
66    switch (StringPrototypeCharCodeAt(pathname, i)) {
67      case SLASH_CODE:
68        return '';
69
70      case DOT_CODE:
71        return StringPrototypeCharCodeAt(pathname, i - 1) === SLASH_CODE ? '' : StringPrototypeSlice(pathname, i);
72    }
73  }
74  return '';
75}
76
77/**
78 * Determine whether the given file URL is under a `node_modules` folder.
79 * This function assumes that the input has already been verified to be a `file:` URL,
80 * and is a file rather than a folder.
81 * @param {URL} url
82 */
83function underNodeModules(url) {
84  if (url.protocol !== 'file:') { return false; } // We determine module types for other protocols based on MIME header
85
86  return StringPrototypeIncludes(url.pathname, '/node_modules/');
87}
88
89/**
90 * @param {URL} url
91 * @param {{parentURL: string}} context
92 * @param {boolean} ignoreErrors
93 * @returns {string}
94 */
95function getFileProtocolModuleFormat(url, context, ignoreErrors) {
96  const ext = extname(url);
97
98  if (ext === '.js') {
99    const packageType = getPackageType(url);
100    if (packageType !== 'none') {
101      return packageType;
102    }
103    // The controlling `package.json` file has no `type` field.
104    if (defaultType === 'module') {
105      // An exception to the type flag making ESM the default everywhere is that package scopes under `node_modules`
106      // should retain the assumption that a lack of a `type` field means CommonJS.
107      return underNodeModules(url) ? 'commonjs' : 'module';
108    }
109    return 'commonjs';
110  }
111
112  if (ext === '') {
113    const packageType = getPackageType(url);
114    if (defaultType === 'commonjs') { // Legacy behavior
115      if (packageType === 'none' || packageType === 'commonjs') {
116        return 'commonjs';
117      } // Else packageType === 'module'
118      return getFormatOfExtensionlessFile(url);
119    } // Else defaultType === 'module'
120    if (underNodeModules(url)) { // Exception for package scopes under `node_modules`
121      return packageType === 'module' ? getFormatOfExtensionlessFile(url) : 'commonjs';
122    }
123    if (packageType === 'none' || packageType === 'module') {
124      return getFormatOfExtensionlessFile(url);
125    } // Else packageType === 'commonjs'
126    return 'commonjs';
127  }
128
129  const format = extensionFormatMap[ext];
130  if (format) { return format; }
131
132  if (experimentalSpecifierResolution !== 'node') {
133    // Explicit undefined return indicates load hook should rerun format check
134    if (ignoreErrors) { return undefined; }
135    const filepath = fileURLToPath(url);
136    throw new ERR_UNKNOWN_FILE_EXTENSION(ext, filepath);
137  }
138
139  return getLegacyExtensionFormat(ext) ?? null;
140}
141
142/**
143 * @param {URL} url
144 * @param {{parentURL: string}} context
145 * @returns {Promise<string> | undefined} only works when enabled
146 */
147function getHttpProtocolModuleFormat(url, context) {
148  if (experimentalNetworkImports) {
149    const { fetchModule } = require('internal/modules/esm/fetch_module');
150    return PromisePrototypeThen(
151      PromiseResolve(fetchModule(url, context)),
152      (entry) => {
153        return mimeToFormat(entry.headers['content-type']);
154      },
155    );
156  }
157}
158
159/**
160 * @param {URL} url
161 * @param {{parentURL: string}} context
162 * @returns {Promise<string> | string | undefined} only works when enabled
163 */
164function defaultGetFormatWithoutErrors(url, context) {
165  const protocol = url.protocol;
166  if (!ObjectPrototypeHasOwnProperty(protocolHandlers, protocol)) {
167    return null;
168  }
169  return protocolHandlers[protocol](url, context, true);
170}
171
172/**
173 * @param {URL} url
174 * @param {{parentURL: string}} context
175 * @returns {Promise<string> | string | undefined} only works when enabled
176 */
177function defaultGetFormat(url, context) {
178  const protocol = url.protocol;
179  if (!ObjectPrototypeHasOwnProperty(protocolHandlers, protocol)) {
180    return null;
181  }
182  return protocolHandlers[protocol](url, context, false);
183}
184
185module.exports = {
186  defaultGetFormat,
187  defaultGetFormatWithoutErrors,
188  extensionFormatMap,
189  extname,
190};
191