@openshift-console/dynamic-plugin-sdk-webpack
Version:
Provides webpack ConsoleRemotePlugin used to build all dynamic plugin assets.
119 lines (118 loc) • 5.14 kB
JavaScript
/* eslint-disable no-console */
Object.defineProperty(exports, "__esModule", { value: true });
exports.getDynamicModuleMap = void 0;
const fs = require("fs");
const path = require("path");
const glob = require("glob");
const ts = require("typescript");
const defaultCompilerOptions = {
target: ts.ScriptTarget.ES2020,
module: ts.ModuleKind.ESNext,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
allowJs: true,
strict: false,
esModuleInterop: true,
skipLibCheck: true,
noEmit: true,
};
/**
* Map all exports of the given index module to their corresponding dynamic modules.
*
* Example: `@patternfly/react-core` package provides ESModules index at `dist/esm/index.js`
* which exports Alert component related code & types via `dist/esm/components/Alert/index.js`
* module.
*
* Given the example above, this function should return a mapping like so:
* ```js
* {
* Alert: 'dist/dynamic/components/Alert',
* AlertProps: 'dist/dynamic/components/Alert',
* AlertContext: 'dist/dynamic/components/Alert',
* // ...
* }
* ```
*
* The above mapping can be used when generating import statements like so:
* ```ts
* import { Alert } from '@patternfly/react-core/dist/dynamic/components/Alert';
* ```
*
* It may happen that the same export is provided by multiple dynamic modules;
* in such case, the resolution favors modules with most specific file paths, for example
* `dist/dynamic/components/Wizard/hooks` is favored over `dist/dynamic/components/Wizard`.
*
* Dynamic modules nested under `deprecated` or `next` directories are ignored.
*
* If the referenced index module does not exist, an empty object is returned.
*/
const getDynamicModuleMap = (basePath, indexModule = 'dist/esm/index.js', resolutionField = 'module', tsCompilerOptions = defaultCompilerOptions) => {
if (!path.isAbsolute(basePath)) {
throw new Error('Package base path must be absolute');
}
const indexModulePath = path.resolve(basePath, indexModule);
if (!fs.existsSync(indexModulePath)) {
return {};
}
const dynamicModulePathToPkgDir = glob
.sync(`${basePath}/dist/dynamic/**/package.json`)
.reduce((acc, pkgFile) => {
// eslint-disable-next-line
const pkg = require(pkgFile);
const pkgModule = pkg[resolutionField];
if (!pkgModule) {
throw new Error(`Missing field ${resolutionField} in ${pkgFile}`);
}
const pkgResolvedPath = path.resolve(path.dirname(pkgFile), pkgModule);
const pkgRelativePath = path.dirname(path.relative(basePath, pkgFile));
acc[pkgResolvedPath] = pkgRelativePath;
return acc;
}, {});
const dynamicModulePaths = Object.keys(dynamicModulePathToPkgDir);
const compilerHost = ts.createCompilerHost(tsCompilerOptions);
const program = ts.createProgram([indexModulePath, ...dynamicModulePaths], tsCompilerOptions, compilerHost);
const errorDiagnostics = ts
.getPreEmitDiagnostics(program)
.filter((d) => d.category === ts.DiagnosticCategory.Error);
if (errorDiagnostics.length > 0) {
const { getCanonicalFileName, getCurrentDirectory, getNewLine } = compilerHost;
console.error(ts.formatDiagnostics(errorDiagnostics, {
getCanonicalFileName,
getCurrentDirectory,
getNewLine,
}));
throw new Error(`Detected TypeScript errors while parsing modules at ${basePath}`);
}
const typeChecker = program.getTypeChecker();
const getExportNames = (sourceFile) => typeChecker
.getExportsOfModule(typeChecker.getSymbolAtLocation(sourceFile))
.map((symbol) => symbol.getName());
const indexModuleExports = getExportNames(program.getSourceFile(indexModulePath));
const dynamicModuleExports = dynamicModulePaths.reduce((acc, modulePath) => {
acc[modulePath] = getExportNames(program.getSourceFile(modulePath));
return acc;
}, {});
const getMostSpecificModulePath = (modulePaths) => modulePaths.reduce((acc, p) => {
const pathSpecificity = p.split(path.sep).length;
const currSpecificity = acc.split(path.sep).length;
if (pathSpecificity > currSpecificity) {
return p;
}
if (pathSpecificity === currSpecificity) {
return !p.endsWith('index.js') && acc.endsWith('index.js') ? p : acc;
}
return acc;
}, '');
return indexModuleExports.reduce((acc, exportName) => {
const foundModulePaths = Object.keys(dynamicModuleExports).filter((modulePath) => dynamicModuleExports[modulePath].includes(exportName));
const filteredModulePaths = foundModulePaths.filter((modulePath) => {
const dirNames = path.dirname(modulePath).split(path.sep);
return !dirNames.includes('deprecated') && !dirNames.includes('next');
});
if (filteredModulePaths.length > 0) {
acc[exportName] = dynamicModulePathToPkgDir[getMostSpecificModulePath(filteredModulePaths)];
}
return acc;
}, {});
};
exports.getDynamicModuleMap = getDynamicModuleMap;
;