Skip to content

Commit

Permalink
Merge pull request #67 from thivi/main
Browse files Browse the repository at this point in the history
chore(react): allow components to be imported through subpath
  • Loading branch information
thivi authored Mar 24, 2023
2 parents 44c1c7a + 29e36ea commit 1e1d9a6
Show file tree
Hide file tree
Showing 15 changed files with 945 additions and 181 deletions.
1 change: 1 addition & 0 deletions packages/react/.eslintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
dist
.rollup.cache
storybook-static
scripts
1 change: 1 addition & 0 deletions packages/react/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
enable-pre-post-scripts=true
25 changes: 18 additions & 7 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
"scripts": {
"build": "rollup -c",
"build-storybook": "build-storybook --no-manager-cache",
"prebuild": "rimraf dist",
"postbuild": "node scripts/build-components.js",
"lint": "pnpm lint:es && pnpm lint:styles",
"lint:es": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint:styles": "stylelint 'src/**/*.{css,scss,sass}' --config stylelint.config.cjs --allow-empty-input",
Expand All @@ -38,20 +40,20 @@
"typecheck": "tsc --noemit -p tsconfig.json"
},
"dependencies": {
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@mui/icons-material": "^5.10.16",
"@mui/lab": "5.0.0-alpha.110",
"@mui/material": "^5.10.16",
"@mui/system": "^5.10.16",
"@mui/utils": "^5.10.16",
"@oxygen-ui/primitives": "*",
"@oxygen-ui/react-icons": "*",
"clsx": "^1.2.1",
"react-world-flags": "^1.5.1"
},
"devDependencies": {
"@babel/core": "^7.20.2",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@mui/icons-material": "^5.10.16",
"@mui/lab": "5.0.0-alpha.110",
"@mui/material": "^5.10.16",
"@mui/system": "^5.10.16",
"@mui/utils": "^5.10.16",
"@oxygen-ui/logger": "*",
"@rollup/plugin-commonjs": "^23.0.3",
"@rollup/plugin-image": "^3.0.1",
Expand Down Expand Up @@ -85,8 +87,10 @@
"babel-loader": "^8.3.0",
"eslint": "8.25.0",
"eslint-plugin-mdx": "^2.0.5",
"fs-extra": "^11.1.0",
"jest": "29.0.3",
"jest-environment-jsdom": "^29.4.1",
"node-sass": "^8.0.0",
"postcss": "8.4.16",
"prettier": "^2.8.0",
"react": "^18.2.0",
Expand All @@ -109,6 +113,13 @@
"typescript": "^4.9.3"
},
"peerDependencies": {
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@mui/icons-material": "^5.10.16",
"@mui/lab": "5.0.0-alpha.110",
"@mui/material": "^5.10.16",
"@mui/system": "^5.10.16",
"@mui/utils": "^5.10.16",
"react": ">=18.0.0",
"react-dom": ">=18.0.0",
"typescript": ">=4.0.0"
Expand Down
55 changes: 48 additions & 7 deletions packages/react/rollup.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,50 @@ const typescript = require('@rollup/plugin-typescript');
const dts = require('rollup-plugin-dts');
const postcss = require('rollup-plugin-postcss');
const {terser} = require('rollup-plugin-terser');
const peerDepsExternal = require('rollup-plugin-peer-deps-external');
const image = require('@rollup/plugin-image');

const pkg = require('./package.json');

/**
* This method returns a method hat takes the id of a module as an argument and returns true if it should not be
* included in the final bundle and returns false if it should be.
*
* @param {boolean} includeMaterialDeps - True if Material dependencies should be
* included in the bundle; False otherwise.
* @returns A method that takes the id of the module as an argument
*/
const shouldExclude = (includeMaterialDeps = false) => {
const peerDeps = includeMaterialDeps
? Object.keys(pkg.peerDependencies || {}).filter(id => {
return (
!id.startsWith('@emotion/react') &&
!id.startsWith('@emotion/styled') &&
!id.startsWith('@mui/icons-material') &&
!id.startsWith('@mui/lab') &&
!id.startsWith('@mui/material') &&
!id.startsWith('@mui/system') &&
!id.startsWith('@mui/utils')
);
})
: Object.keys(pkg.peerDependencies || {});

const regExps = peerDeps.map(dep => new RegExp(`^${dep}(\\/\.+)*$`));
return id => regExps.some(regExp => regExp.test(id));
};

module.exports = [
{
cache: false,
input: 'src/index.ts',
external: shouldExclude(),
output: [
{
file: pkg.main,
format: 'cjs',
sourcemap: true,
},
{
file: pkg.module,
format: 'esm',
sourcemap: true,
},
],
plugins: [
peerDepsExternal(),
resolve(),
commonjs(),
typescript({tsconfig: './tsconfig.lib.json'}),
Expand All @@ -53,6 +74,26 @@ module.exports = [
image(),
],
},
{
cache: false,
input: 'src/index.ts',
external: shouldExclude(true),
output: [
{
file: pkg.main,
format: 'cjs',
sourcemap: true,
},
],
plugins: [
resolve(),
commonjs(),
typescript({ tsconfig: './tsconfig.lib.json' }),
postcss(),
terser(),
image(),
],
},
{
cache: false,
external: [/\.s?css$/],
Expand Down
80 changes: 80 additions & 0 deletions packages/react/scripts/build-components.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). All Rights Reserved.
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

const path = require('path');
const fs = require('fs-extra');
const {execSync} = require('child_process');
const packageJson = require('../package.json');
const fixFiles = require('./fix-files');
const {logger} = require('@oxygen-ui/logger');

const outputDir = path.resolve(__dirname, '..', 'dist', 'transpiled');

logger.log('💅 Transpiling components...');
execSync(
`npx tsc --project tsconfig.lib.json --declarationDir ${outputDir} --emitDeclarationOnly false --outDir ${outputDir}`,
);
logger.log('💅 Components transpiled! 🎉🎉🎉');

logger.log('💅 Copying components...');
fs.rmSync(path.resolve(__dirname, '..', 'dist','transpiled','src','components','index.js'));
fs.rmSync(path.resolve(__dirname, '..', 'dist', 'transpiled', 'src', 'components', 'index.d.ts'));
fs.copySync(path.resolve(__dirname, '..', 'dist', 'transpiled', 'src', 'components', ''), path.resolve(__dirname, '..', 'dist'));
fs.rmdirSync(path.resolve(__dirname, '..', 'dist', 'transpiled', 'src', 'components'), {recursive: true});
fs.rmSync(path.resolve(__dirname, '..', 'dist', 'transpiled', 'src', 'index.js'));
fs.rmSync(path.resolve(__dirname, '..', 'dist', 'transpiled', 'src', 'index.d.ts'));
fs.copySync(path.resolve(__dirname, '..', 'dist', 'transpiled', 'src'), path.resolve(__dirname, '..', 'dist', ''));
fs.rmdirSync(path.resolve(__dirname, '..', 'dist', 'transpiled'), {recursive: true});
logger.log('💅 Components copied! 🎉🎉🎉');

const packageJsonContent = {
exports: './index.js',
types: './index.d.ts',
};

const componentsDir = path.resolve(__dirname, '..', 'src', 'components');
const componentFiles = fs.readdirSync(componentsDir);
const srcDir = path.resolve(__dirname, '..', 'src');
const srcFiles = fs.readdirSync(srcDir);

logger.log('💅 Creating package.json files...');
componentFiles.forEach(file => {
const componentDir = path.resolve(componentsDir, file);
if (fs.lstatSync(componentDir).isDirectory()) {
fs.writeFileSync(path.resolve(__dirname, '..', 'dist', file, 'package.json'), JSON.stringify(packageJsonContent));
}
});

srcFiles.forEach(file => {
const dir = path.resolve(srcDir, file);
if (fs.lstatSync(dir).isDirectory() && file !== 'components') {
fs.writeFileSync(path.resolve(__dirname, '..', 'dist', file, 'package.json'), JSON.stringify(packageJsonContent));
}
});
logger.log('💅 package.json files created! 🎉🎉🎉');

logger.log('💅 Creating root package.json file...');
packageJson.main = './cjs/index.js';
packageJson.module = './esm/index.js';
packageJson.types = './index.d.ts';
fs.writeFileSync(path.resolve(__dirname, '..', 'dist', 'package.json'), JSON.stringify(packageJson));
logger.log('💅 Root package.json file created! 🎉🎉🎉');

logger.log('💅 Fixing the imports in the components and injecting CSS...');
fixFiles();
logger.log('💅 Imports fixed and CSS injected into components! 🎉🎉🎉');
146 changes: 146 additions & 0 deletions packages/react/scripts/fix-files.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). All Rights Reserved.
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

const fs = require('fs');
const path = require('path');
const saas = require('node-sass');
const {logger} = require('@oxygen-ui/logger');

module.exports = fixFiles = () => {
//All files and directories in the src/components folder.
const componentFiles = fs.readdirSync(path.resolve(__dirname, '..', 'src', 'components'));

// All directories in the src/components folder.
const componentDirs = componentFiles.filter(file => {
const componentDir = path.resolve(__dirname, '..', 'src', 'components', file);

return fs.lstatSync(componentDir).isDirectory();
});

// The directories containing the files bundled by Rollup.
const moduleDirs = ['cjs', 'esm'];

// The non-component directories in the dist folder.
const nonComponentDirsInDist = fs.readdirSync(path.resolve(__dirname, '..', 'dist')).filter(file => {
return (
!componentDirs.includes(file) &&
fs.lstatSync(path.resolve(__dirname, '..', 'dist', file)).isDirectory() &&
!moduleDirs.includes(file)
);
});

/**
* This method fixes the references to the non-component directories in the dist folder such as utils, constants, etc.
* in imports.
*
* @param {string} file - The file to be fixed.
* @returns {string} - The fixed file.
*/
const fixImportStatements = file => {
const regExpString = `(import.*from.*[\"|'])(.*(${nonComponentDirsInDist.join('|')}).*)([\"|'].*)`;
const regExp = new RegExp(regExpString, 'g');

return file.replace(regExp, (_fullMatch, groupOne, groupTwo, _groupThree, groupFour) => {
return `${groupOne}${groupTwo.replace('../', '')}${groupFour}`;
});
};

/**
* This method updates imports and injects css into component files.
*
* @param {string} filePath - The path to the file to be fixed.
*/
const fixFiles = filePath => {
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
logger.error(err);

return;
}

const fixedImports = fixImportStatements(data);
const fixedCSS = loadCSS(fixedImports, filePath);

fs.writeFile(filePath, fixedCSS, 'utf8', err => {
if (err) {
logger.error(err);
}
});
});
};

