node/lib/internal/modules/esm/load.js

238 lines
6.2 KiB
JavaScript

'use strict';
const {
RegExpPrototypeExec,
} = primordials;
const {
kEmptyObject,
} = require('internal/util');
const { defaultGetFormat } = require('internal/modules/esm/get_format');
const { validateAttributes, emitImportAssertionWarning } = require('internal/modules/esm/assert');
const { getOptionValue } = require('internal/options');
const { readFileSync } = require('fs');
const defaultType =
getOptionValue('--experimental-default-type');
const { Buffer: { from: BufferFrom } } = require('buffer');
const { URL } = require('internal/url');
const {
ERR_INVALID_URL,
ERR_UNKNOWN_MODULE_FORMAT,
ERR_UNSUPPORTED_ESM_URL_SCHEME,
} = require('internal/errors').codes;
const {
dataURLProcessor,
} = require('internal/data_url');
/**
* @param {URL} url URL to the module
* @param {ESModuleContext} context used to decorate error messages
* @returns {Promise<{ responseURL: string, source: string | BufferView }>}
*/
async function getSource(url, context) {
const { protocol, href } = url;
const responseURL = href;
let source;
if (protocol === 'file:') {
const { readFile: readFileAsync } = require('internal/fs/promises').exports;
source = await readFileAsync(url);
} else if (protocol === 'data:') {
const result = dataURLProcessor(url);
if (result === 'failure') {
throw new ERR_INVALID_URL(responseURL, null);
}
source = BufferFrom(result.body);
} else {
const supportedSchemes = ['file', 'data'];
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(url, supportedSchemes);
}
return { __proto__: null, responseURL, source };
}
/**
* @param {URL} url URL to the module
* @param {ESModuleContext} context used to decorate error messages
* @returns {{ responseURL: string, source: string | BufferView }}
*/
function getSourceSync(url, context) {
const { protocol, href } = url;
const responseURL = href;
let source;
if (protocol === 'file:') {
source = readFileSync(url);
} else if (protocol === 'data:') {
const result = dataURLProcessor(url);
if (result === 'failure') {
throw new ERR_INVALID_URL(responseURL);
}
source = BufferFrom(result.body);
} else {
const supportedSchemes = ['file', 'data'];
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(url, supportedSchemes);
}
return { __proto__: null, responseURL, source };
}
/**
* Node.js default load hook.
* @param {string} url
* @param {LoadContext} context
* @returns {LoadReturn}
*/
async function defaultLoad(url, context = kEmptyObject) {
let responseURL = url;
let {
importAttributes,
format,
source,
} = context;
if (importAttributes == null && !('importAttributes' in context) && 'importAssertions' in context) {
emitImportAssertionWarning();
importAttributes = context.importAssertions;
// Alias `importAssertions` to `importAttributes`
context = {
...context,
importAttributes,
};
}
const urlInstance = new URL(url);
throwIfUnsupportedURLScheme(urlInstance);
if (urlInstance.protocol === 'node:') {
source = null;
format ??= 'builtin';
} else if (format !== 'commonjs' || defaultType === 'module') {
if (source == null) {
({ responseURL, source } = await getSource(urlInstance, context));
context = { __proto__: context, source };
}
if (format == null) {
// Now that we have the source for the module, run `defaultGetFormat` to detect its format.
format = await defaultGetFormat(urlInstance, context);
if (format === 'commonjs') {
// For backward compatibility reasons, we need to discard the source in
// order for the CJS loader to re-fetch it.
source = null;
}
}
}
validateAttributes(url, format, importAttributes);
return {
__proto__: null,
format,
responseURL,
source,
};
}
/**
* @typedef LoadContext
* @property {string} [format] A hint (possibly returned from `resolve`)
* @property {string | Buffer | ArrayBuffer} [source] source
* @property {Record<string, string>} [importAttributes] import attributes
*/
/**
* @typedef LoadReturn
* @property {string} format format
* @property {URL['href']} responseURL The module's fully resolved URL
* @property {Buffer} source source
*/
/**
* @param {URL['href']} url
* @param {LoadContext} [context]
* @returns {LoadReturn}
*/
function defaultLoadSync(url, context = kEmptyObject) {
let responseURL = url;
const { importAttributes } = context;
let {
format,
source,
} = context;
const urlInstance = new URL(url);
throwIfUnsupportedURLScheme(urlInstance, false);
if (urlInstance.protocol === 'node:') {
source = null;
} else if (source == null) {
({ responseURL, source } = getSourceSync(urlInstance, context));
context.source = source;
}
format ??= defaultGetFormat(urlInstance, context);
validateAttributes(url, format, importAttributes);
return {
__proto__: null,
format,
responseURL,
source,
};
}
/**
* throws an error if the protocol is not one of the protocols
* that can be loaded in the default loader
* @param {URL} parsed
*/
function throwIfUnsupportedURLScheme(parsed) {
// Avoid accessing the `protocol` property due to the lazy getters.
const protocol = parsed?.protocol;
if (
protocol &&
protocol !== 'file:' &&
protocol !== 'data:' &&
protocol !== 'node:' &&
(
protocol !== 'https:' &&
protocol !== 'http:'
)
) {
const schemes = ['file', 'data', 'node'];
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(parsed, schemes);
}
}
/**
* For a falsy `format` returned from `load`, throw an error.
* This could happen from either a custom user loader _or_ from the default loader, because the default loader tries to
* determine formats for data URLs.
* @param {string} url The resolved URL of the module
* @param {null | undefined | false | 0 | -0 | 0n | ''} format Falsy format returned from `load`
*/
function throwUnknownModuleFormat(url, format) {
const dataUrl = RegExpPrototypeExec(
/^data:([^/]+\/[^;,]+)(?:[^,]*?)(;base64)?,/,
url,
);
throw new ERR_UNKNOWN_MODULE_FORMAT(
dataUrl ? dataUrl[1] : format,
url);
}
module.exports = {
defaultLoad,
defaultLoadSync,
getSourceSync,
throwUnknownModuleFormat,
};