diff --git a/lib/internal/modules/esm/default_resolve.js b/lib/internal/modules/esm/default_resolve.js index 5471ee629a..ef1e1a54fe 100644 --- a/lib/internal/modules/esm/default_resolve.js +++ b/lib/internal/modules/esm/default_resolve.js @@ -10,6 +10,7 @@ const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); const { ERR_INVALID_PACKAGE_CONFIG, ERR_TYPE_MISMATCH, ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes; +const experimentalJsonModules = getOptionValue('--experimental-json-modules'); const { resolve: moduleWrapResolve } = internalBinding('module_wrap'); const { pathToFileURL, fileURLToPath, URL } = require('internal/url'); const asyncESM = require('internal/process/esm_loader'); @@ -34,6 +35,16 @@ const legacyExtensionFormatMap = { '.node': 'commonjs' }; +if (experimentalJsonModules) { + // This is a total hack + Object.assign(extensionFormatMap, { + '.json': 'json' + }); + Object.assign(legacyExtensionFormatMap, { + '.json': 'json' + }); +} + function readPackageConfig(path, parentURL) { const existing = pjsonCache.get(path); if (existing !== undefined) diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index 7b903b1816..5755e4f837 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -3,7 +3,8 @@ const { NativeModule } = require('internal/bootstrap/loaders'); const { ModuleWrap, callbackMap } = internalBinding('module_wrap'); const { - stripShebang + stripShebang, + stripBOM } = require('internal/modules/cjs/helpers'); const CJSModule = require('internal/modules/cjs/loader'); const internalURLModule = require('internal/url'); @@ -13,7 +14,7 @@ const fs = require('fs'); const { SafeMap, } = primordials; -const { URL } = require('url'); +const { fileURLToPath, URL } = require('url'); const { debuglog, promisify } = require('util'); const esmLoader = require('internal/process/esm_loader'); const { @@ -21,6 +22,7 @@ const { } = require('internal/errors').codes; const readFileAsync = promisify(fs.readFile); const StringReplace = Function.call.bind(String.prototype.replace); +const JsonParse = JSON.parse; const debug = debuglog('esm'); @@ -99,3 +101,35 @@ translators.set('builtin', async function builtinStrategy(url) { reflect.exports.default.set(module.exports); }); }); + +// Strategy for loading a JSON file +translators.set('json', async function jsonStrategy(url) { + debug(`Translating JSONModule ${url}`); + debug(`Loading JSONModule ${url}`); + const pathname = fileURLToPath(url); + const modulePath = isWindows ? + StringReplace(pathname, winSepRegEx, '\\') : pathname; + let module = CJSModule._cache[modulePath]; + if (module && module.loaded) { + const exports = module.exports; + return createDynamicModule(['default'], url, (reflect) => { + reflect.exports.default.set(exports); + }); + } + const content = await readFileAsync(pathname, 'utf-8'); + try { + const exports = JsonParse(stripBOM(content)); + module = { + exports, + loaded: true + }; + } catch (err) { + err.message = pathname + ': ' + err.message; + throw err; + } + CJSModule._cache[modulePath] = module; + return createDynamicModule(['default'], url, (reflect) => { + debug(`Parsing JSONModule ${url}`); + reflect.exports.default.set(module.exports); + }); +}); diff --git a/src/node_options.cc b/src/node_options.cc index e966b024fe..0f7471b29c 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -214,6 +214,10 @@ DebugOptionsParser::DebugOptionsParser() { } EnvironmentOptionsParser::EnvironmentOptionsParser() { + AddOption("--experimental-json-modules", + "experimental JSON interop support for the ES Module loader", + &EnvironmentOptions::experimental_json_modules, + kAllowedInEnvironment); AddOption("--experimental-modules", "experimental ES Module support and caching modules", &EnvironmentOptions::experimental_modules, diff --git a/src/node_options.h b/src/node_options.h index f0973679c6..d0db989813 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -91,6 +91,7 @@ class DebugOptions : public Options { class EnvironmentOptions : public Options { public: bool abort_on_uncaught_exception = false; + bool experimental_json_modules = false; bool experimental_modules = false; std::string es_module_specifier_resolution = "explicit"; std::string module_type; diff --git a/test/es-module/test-esm-json-cache.mjs b/test/es-module/test-esm-json-cache.mjs new file mode 100644 index 0000000000..ecd27c5488 --- /dev/null +++ b/test/es-module/test-esm-json-cache.mjs @@ -0,0 +1,26 @@ +// Flags: --experimental-modules --experimental-json-modules +/* eslint-disable node-core/required-modules */ +import '../common/index.mjs'; + +import { strictEqual, deepStrictEqual } from 'assert'; + +import { createRequireFromPath as createRequire } from 'module'; +import { fileURLToPath as fromURL } from 'url'; + +import mod from '../fixtures/es-modules/json-cache/mod.cjs'; +import another from '../fixtures/es-modules/json-cache/another.cjs'; +import test from '../fixtures/es-modules/json-cache/test.json'; + +const require = createRequire(fromURL(import.meta.url)); + +const modCjs = require('../fixtures/es-modules/json-cache/mod.cjs'); +const anotherCjs = require('../fixtures/es-modules/json-cache/another.cjs'); +const testCjs = require('../fixtures/es-modules/json-cache/test.json'); + +strictEqual(mod.one, 1); +strictEqual(another.one, 'zalgo'); +strictEqual(test.one, 'it comes'); + +deepStrictEqual(mod, modCjs); +deepStrictEqual(another, anotherCjs); +deepStrictEqual(test, testCjs); diff --git a/test/es-module/test-esm-json.mjs b/test/es-module/test-esm-json.mjs new file mode 100644 index 0000000000..b140d031ca --- /dev/null +++ b/test/es-module/test-esm-json.mjs @@ -0,0 +1,9 @@ +// Flags: --experimental-modules --experimental-json-modules +/* eslint-disable node-core/required-modules */ + +import '../common/index.mjs'; +import { strictEqual } from 'assert'; + +import secret from '../fixtures/experimental.json'; + +strictEqual(secret.ofLife, 42); diff --git a/test/fixtures/es-modules/json-cache/another.cjs b/test/fixtures/es-modules/json-cache/another.cjs new file mode 100644 index 0000000000..8c8e9f1c0f --- /dev/null +++ b/test/fixtures/es-modules/json-cache/another.cjs @@ -0,0 +1,7 @@ +const test = require('./test.json'); + +module.exports = { + ...test +}; + +test.one = 'it comes'; diff --git a/test/fixtures/es-modules/json-cache/mod.cjs b/test/fixtures/es-modules/json-cache/mod.cjs new file mode 100644 index 0000000000..047cfb24a4 --- /dev/null +++ b/test/fixtures/es-modules/json-cache/mod.cjs @@ -0,0 +1,7 @@ +const test = require('./test.json'); + +module.exports = { + ...test +}; + +test.one = 'zalgo'; diff --git a/test/fixtures/es-modules/json-cache/test.json b/test/fixtures/es-modules/json-cache/test.json new file mode 100644 index 0000000000..120cbb2840 --- /dev/null +++ b/test/fixtures/es-modules/json-cache/test.json @@ -0,0 +1,5 @@ +{ + "one": 1, + "two": 2, + "three": 3 +} diff --git a/test/fixtures/experimental.json b/test/fixtures/experimental.json new file mode 100644 index 0000000000..12611d2385 --- /dev/null +++ b/test/fixtures/experimental.json @@ -0,0 +1,3 @@ +{ + "ofLife": 42 +}