UNPKG

ui5-tooling-modules

Version:
119 lines (118 loc) 6.11 kB
/** * Rollup plugin: produce a clean ES-module surface for every entry. * * For each entry id rollup is asked to resolve, this plugin returns a * proxy id (the original id with a `?inject-esmodule` suffix). The proxy's * generated source loads the real entry once and then re-exports it in a * shape that consumers can rely on: * * - if the entry has a default export (or is CommonJS converted to one * by `@rollup/plugin-commonjs`), the proxy emits `export default ...` * plus a non-enumerable `__esModule = true` marker so downstream * `_interopRequireDefault` callers behave correctly. * - if the entry only has named exports, the proxy emits * `export * from <entry>` plus `export const __esModule = true`. * * Special-case behaviour: * - frozen default exports (e.g. `module.exports = Object.freeze(...)`) are * re-frozen after copying so consumers do not see different identities. * - some libraries (`fetch-mock`, `chart.js`, ...) ship a default that is * itself a function whose properties carry the named API; the proxy * detects those and copies missing named properties onto the default. * - UI5 web component entries (`moduleInfo.meta.ui5`) are skipped because * `rollup-plugin-webcomponents` already handles them. * - entries with import attributes are passed through unchanged so attr * semantics are preserved. * * Inspired by https://rollupjs.org/plugin-development/#resolveid (the * Polyfill Injection example). * * @returns {import('rollup').Plugin} */ const PROXY_SUFFIX = "?inject-esmodule"; module.exports = function (/* { log } = {} */) { return { name: "inject-esmodule", async resolveId(source, importer, options) { if (options.isEntry) { // Determine what the actual entry would have been. We need // "skipSelf" to avoid an infinite loop. const resolution = await this.resolve(source, importer, { skipSelf: true, ...options, }); // If it cannot be resolved or is external, just return it // so that Rollup can display an error if (!resolution || resolution.external) return resolution; // In the load hook of the proxy, we need to know if the // entry has a default export. There, however, we no longer // have the full "resolution" object that may contain // meta-data from other plugins that is only added on first // load. Therefore we trigger loading here. const moduleInfo = await this.load(resolution); // We need to make sure side effects in the original entry // point are respected even for // treeshake.moduleSideEffects: false. "moduleSideEffects" // is a writable property on ModuleInfo. moduleInfo.moduleSideEffects = true; // It is important that the new entry does not start with // \0 and has the same directory as the original one to not // mess up relative external import generation. Also // keeping the name and just adding a "?query" to the end // ensures that preserveModules will generate the original // entry name for this entry. // AND: // we also ignore the modules with UI5 meta (web components) // as they are already handled by the rollup-plugin-webcomponents const hasAttributes = Object.keys(moduleInfo.attributes || {}).length === 0; const isUI5WebComponent = !!moduleInfo.meta?.ui5; if (hasAttributes && !isUI5WebComponent) { return { id: `${resolution.id}${PROXY_SUFFIX}`, }; } } return null; }, load(id) { if (id.endsWith(PROXY_SUFFIX)) { const entryId = id.slice(0, -PROXY_SUFFIX.length); // ModuleInfo.hasDefaultExport is used to determine if the module requires special handling let { hasDefaultExport, exports, meta /*, code: oldCode*/ } = this.getModuleInfo(entryId); let code = ""; // namespace re-exports do not reexport default, so we need special handling here if (hasDefaultExport || meta.commonjs?.hasDefaultExport || meta.commonjs?.isCommonJS === "withRequireFunction") { // we import the default export and assign it to a variable code += `import { default as defExp } from ${JSON.stringify(entryId)};\n`; if (meta.commonjs?.hasNamedExports || exports?.length > 0) { // we import the named exports and assign them to a variable code += `import * as namedExports from ${JSON.stringify(entryId)};\n`; } // in some cases the default exports provides a default property // fetch-mock: ("export { index as default }" instead of "export default index") // other case like chart.js the default export is a function and we need to keep // it, so we only copy the properties from the defExp object if it is frozen code += `const defaultExports = Object.isFrozen(defExp) ? Object.assign({}, defExp?.default || defExp || { __emptyModule: true }) : defExp;\n`; if (meta.commonjs?.hasNamedExports || exports?.length > 0) { // we merge the named exports into the default export (if not already present) code += `Object.keys(namedExports || {}).filter((key) => !defaultExports[key]).forEach((key) => defaultExports[key] = namedExports[key]);\n`; } // we also re-export the default export as "default" to ensure compatibility with _interopRequireDefault // which expects the "default" property to be present for CommonJS interop reasons // ==> NOT NEEDED ANYMORE AS WE HAVE A CONSISTENT __esModule addition for interopRequireDefault // code += `defaultExports.default = Object.assign({}, defExp);\n`; // we set the __esModule flag to true to indicate that this is an ES module code += `Object.defineProperty(defaultExports, "__" + "esModule", { value: true });\n`; // we freeze the object to disallow further modifications code += `export default Object.isFrozen(defExp) ? Object.freeze(defaultExports) : defaultExports;\n`; } else { // just re-export the module as is and set the __esModule flag to true code = `export * from ${JSON.stringify(entryId)};`; code += `export const __esModule = true ;\n`; } return code; } return null; }, }; };