Skip to content

Commit ad8f4cd

Browse files
committed
module: expose module.format
To help users relying on require.cache determine the format of a cached module. Refs: #59868
1 parent 15c276d commit ad8f4cd

File tree

4 files changed

+117
-12
lines changed

4 files changed

+117
-12
lines changed

doc/api/modules.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1152,6 +1152,20 @@ added: v0.1.16
11521152

11531153
The fully resolved filename of the module.
11541154

1155+
### `module.format`
1156+
1157+
<!-- YAML
1158+
added: REPLACEME
1159+
-->
1160+
1161+
* Type: {string|undefined}
1162+
1163+
The [detected format][Determining module system] of the module. Possible values are documented
1164+
in [`load` customization hooks][].
1165+
1166+
This property is not guaranteed to be defined on the `module` object, nor to be accurate. If
1167+
it's undefined, it's likely that Node.js does not yet know the module format.
1168+
11551169
### `module.id`
11561170

11571171
<!-- YAML
@@ -1277,6 +1291,7 @@ This section was moved to
12771291
[`__dirname`]: #__dirname
12781292
[`__filename`]: #__filename
12791293
[`import()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import
1294+
[`load` customization hooks]: module.md#loadurl-context-nextload
12801295
[`module.builtinModules`]: module.md#modulebuiltinmodules
12811296
[`module.children`]: #modulechildren
12821297
[`module.id`]: #moduleid

lib/internal/modules/cjs/loader.js

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,12 @@ function Module(id = '', parent) {
338338
this.filename = null;
339339
this.loaded = false;
340340
this.children = [];
341+
ObjectDefineProperty(this, 'format', {
342+
__proto__: null,
343+
enumerable: true,
344+
configurable: true,
345+
get() { return this[kFormat]; },
346+
});
341347
}
342348

343349
/** @type {Record<string, Module>} */
@@ -1702,43 +1708,51 @@ function wrapSafe(filename, content, cjsModuleInstance, format) {
17021708
* `exports`) to the file. Returns exception, if any.
17031709
* @param {string} content The source code of the module
17041710
* @param {string} filename The file path of the module
1705-
* @param {'module'|'commonjs'|'commonjs-typescript'|'module-typescript'|'typescript'} format
1711+
* @param {'module'|'commonjs'|'commonjs-typescript'|'module-typescript'|'typescript'} fileFormat
17061712
* Intended format of the module.
17071713
* @returns {any}
17081714
*/
1709-
Module.prototype._compile = function(content, filename, format) {
1710-
if (format === 'commonjs-typescript' || format === 'module-typescript' || format === 'typescript') {
1715+
Module.prototype._compile = function(content, filename, fileFormat) {
1716+
let runFormat = fileFormat;
1717+
if (fileFormat === 'commonjs-typescript' || fileFormat === 'module-typescript' || fileFormat === 'typescript') {
17111718
content = stripTypeScriptModuleTypes(content, filename);
1712-
switch (format) {
1719+
switch (fileFormat) {
17131720
case 'commonjs-typescript': {
1714-
format = 'commonjs';
1721+
runFormat = 'commonjs';
17151722
break;
17161723
}
17171724
case 'module-typescript': {
1718-
format = 'module';
1725+
runFormat = 'module';
17191726
break;
17201727
}
17211728
// If the format is still unknown i.e. 'typescript', detect it in
17221729
// wrapSafe using the type-stripped source.
17231730
default:
1724-
format = undefined;
1731+
runFormat = undefined;
17251732
break;
17261733
}
17271734
}
17281735

17291736
let redirects;
17301737

17311738
let compiledWrapper;
1732-
if (format !== 'module') {
1733-
const result = wrapSafe(filename, content, this, format);
1739+
// If the format is unknown, or it's commonjs.
1740+
if (runFormat !== 'module') {
1741+
const result = wrapSafe(filename, content, this, runFormat);
17341742
compiledWrapper = result.function;
17351743
if (result.canParseAsESM) {
1736-
format = 'module';
1744+
runFormat = 'module';
17371745
}
17381746
}
17391747

1740-
if (format === 'module') {
1741-
loadESMFromCJS(this, filename, format, content);
1748+
runFormat ??= 'commonjs';
1749+
this[kFormat] ??= fileFormat || runFormat;
1750+
if (this[kFormat] === 'typescript') {
1751+
this[kFormat] = runFormat === 'module' ? 'module-typescript' : 'commonjs-typescript';
1752+
}
1753+
1754+
if (runFormat === 'module') {
1755+
loadESMFromCJS(this, filename, runFormat, content);
17421756
return;
17431757
}
17441758

@@ -1916,6 +1930,7 @@ Module._extensions['.json'] = function(module, filename) {
19161930
*/
19171931
Module._extensions['.node'] = function(module, filename) {
19181932
// Be aware this doesn't use `content`
1933+
this[kFormat] = 'addon';
19191934
return process.dlopen(module, path.toNamespacedPath(filename));
19201935
};
19211936

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Similar to ./test-module-format.mjs but imports a CJS file instead.
2+
3+
import '../common/index.mjs';
4+
// FIXME(https://github.com/nodejs/node/issues/59666): this only works
5+
// when there are no customization hooks or the hooks do no override the
6+
// source. Otherwise the re-invented require() kicks in which does not
7+
// properly initialize all the properties of the CJS cache entry.
8+
await import('./test-module-format.js');
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
'use strict';
2+
3+
// This file tests that the `format` property is correctly set on
4+
// `cache` entries when loading CommonJS and ES modules.
5+
6+
require('../common');
7+
const assert = require('assert');
8+
const fixtures = require('../common/fixtures.js');
9+
const cache = require('module')._cache;
10+
assert.strictEqual(cache[__filename].format, 'commonjs');
11+
12+
const list = [
13+
{
14+
filename: fixtures.path('es-modules/package-without-type/commonjs.js'),
15+
format: 'commonjs',
16+
},
17+
{
18+
filename: fixtures.path('es-modules/package-without-type/imports-commonjs.cjs'),
19+
format: 'commonjs',
20+
},
21+
{
22+
filename: fixtures.path('es-modules/package-without-type/module.js'),
23+
format: 'module',
24+
},
25+
{
26+
filename: fixtures.path('es-modules/package-without-type/imports-esm.mjs'),
27+
format: 'module',
28+
},
29+
{
30+
filename: fixtures.path('es-modules/package-type-module/esm.js'),
31+
format: 'module',
32+
},
33+
{
34+
filename: fixtures.path('es-modules/package-type-commonjs/index.js'),
35+
format: 'commonjs',
36+
},
37+
{
38+
filename: fixtures.path('es-modules/package-type-module/package.json'),
39+
format: 'json',
40+
},
41+
{
42+
filename: fixtures.path('typescript/ts/module-logger.ts'),
43+
format: 'module-typescript',
44+
},
45+
{
46+
filename: fixtures.path('typescript/mts/test-mts-export-foo.mts'),
47+
format: 'module-typescript',
48+
},
49+
{
50+
filename: fixtures.path('typescript/mts/test-module-export.ts'),
51+
format: 'module-typescript',
52+
},
53+
{
54+
filename: fixtures.path('typescript/cts/test-cts-export-foo.cts'),
55+
format: 'commonjs-typescript',
56+
},
57+
{
58+
filename: fixtures.path('typescript/cts/test-commonjs-export.ts'),
59+
format: 'commonjs-typescript',
60+
},
61+
];
62+
63+
for (const { filename, format } of list) {
64+
assert(!cache[filename], `Expected ${filename} to not be in cache yet`);
65+
require(filename);
66+
assert.strictEqual(cache[filename].format, format, `Unexpected format for ${filename}`);
67+
}

0 commit comments

Comments
 (0)