ui5-tooling-modules
Version:
UI5 CLI extensions to load and convert node modules as UI5 AMD-like modules
119 lines (118 loc) • 6.11 kB
JavaScript
/**
* 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;
},
};
};