From c32a86335921179fbfd4c45182fef1ef15d647b5 Mon Sep 17 00:00:00 2001 From: Myles Borins Date: Tue, 26 Feb 2019 03:25:23 -0500 Subject: [PATCH] esm: add experimental .json support to loader With the new flag `--experimental-json-modules` it is now possible to import .json files. It piggy backs on the current cjs loader implementation, so it only exports a default. This is a bit of a hack, and it should potentially have it's own loader, especially if we change the cjs loader at all. The behavior for .json in the cjs loader matches the current planned behavior if json modules were to be standardized, specifically that a .json module only exports a default. Refs: https://github.com/nodejs/modules/issues/255 Refs: https://github.com/whatwg/html/issues/4315 Refs: https://github.com/w3c/webcomponents/issues/770 --- lib/internal/modules/esm/default_resolve.js | 11 ++++++ lib/internal/modules/esm/translators.js | 38 ++++++++++++++++++- src/node_options.cc | 4 ++ src/node_options.h | 1 + test/es-module/test-esm-json-cache.mjs | 26 +++++++++++++ test/es-module/test-esm-json.mjs | 9 +++++ .../es-modules/json-cache/another.cjs | 7 ++++ test/fixtures/es-modules/json-cache/mod.cjs | 7 ++++ test/fixtures/es-modules/json-cache/test.json | 5 +++ test/fixtures/experimental.json | 3 ++ 10 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 test/es-module/test-esm-json-cache.mjs create mode 100644 test/es-module/test-esm-json.mjs create mode 100644 test/fixtures/es-modules/json-cache/another.cjs create mode 100644 test/fixtures/es-modules/json-cache/mod.cjs create mode 100644 test/fixtures/es-modules/json-cache/test.json create mode 100644 test/fixtures/experimental.json 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 456f541ee6..243d33e7e8 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,12 +14,13 @@ 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 readFileAsync = promisify(fs.readFile); const StringReplace = Function.call.bind(String.prototype.replace); +const JsonParse = JSON.parse; const debug = debuglog('esm'); @@ -94,3 +96,35 @@ translators.set('builtin', async function(url) { reflect.exports.default.set(module.exports); }); }); + +// Strategy for loading a JSON file +translators.set('json', async (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 74a2c54f0d..8f8b108c95 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 862514f2a7..de1f7aea40 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -85,6 +85,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 +}