Skip to content

Commit

Permalink
Generate dist/dynamic-modules.json for relevant packages (#10046)
Browse files Browse the repository at this point in the history
* Generate dist/dynamic-modules.json for relevant packages

* Address review comments

* Skip writing dynamic-modules.json when the module map is empty
  • Loading branch information
vojtechszocs authored Apr 22, 2024
1 parent 325c0b3 commit 9fa6654
Show file tree
Hide file tree
Showing 2 changed files with 213 additions and 32 deletions.
86 changes: 54 additions & 32 deletions scripts/build-single-packages.js
Original file line number Diff line number Diff line change
@@ -1,65 +1,73 @@
/* eslint-disable no-console */
const fse = require('fs-extra');
const path = require('path');
const glob = require('glob');
const path = require('path')
const getDynamicModuleMap = require('./parse-dynamic-modules');

const root = process.cwd();
const packageJson = require(`${root}/package.json`);

if (!(process.argv.includes('--config') && process.argv.indexOf('--config') + 1 < process.argv.length)) {
if (!(process.argv.includes('--config') && process.argv.indexOf('--config') + 1 < process.argv.length)) {
console.log('--config is required followed by the config file name');
process.exit(1);
}

const configJson = require(`${root}/${process.argv[process.argv.indexOf('--config') + 1]}`);

const foldersExclude = configJson.exclude ? configJson.exclude : []
const foldersExclude = configJson.exclude ? configJson.exclude : [];

let moduleGlob = configJson.moduleGlob
if(moduleGlob && !Array.isArray(moduleGlob)) {
moduleGlob = [moduleGlob]
let moduleGlob = configJson.moduleGlob;

if (moduleGlob && !Array.isArray(moduleGlob)) {
moduleGlob = [moduleGlob];
} else if (!moduleGlob) {
moduleGlob = ['/dist/esm/*/*/**/index.js']
moduleGlob = ['/dist/esm/*/*/**/index.js'];
}

const components = {
// need the /*/*/ to avoid grabbing top level index files
/**
* We don't want the /index.js or /components/index.js to be have packages
* These files will not help with tree shaking in module federation environments
*/
files: moduleGlob.map(pattern => glob
.sync(`${root}${pattern}`)
.filter((item) => !foldersExclude.some((name) => item.includes(name)))
.map((name) => name.replace(/\/$/, '')))
.flat(),
}


files: moduleGlob
.map((pattern) =>
glob
.sync(`${root}${pattern}`)
.filter((item) => !foldersExclude.some((name) => item.includes(name)))
.map((name) => name.replace(/\/$/, ''))
)
.flat()
};

async function createPackage(component) {
const cmds = [];
let destFile = component.replace(/[^/]+\.js$/g, 'package.json').replace('/dist/esm/', '/dist/dynamic/');
if(component.match(/index\.js$/)) {

if (component.match(/index\.js$/)) {
destFile = component.replace(/[^/]+\.js$/g, 'package.json').replace('/dist/esm/', '/dist/dynamic/');
} else {
destFile = component.replace(/\.js$/g, '/package.json').replace('/dist/esm/', '/dist/dynamic/');
}

const pathAsArray = component.split('/');
const destDir = destFile.replace(/package\.json$/, '')
const esmRelative = path.relative(destDir, component)
const cjsRelative = path.relative(destDir, component.replace('/dist/esm/', '/dist/js/'))
const typesRelative = path.relative(destDir, component.replace(/\.js$/, '.d.ts'))
const destDir = destFile.replace(/package\.json$/, '');
const esmRelative = path.relative(destDir, component);
const cjsRelative = path.relative(destDir, component.replace('/dist/esm/', '/dist/js/'));
const typesRelative = path.relative(destDir, component.replace(/\.js$/, '.d.ts'));

const packageName = configJson.packageName;

if (!packageName) {
console.log("packageName is required!")
console.log('packageName is required!');
process.exit(1);
}

let componentName = pathAsArray[pathAsArray.length - (component.match(/index\.js$/) ? 2 : 1)];
if (pathAsArray.includes("next")) {

if (pathAsArray.includes('next')) {
componentName = `${componentName.toLowerCase()}-next-dynamic`;
} else if (pathAsArray.includes("deprecated")) {
} else if (pathAsArray.includes('deprecated')) {
componentName = `${componentName.toLowerCase()}-deprecated-dynamic`;
} else {
componentName = `${componentName.toLowerCase()}-dynamic`;
Expand All @@ -75,25 +83,39 @@ async function createPackage(component) {
};

// use ensureFile to not having to create all the directories
fse.ensureDirSync(destDir)
cmds.push(fse.writeJSON(destFile, content))
fse.ensureDirSync(destDir);
cmds.push(fse.writeJSON(destFile, content));

return Promise.all(cmds);
}

async function generatePackages(components, dist) {
const cmds = components.map((component) => createPackage(component, dist));
async function generatePackages(components) {
const cmds = components.map((component) => createPackage(component));
return Promise.all(cmds);
}

async function run(components) {
async function generateDynamicModuleMap() {
const moduleMap = getDynamicModuleMap(root);

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() {
try {
await generatePackages(components);
await generatePackages(components.files);
await generateDynamicModuleMap();
} catch (error) {
console.log(error)
console.log(error);
process.exit(1);
}
}

run(components.files);

run();
159 changes: 159 additions & 0 deletions scripts/parse-dynamic-modules.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/* eslint-disable no-console */
const fs = require('fs-extra');
const path = require('path');
const glob = require('glob');
const ts = require('typescript');

/** @type {ts.CompilerOptions} */
const tsConfigBase = require(path.resolve(__dirname, '../packages/tsconfig.base.json'));

/** @type {ts.CompilerOptions} */
const defaultCompilerOptions = {
target: tsConfigBase.target,
module: tsConfigBase.module,
moduleResolution: tsConfigBase.moduleResolution,
esModuleInterop: tsConfigBase.esModuleInterop,
allowJs: true,
strict: false,
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.
*
* @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;
}, {});
};

module.exports = getDynamicModuleMap;

0 comments on commit 9fa6654

Please sign in to comment.