// The JS code that injects CSS into the head tag.
const codeToInjectCSSIntoHeadTag =
"function addCSS(css){var head = document.getElementsByTagName('head')[ 0 ];" +
"var s = document.createElement('style');s.setAttribute('type', 'text/css');" +
's.appendChild(document.createTextNode(css));head.appendChild(s);}';

/**
* This method injects teh `addCSS` function into the file passed as the argument along with the, compiles the
* SASS code of corresponding to the component into CSS, and calls the `addCSS` method with the CSS code as the
* argument.
*
* @param {string} file - The file to be injected with the `addCSS` function.
* @param {string} filePath - The path to the file.
* @returns {string} file - The file with the `addCSS` function injected.
*/
const loadCSS = (file, filePath) => {
const componentDir = filePath.split(path.sep).slice(-2)[0];
let css = '';

file.match(/['|"].*\.scss["|']/g)?.forEach(match => {
const sassFilePath = path.resolve(__dirname, '..', 'src', 'components', componentDir, match.replace(/['|"]/g, ''));
css += saas.renderSync({file: sassFilePath}).css.toString();
});

file = file.replace(/import.*['|"].*\.scss["|'].*/g, '');

if (css === '' || path.extname(filePath) === '.ts') {
return file;
}

file += `${codeToInjectCSSIntoHeadTag}\naddCSS(\'${JSON.stringify(css)}\');`;

return file;
};

/**
* Traverses through the directories and fixes imports and injects CSS.
*
* @param {string} directory - The directory to be parsed.
*/
parseComponents = (directory) => {
fs.readdirSync(directory).forEach(file => {
const absolutePath = path.resolve(directory, file);
if (![...nonComponentDirsInDist, ...moduleDirs].includes(file)) {
if (fs.statSync(absolutePath).isDirectory()) {
parseComponents(absolutePath);
} else {
if (
(file.endsWith('.js') || file.endsWith('.ts')) &&
componentDirs.includes(directory.split(path.sep).slice(-1)[0])
) {
fixFiles(absolutePath);
}
}
}
});
};

parseComponents(path.resolve(__dirname, '..', 'dist'));
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ exports[`Badge should match the snapshot 1`] = `
<body>
<div>
<span
class="MuiBadge-root oxygen-badge MuiBadge-root css-1c32n2y-MuiBadge-root"
class="MuiBadge-root oxygen-badge css-1c32n2y-MuiBadge-root"
>
<span
class="MuiBadge-badge MuiBadge-standard MuiBadge-invisible MuiBadge-anchorOriginTopRight MuiBadge-anchorOriginTopRightRectangular MuiBadge-overlapRectangular MuiBadge-badge css-uh1ep6-MuiBadge-badge"
class="MuiBadge-badge MuiBadge-standard MuiBadge-invisible MuiBadge-anchorOriginTopRight MuiBadge-anchorOriginTopRightRectangular MuiBadge-overlapRectangular css-1cgiqj2-MuiBadge-badge"
/>
</span>
</div>
Expand Down
Loading

0 comments on commit 1e1d9a6

Please sign in to comment.