From caf96906d9e48efc3e877183569f0cefaff365f9 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 | 13 ++++++++++- lib/internal/modules/esm/translators.js | 24 +++++++++++++++++++-- src/node_options.cc | 4 ++++ src/node_options.h | 1 + test/es-module/test-esm-json.mjs | 12 +++++++++++ test/fixtures/experimental.json | 3 +++ 6 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 test/es-module/test-esm-json.mjs 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..bacb813a16 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'); @@ -29,11 +30,20 @@ const legacyExtensionFormatMap = { '__proto__': null, '.cjs': 'commonjs', '.js': 'commonjs', - '.json': 'commonjs', '.mjs': 'module', '.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) @@ -103,6 +113,7 @@ function getModuleFormat(url, isMain, parentURL) { return format; } + function resolve(specifier, parentURL) { if (NativeModule.canBeRequiredByUsers(specifier)) { return { diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index 456f541ee6..8873e84316 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,21 @@ 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 content = await readFileAsync(pathname, 'utf-8'); + return createDynamicModule(['default'], url, (reflect) => { + debug(`Parsing JSONModule ${url}`); + try { + const exports = JsonParse(stripBOM(content)); + reflect.exports.default.set(exports); + } catch (err) { + err.message = pathname + ': ' + err.message; + throw err; + } + }); +}); diff --git a/src/node_options.cc b/src/node_options.cc index 413169fde4..8c338c562f 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -212,6 +212,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 83c3be8674..5ce59a7d00 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 module_type; std::string experimental_policy; diff --git a/test/es-module/test-esm-json.mjs b/test/es-module/test-esm-json.mjs new file mode 100644 index 0000000000..9c23b44b0b --- /dev/null +++ b/test/es-module/test-esm-json.mjs @@ -0,0 +1,12 @@ +// 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'; +import * as namespace from '../fixtures/experimental.json'; + +strictEqual(secret.ofLife, 42); +strictEqual(namespace.ofLife, undefined); +strictEqual(namespace.default.ofLife, 42); 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 +}