From 10705ef9d8a008eac080f8ebe97252379ee84339 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 70c68ae0f0..963a4844a9 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(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 a5d123f666..df3d11a4ba 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 312751822e..399c46be50 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 +}