Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/module/generate-fed-package-json.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const fse = require('fs-extra');
const { globSync } = require('glob');
const path = require('path');
const { default: getDynamicModuleMap } = require('../../scripts/parse-dynamic-modules.mjs');

const root = process.cwd();

Expand Down Expand Up @@ -60,12 +61,29 @@ async function generatePackages(files) {
return Promise.all(cmds);
}

async function generateDynamicModuleMap() {
const moduleMap = getDynamicModuleMap(root);
// eslint-disable-next-line no-console
console.log('Generating dynamic module map for', Object.keys(moduleMap).length, 'modules');

if (Object.keys(moduleMap).length === 0) {
return Promise.resolve();
}

const moduleMapSorted = Object.keys(moduleMap)
.sort()
.reduce((acc, key) => ({ ...acc, [key]: moduleMap[key] }), {});

return fse.writeJSON(path.resolve(root, 'dist/dynamic-modules.json'), moduleMapSorted, { spaces: 2 });
}

async function run(files) {
try {
await generatePackages(files);
if (indexTypings.length === 1) {
copyTypings(indexTypings, root);
}
await generateDynamicModuleMap()
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
Expand Down
164 changes: 164 additions & 0 deletions scripts/parse-dynamic-modules.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/* eslint-disable no-console */
import fs from 'fs-extra';
import { createRequire } from 'node:module';
import path from 'node:path';
import { glob } from 'glob';
import ts from 'typescript';

const require = createRequire(import.meta.url);

/** @type {ts.CompilerOptions} */
const tsConfigBase = require(path.resolve(import.meta.dirname, '../packages/module/tsconfig.cjs.json'));

/** @type {ts.CompilerOptions} */
const defaultCompilerOptions = {
target: tsConfigBase.target,
module: tsConfigBase.module,
moduleResolution: tsConfigBase.moduleResolution,
esModuleInterop: tsConfigBase.esModuleInterop,
allowJs: true,
strict: false,
skipLibCheck: true,
skipDefaultLibCheck: true,
noEmit: true,
types: []
};

/**
* 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.
*
* @param {string} basePath
* @param {string} indexModule
* @param {string} resolutionField
* @param {ts.CompilerOptions} tsCompilerOptions
* @returns {Record<string, string>}
*/
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 {};
}

/** @type {Record<string, string>} */
const dynamicModulePathToPkgDir = glob.sync(`${basePath}/dist/dynamic/**/package.json`).reduce((acc, pkgFile) => {
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();

/** @param {ts.SourceFile} sourceFile */
const getExportNames = (sourceFile) =>
typeChecker.getExportsOfModule(typeChecker.getSymbolAtLocation(sourceFile)).map((symbol) => symbol.getName());

const indexModuleExports = getExportNames(program.getSourceFile(indexModulePath));

/** @type {Record<string, string[]>} */
const dynamicModuleExports = dynamicModulePaths.reduce((acc, modulePath) => {
acc[modulePath] = getExportNames(program.getSourceFile(modulePath));
return acc;
}, {});

/** @param {string[]} modulePaths */
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;
}, {});
};

export default getDynamicModuleMap;
Loading