Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate dist/dynamic-modules.json for relevant packages #10046

Merged
merged 3 commits into from
Apr 22, 2024
Merged
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
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;
Loading