UNPKG

@chialab/cjs-to-esm

Version:

A commonjs to esm converter.

418 lines (362 loc) 13.7 kB
import { TokenType, parse, walk, getBlock, getStatement, parseCommonjs, parseEsm, createEmptySourcemapComment } from '@chialab/estransform'; export const REQUIRE_REGEX = /([^.\w$]|^)require\s*\((['"])(.*?)\2\)/g; export const UMD_REGEXES = [ /\btypeof\s+(module\.exports|module|exports)\s*===?\s*['|"]object['|"]/, /['|"]object['|"]\s*===?\s*typeof\s+(module\.exports|module|exports)/, /\btypeof\s+define\s*===?\s*['|"]function['|"]/, /['|"]function['|"]\s*===?\s*typeof\s+define/, ]; export const UMD_GLOBALS = ['globalThis', 'global', 'self', 'window']; export const ESM_KEYWORDS = /((?:^\s*|;\s*)(\bimport\s*(\{.*?\}\s*from|\s[\w$]+\s+from|\*\s*as\s+[^\s]+\s+from)?\s*['"])|((?:^\s*|;\s*)export(\s+(default|const|var|let|function|class)[^\w$]|\s*\{)))/m; export const EXPORTS_KEYWORDS = /\b(module\.exports\b|exports\b)/; export const CJS_KEYWORDS = /\b(module\.exports\b|exports\b|require[.(])/; export const THIS_PARAM = /(}\s*\()this(,|\))/g; export const REQUIRE_FUNCTION = '__cjs_default__'; export const HELPER_MODULE = '__cjs_helper__.js'; export const GLOBAL_HELPER = `((typeof window !== 'undefined' && window) || (typeof self !== 'undefined' && self) || (typeof global !== 'undefined' && global) || (typeof globalThis !== 'undefined' && globalThis) || {})`; export const REQUIRE_HELPER = `function ${REQUIRE_FUNCTION}(requiredModule) { var Object = ${GLOBAL_HELPER}.Object; var isEsModule = false; var specifiers = Object.create(null); var hasNamedExports = false; var hasDefaultExport = false; Object.defineProperty(specifiers, '__esModule', { value: true, enumerable: false, configurable: true, }); if (requiredModule) { var names = Object.getOwnPropertyNames(requiredModule);; names.forEach(function(k) { if (k === 'default') { hasDefaultExport = true; } else if (!hasNamedExports && k != '__esModule') { try { hasNamedExports = requiredModule[k] != null; } catch (err) { // } } Object.defineProperty(specifiers, k, { get: function () { return requiredModule[k]; }, enumerable: true, configurable: false, }); }); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(requiredModule); symbols.forEach(function(k) { Object.defineProperty(specifiers, k, { get: function () { return requiredModule[k]; }, enumerable: false, configurable: false, }); }); } Object.preventExtensions(specifiers); Object.seal(specifiers); if (Object.freeze) { Object.freeze(specifiers); } } if (hasNamedExports) { return specifiers; } if (hasDefaultExport) { if (Object.isExtensible(specifiers.default) && !('default' in specifiers.default)) { Object.defineProperty(specifiers.default, 'default', { value: specifiers.default, configurable: false, enumerable: false, }) } return specifiers.default; } return specifiers; } `; /** * Create an ESM module that exports the helper with an empty sourcemap. */ export function createRequireHelperModule() { return `export default ${REQUIRE_HELPER};\n${createEmptySourcemapComment()}`; } /** * Check if there are chanches that the provided code is a commonjs module. * @param {string} code */ export async function maybeCommonjsModule(code) { if (!CJS_KEYWORDS.test(code)) { return false; } try { const [imports, exports] = await parseEsm(code); if (imports.length !== 0 || exports.length !== 0) { return false; } } catch (err) { return false; } // es-module-parse seems to not detect deconstructed exports if (code.match(/\bexport\s+const\s*{/)) { return false; } return true; } /** * Check if there are chanches that the provided code is both a esm and commonjs module. * @param {string} code */ export async function maybeMixedModule(code) { if (!CJS_KEYWORDS.test(code)) { return false; } try { const [imports, exports] = await parseEsm(code); return (imports.length !== 0 || exports.length !== 0); } catch(err) { // } return false; } /** * Check if an expression is a require call. * @param {import('@chialab/estransform').TokenProcessor} processor */ function isRequireCallExpression(processor) { return processor.matches4(TokenType.name, TokenType.parenL, TokenType.string, TokenType.parenR) && processor.identifierNameAtIndex(processor.currentIndex()) === 'require'; } /** * @typedef {(specifier: string) => boolean|Promise<boolean>} IgnoreCallback */ /** * @typedef {Object} TransformOptions * @property {boolean} [sourcemap] * @property {string} [source] * @property {boolean} [sourcesContent] * @property {IgnoreCallback} [ignore] * @property {boolean} [helperModule] * @property {boolean} [ignoreTryCatch] */ /** * @param {string} code * @param {TransformOptions|undefined} options */ export async function transform(code, { sourcemap = true, source, sourcesContent = false, ignore = () => false, helperModule = false, ignoreTryCatch = true } = {}) { if (await maybeMixedModule(code)) { throw new Error('Cannot convert mixed modules'); } const specs = new Map(); const ns = new Map(); const { helpers, processor } = await parse(code, source); const isUmd = UMD_REGEXES.some((regex) => regex.test(code)); let insertHelper = false; if (!isUmd) { /** * @type {*[]} */ const ignoredExpressions = []; if (ignoreTryCatch) { let openBlocks = 0; await walk(processor, (token) => { if (token.type === TokenType._try) { openBlocks++; return; } if (token.type === TokenType._catch) { openBlocks--; return; } if (openBlocks && isRequireCallExpression(processor)) { ignoredExpressions.push(processor.currentIndex()); } }); } await walk(processor, (token, index) => { if (!isRequireCallExpression(processor) || ignoredExpressions.includes(index)) { return; } const specifierToken = processor.tokens[index + 2]; const specifier = processor.stringValueAtIndex(index + 2); return (async () => { let spec = specs.get(specifier); if (!spec) { let id = `$cjs$${specifier.replace(/[^\w_$]+/g, '_')}`; const count = (ns.get(id) || 0) + 1; ns.set(id, count); if (count > 1) { id += count; } if (await ignore(specifier)) { return; } spec = { id, specifier }; specs.set(specifier, spec); } insertHelper = true; helpers.overwrite(token.start, token.end, REQUIRE_FUNCTION); processor.nextToken(); processor.nextToken(); helpers.overwrite(specifierToken.start, specifierToken.end, `typeof ${spec.id} !== 'undefined' ? ${spec.id} : {}`); processor.nextToken(); })(); }); } const { exports, reexports } = await parseCommonjs(code); const named = exports.filter((entry) => entry !== '__esModule' && entry !== 'default'); const isEsModule = exports.includes('__esModule'); const hasDefault = exports.includes('default'); if (isUmd) { let endDefinition = code.indexOf('\'use strict\';'); if (endDefinition === -1) { endDefinition = code.indexOf('"use strict";'); } if (endDefinition === -1) { endDefinition = code.length; } helpers.prepend(`var __umdGlobal = ${GLOBAL_HELPER}; var __umdExports = []; var __umdRoot = new Proxy(__umdGlobal, { get: function(target, name) { var value = Reflect.get(target, name); if (__umdExports.indexOf(name) !== -1) { return value; } if (typeof value === 'function' && !value.prototype) { return value.bind(__umdGlobal); } return value; }, set: function(target, name, value) { __umdExports.push(name); return Reflect.set(target, name, value); }, }); var __umdFunction = function ProxyFunction(code) { return __umdGlobal.Function(code).bind(__umdRoot); }; __umdFunction.prototype = Function.prototype; (function(window, global, globalThis, self, module, exports, Function) { `); helpers.append(` }).call(__umdRoot, __umdRoot, __umdRoot, __umdRoot, __umdRoot, undefined, undefined, __umdFunction); export default (__umdExports.length !== 1 && __umdRoot[__umdExports[0]] !== __umdRoot[__umdExports[1]] ? __umdRoot : __umdRoot[__umdExports[0]]);`); // replace the usage of `this` as global object because is not supported in esm let thisMatch = THIS_PARAM.exec(code); while (thisMatch) { helpers.overwrite(thisMatch.index, thisMatch.index + thisMatch[0].length, `${thisMatch[1]}this || __umdGlobal${thisMatch[2]}`); thisMatch = THIS_PARAM.exec(code); } } else if (exports.length > 0 || reexports.length > 0) { helpers.prepend(`var global = ${GLOBAL_HELPER}; var exports = {}; var module = { get exports() { return exports; }, set exports(value) { exports = value; }, }; `); if (named.length) { const conditions = ['Object.isExtensible(module.exports)']; if (!hasDefault && !isEsModule) { // add an extra conditions for some edge cases not handled by the cjs lexer // such as an object exports that has a function as first member. conditions.push(`Object.keys(module.exports).length === ${named.length}`); } helpers.append(`\nvar ${named.map((name, index) => `__export${index}`).join(', ')}; if (${conditions.join(' && ')}) { ${named.map((name, index) => `__export${index} = module.exports['${name}'];`).join('\n ')} }`); helpers.append(`\nexport { ${named.map((name, index) => `__export${index} as ${name}`).join(', ')} }`); } if (isEsModule) { if (!isUmd && (hasDefault || named.length === 0)) { helpers.append('\nexport default (module.exports != null && typeof module.exports === \'object\' && \'default\' in module.exports ? module.exports.default : module.exports);'); } } else { helpers.append('\nexport default module.exports;'); } reexports.forEach((reexport) => { helpers.append(`\nexport * from '${reexport}';`); }); } else if (EXPORTS_KEYWORDS.test(code)) { helpers.prepend(`var global = ${GLOBAL_HELPER}; var exports = {}; var module = { get exports() { return exports; }, set exports(value) { exports = value; }, }; `); helpers.append('\nexport default module.exports;'); } if (insertHelper) { if (helperModule) { helpers.prepend(`import ${REQUIRE_FUNCTION} from './${HELPER_MODULE}';\n`); } else { helpers.prepend(`// Require helper for interop\n${REQUIRE_HELPER}`); } } specs.forEach((spec) => { helpers.prepend(`import * as ${spec.id} from "${spec.specifier}";\n`); }); if (!helpers.isDirty()) { return; } return helpers.generate({ sourcemap, sourcesContent, }); } /** * Wrap with a try catch block any require call. * @param {string} code * @param {{ sourcemap?: boolean, source?: string; sourcesContent?: boolean }} options */ export async function wrapDynamicRequire(code, { sourcemap = true, source, sourcesContent = false } = {}) { const { helpers, processor } = await parse(code, source); await walk(processor, (token, index) => { if (!processor.matches5(TokenType._if, TokenType.parenL, TokenType._typeof, TokenType.name, TokenType.equality)) { return; } const identifier = processor.identifierNameAtIndex(index + 3); if (identifier !== 'require') { return; } getBlock(processor, TokenType.parenL, TokenType.parenR); processor.nextToken(); const tokens = []; if (processor.currentToken() && processor.currentToken().type === TokenType.braceL) { tokens.push(...getBlock(processor).slice(1, -1)); } else { tokens.push(...getStatement(processor)); } const startToken = tokens[0]; const endToken = tokens[tokens.length - 1]; helpers.prepend('(() => { try { return (() => {', startToken.start); helpers.append('})(); } catch(err) {} })();', endToken.end); }); if (!helpers.isDirty()) { return; } return helpers.generate({ sourcemap, sourcesContent, }); }