Use Postcss in Custom Angular Library

@gquinteros93 I can feel your pain but this is exactly what ViewEncapsulation is designed for - to encapsulate components from each other and global styles.
Essentially adding a hash to class names is exactly what Angular Compiler does under the hood. I wonder why it wasn’t enough for you. Could you please elaborate more on that?

1 Like

Hi @jeb , thanks for your response.

Yes, I use ViewEncapsulation on my components, however it’s not enough to prevent the css overrides.

The issue is that some third party websites use class names as selector to set css rules.
I want to prevent that through hashing my classes names. Because of that, I need to use postcss-modules and posthtml-css-modules.

As the images of the form I showed you before, the ViewEncapsulation wasn’t enough to prevent the third party website add a gray frame to my inputs in the contact form.

@gquinteros93 Got you. Have you tried ViewEncapsulation.ShadowDom? It might give you what you want.

1 Like

Keep in might though it’s not available in all the browsers (had to post it as a separate comments since I’m a new user and not allowed more than 2 links in the post :man_facepalming:)

hi @jeb

Yes, ViewEncapsulation.ShadowDom was an option that we handle.

But, like you said it’s not available for all the Browser, because of that I used the CSS modules approach.

My mistake/error was thinking that I would be able to extend the Angular Library build like I did in the Angular Project. I didn’t know that ng-packagr is not able to extend.

There is a discussion to extend/modify template and styles compilation in ng-packagr :

But, the PR is still open and they didn’t merge it yet.

Good afternoon, @m.koretskyi, @LayZee and @jeb.

Did you have the opportunity to see if there is a possible solution to modify the scss and html files during the compilation of an angular library?

In my case, I researched and I find a possible solution that are developing in ng-packagr:


It seems to be ready, however, they have a pull request open from Mar 5, 2018 and they didn’t merge it yet.

Thanks in advance for your time and effort.

Regards,
German.

Could it help you to use ng-packagr API in a script? See comment by Alan Agius:

Hi @LayZee ,

Yes, that’s is a good option.
In fact, I am already using scss-bundle in the postbuild to export the scss files of my library.

However, what I could not achieve yet is how to replace the class name in the html by the hash name during or after the build.

Because, after the build the html code are embedded in the js files of the library.

Perhaps, I can use the same approach I used to replace the class name with Ngclass through Lodash.

But, I don’t know how to do that in the html code that are embedded in the js files of the library.

Do you have any idea how to do that?

The other option (that I also do not how to do it) is modifies the HTML files during the compilation before they get embedded in the js files. Because, later I can modify the scss files with scss-bundle like you said before.

Unfortunately, I don’t know how to help you with what you’re asking. I only now realized that ng-packagr doesn’t use Webpack.

I also noticed that the document you’re referring to now says that developers won’t be able to extend the Rollup configuration:

Hi @LayZee, thanks for your response.

Yes, extending rollup was my first idea. But, now I see it’s not possible. Also, from what I researched they only use rollup to do the umd bundle, so now I think extending rollup was not the best solution.

If I just were able to modify the HTML files during build time (or after) I would use Lodash and replace the class names by the hashed class names like I did in the Angular Project for ngClass but for all the class names:

Perhaps @m.koretskyi or @jeb can have an idea to do that :pray:

Finally, I want to thanks you all you guys (@LayZee, @m.koretskyi and @jeb) for giving me support and helping me with this challenge, I know you are spending your free time trying to help me resolve this crazy idea.

I’ve asked Serkan Sepahi to take a look. He’s a tooling and compiler nerd and I mean that in the best way possible :smile:

1 Like

Great :raised_hands: , I hope he can help us.
I will cross my fingers :crossed_fingers: .
Thanks for you help :muscle:

(post withdrawn by author, will be automatically deleted in 24 hours unless flagged)

(post withdrawn by author, will be automatically deleted in 24 hours unless flagged)

Ok, I feel like I have to go a bit deeper.

Here is the thing: ng-packagr has a completely custom build chain. The chain is based on transforms and pipelines (read more here).
This architecture is extensible to some degree however it’s nearly impossible to do what you want. If the STYLESHEET_TRANSFORM was implemented it would have been a easier but unfortunately it’s not implemented, despite being mentioned in one of the examples.

Let’s start with your first idea - rollup. rollup is used by ng-packagr solely for bundling purposes, it doesn’t take any part in compilation and file processing. rollup is applied inside WRITE_BUNDLES transform, which is implemented, and it even has an option for custom plugin. Since WRITE_BUNDLES is an existing transform, theoretically it could be replaced with a custom provider. However, rollup is applied deep inside this transform so you would have to basically copy-paste the entire transform tree down to the rollup function in order to be able to add your custom plugin. Which, obviously, can break with every minor release. But even when you did that it wouldn’t solve your problem entirely. rollup is processing only fesm and umd bundles and in library build you also have esm modules which are not processed by rollup.

Let’s face it - you cannot modify rollup process in order to change the final bundle.

Your another option is to try replacing the compileNgcTransform with a custom one which mimics its behavior but passes to compileSourceFiles function a custom stylesheetProcessor, which, in turn, is the same processor as the default but applies additional postcss plugin.
This can be easily broken by a minor release too (since you copy-pasted the whole build pipeline) and it’s also hardly maintainable.
Moreover, if you managed to replace the whole pipeline and applied your post css plugin you still have to modify the html. Which you can’t do inside the build process, because HTML and TS processing is done by ng compiler host and there is no way to hook into it. You’ll have to override the compiler host, and believe me, you don’t want it.
So you’ll still have to go to the final bundle/modules and somehow replace the class names with hashes that are written during the build process to a json file.

As you can see there is almost no way to do what you want with ng-packagr.
In my opinion your best shot is to try modifying the final bundles/modules with a custom post-build script.

2 Likes

Hi @jeb,

Thanks a lot for your explanation. It was really complete and you explained all possible cases and why they are not achievable.

I will make the styles of the library global and hash all the syles with scss-bundles and postcss-modules. Then, I will implement the post-build script to modify HTML code in the bundles on the dist folder.

I will copy my solution here in order to share it with you and the community.

That seems to be the only solution available until the ng-packagr add the features of:

  • STYLESHEET_TRANSFORM
  • TEMPLATE_TRANSFOR
1 Like

Hi guys, how are you?

I have just wanted to let you know I was was able to use css modules in a custom library modifying the final bundles/modules like @jeb suggested :clap: .

Later I will do another reply explaining the approach I used and copying the scripts I used.

Thanks for your help and support.

1 Like

That would be really interesting to see. Glad it worked out for you!

Great news, looking forward to seeing your solution! I’m glad I could be of help.

Some comments before explaining my solution:

  1. I applied this solution to a Custom Library that has global styles and components styles.
  2. I created a config file where I defined all the paths to the different files I use through the different scrips. This file is /scripts/config/index.js
  3. I created and stored all the files related to this solution in the folder /scripts in the root of the project
  4. I created the ts file /scripts/common-functions.ts where I added functions that I reuse in different js/ts files in the script folder

/scripts/config/index.js

module.exports = {
 JSON_GLOBAL_STYLES_PATH: './projects/my-lib/src/lib/styles/styles-hashed/_components-hashed.scss.json',
 JSON_ENCAPSULATED_STYLES_PATH: './projects/my-lib/src/lib/styles/styles-hashed/_encapsulated-hashed-styles.scss.json',
 COMPONENTS_GLOBAL_STYLES_PATH: './projects/my-lib/src/lib/styles/_components.scss',
 COMPONENTS_HASHED_GLOBAL_STYLES_PATH: './projects/my-lib/src/lib/styles/styles-hashed/_components-hashed.scss',
 LIBRARY_SOURCE_PATH: './projects/my-lib',
 LIBRARY_JSON_METADATA_PATH: './dist/my-lib/my-lib.metadata.json',
 LIBRARY_DIST_PATH: './dist/my-lib'
};

/scripts/common-functions.ts

import * as path from 'path';
import * as fs from 'fs';
 
export function filesFromPath(startPath, filter): string[] {
   try {
       if (!fs.existsSync(startPath)) {
           throw new Error(`no dir ${startPath}`);
       }
 
       const files = fs.readdirSync(startPath);
       let filename; let stat;
       let result: string[] = [];
       for (let file of files) {
           filename = path.join(startPath, file);
           stat = fs.lstatSync(filename);
           if (stat.isDirectory()) {
               result = result.concat(filesFromPath(filename, filter));
           } else if (filename.indexOf(filter) >= 0) {
               result.push(filename);
           }
       }
       return result;
   } catch (e) {
       throw new Error(`Fail getting files with extension: ${filter} in path: ${startPath}. Error: ${e}`);
   }
}
 
export function importJsonFile(filePath: string): any {
   try {
       if (!fs.existsSync(filePath)) {
           throw new Error(`Json file with path: ${filePath}, doesn't exists.`);
       }
       return JSON.parse(fs.readFileSync(filePath).toString());
   } catch (e) {
       throw new Error(`Fail getting json file with path: ${filePath}. Error: ${e}`);
   }
}

Well, to achieve the goal of Use Css Modules with Postcss in an Angular Library I have to do the following actions in the prebuild, build and postbuild of the library

prebuild:

  1. Read all the global styles and generate the json files that contains the relation between the class name and class names hashed through postcss-modules. Also, I save the global style with their class names hashed with a different name of the original global style, in order to do not override the original file.

  2. Read all the encapsulated styles and generate the json files that contain the relation between the class name and the hash class name through postcss-modules. Here, I do not save the styles of the components, because they will be in the js file of each component after the build.

To complete these actions, I created the /scripts/prebuild/hash-global-styles.ts (for the global styles) and /scripts/prebuild/generate-hashed-class-names.ts (for the components styles)

/scripts/prebuild/hash-global-styles.ts

import * as scriptsConfig from '../config/index';
import { Bundler } from 'scss-bundle';
import { relative } from 'path';
import { writeFile } from 'fs-extra';
import * as path from 'path';
import * as fs from 'fs';
import * as postcss from 'postcss';
import * as autoprefixer from 'autoprefixer';
import * as postcssModules from 'postcss-modules';
const postcssScss = require('postcss-scss');
 
/** Bundles all SCSS files into a single file */
export async function hashGlobalStyle(input: string, output: string) {
 const {found, bundledContent, imports} = await new Bundler()
   .Bundle(input, ['./src/styles/**/*.scss']);
 if (imports) {
   const cwd = process.cwd();
 
   const filesNotFound = imports
     .filter((x) => !x.found)
     .map((x) => relative(cwd, x.filePath));
 
   if (filesNotFound.length) {
     console.error(`SCSS imports failed \n\n${filesNotFound.join('\n - ')}\n`);
     throw new Error('One or more SCSS imports failed');
   }
 }
 
 if (found) {
   const jsonFile: string = output.replace('.scss', '.scss.json');
   const scssHashed = await postcss([
       postcssModules({
         generateScopedName: '[hash:base64:8]',
         getJSON(cssFileName: any, json: any, outputFileName: any) {
           const jsonFileName = path.resolve(jsonFile);
           fs.writeFileSync(jsonFileName, JSON.stringify(json));
         }
       }),
       autoprefixer
     ]
   ).process(bundledContent, {parser: postcssScss});
 
   await writeFile(output, scssHashed);
 }
 
 return;
}
 
hashGlobalStyle(scriptsConfig.COMPONENTS_GLOBAL_STYLES_PATH, scriptsConfig.COMPONENTS_HASHED_GLOBAL_STYLES_PATH);

/scripts/prebuild/generate-hashed-class-names.ts

import * as scriptsConfig from '../config/index';
import { filesFromPath } from '../common-functions';
import * as fs from 'fs';
import * as postcss from 'postcss';
import * as autoprefixer from 'autoprefixer';
import * as postcssModules from 'postcss-modules';
import { writeFile } from 'fs-extra';
const postcssScss = require('postcss-scss');
 
 
export async function generateHashedClassNames(intputPath: string, outputPath: string) {
 if (!outputPath) {
   return;
 }
 let jsonHashedClassNames = {};
 const scssFiles = filesFromPath(intputPath, '.component.scss');
 for (let scss of scssFiles) {
   jsonHashedClassNames = await extractClassNames(scss, jsonHashedClassNames);
 }
 await writeFile(outputPath, JSON.stringify(jsonHashedClassNames));
}
 
async function extractClassNames(filePath: string, jsonHashedClassNames: any) {
 try {
   if (!filePath || !fs.existsSync(filePath) || !jsonHashedClassNames) {
     return jsonHashedClassNames;
   }
   let scssFileContent: string = fs.readFileSync(filePath).toString();
   const scssHashed = await postcss([
     postcssModules({
       generateScopedName: '[hash:base64:8]',
       getJSON(cssFileName: any, json: any, outputFileName: any) {
         jsonHashedClassNames = Object.assign({}, jsonHashedClassNames, json);
       }
     }),
     autoprefixer
   ]).process(scssFileContent, {parser: postcssScss});
   return jsonHashedClassNames;
 } catch (e) {
   throw new Error(`Fail modifying file path: ${filePath}. Error: ${e}`);
 }
}
 
generateHashedClassNames(scriptsConfig.LIBRARY_SOURCE_PATH, scriptsConfig.JSON_ENCAPSULATED_STYLES_PATH);

To execute those scripts before the build I use the pre hook of npm:

package.json

...
…
 "scripts": {
   "ng": "ng",
   "clean": "yarn rimraf ./dist/**",
   "build": "yarn clean && ng build my-lib",
   "compile:hash-global-styles": "ts-node  --compilerOptions '{\"module\":\"commonjs\"}' ./scripts/prebuild/hash-global-styles.ts",
   "compile:hash-component-styles": "ts-node  --compilerOptions '{\"module\":\"commonjs\"}' ./scripts/prebuild/generate-hashed-class-names.ts",
   "prebuild": "yarn compile:hash-component-styles && yarn compile:hash-global-styles",
 },
...

build:

Here I just build the library. However, it’s good to mention that my global styles imports hashed global styles created in the /scripts/prebuild/hash-global-styles.ts

styles.scss

/**
* COMPONENTS Hashed📚
* All *.component.scss files hashed in file ./styles-hashed/_components-hashed.scss .
**/
@import './styles-hashed/components-hashed';
@import './should-not-be-hashed';

Like you see I had another scss file where I define some rules that should not be hash.

postbuild:

Before explaining this I want to mention that all the class names I want to hash and replace in the component.html files I identify them like this:

   <div>
     <button class="<%= getHashedClass(`submit-btn`) %>" (click)="submit()">
       Submit
     </button>
   </div>

I used the same approach I use for the ngClass in an Agular Application instead of using the approach of css-module like posthtml-css-modules does.

Why?

Why?

Because I thought there are many border cases if use the css-modules approach:

  • More than one class to hash
  • What happens if the class attribute is also added? Should I override it or added to the other ones?

In order to have the fastest solution to implement, I decided to add the <%= getHashedClass(`class-name`) %> function in all the classes I want to hash. It causes me more time to add the getHashedClass but I think it was faster to implement.

However, I think it would be better to use the approach that posthtml-css-modules does.

After that comment, we can continue :smiley:.

As you know, after the library was built all the html and css files are embedded in the js bundle files. Because of that, I need to go through each js file and replace the class names in the component styles and templates for the hashed classes names ones.

First, I execute a script that searches for all the components templates, then inside each template, the script search for all the <%= getHashedClass(`class-name`) %> and replace the function (getHashedClass) by the correspondent hashed class name for the value class-name given as the parameter. I search for the corresponding hashed class name in the json files JSON_GLOBAL_STYLES_PATH and JSON_ENCAPSULATED_STYLES_PATH.

Then, I do the same approach for styles, I list all the class names in the styles, then I search for each class name if it has a hashed value in the json files JSON_GLOBAL_STYLES_PATH and JSON_ENCAPSULATED_STYLES_PATH, and if it contains a value for that class name I replace it.

When I installed the library for the first time (after I applied css modules) in my angular project the class names in the html files were hashed, however, the class names in the styles weren’t hashed. After some research and trying stuff I discovered that I had to modify the my-lib.metadata.json too. After I execute the same approach for this file the library worked as expected.

I saw that the function that modifies the templates also modified this file, however, the function that modifies the styles didn’t apply the replace function here, because of that, I just needed to replace class names in the styles for the my-lib.metadata.json

This is the script that executes all those steps in the post-build

/scripts/postbuild/css-module-replacer.ts

import * as scriptsConfig from '../config/index';
import { filesFromPath, importJsonFile } from '../common-functions';
import * as fs from 'fs';
 
const hashFunction = 'getHashedClass';
const regexStyleInJs = /styles:[ ]{0,}\["([^\]]*)"\]/gi;
const regexReplaceStyleInJs = /styles:[ ]{0,}\["/gi;
const regexStyleInMetadata = /"styles":[ ]{0,}\["([^\]]*)"\]/gi;
const regexReplaceStyleInMetadata = /"styles":[ ]{0,}\["/gi;
 
function modifyTemplate(jsFile: string, jsonHashedNames: any) {
 try {
   // If it doesn't have getHashedClass we do not need to do anything
   if (!jsFile || !jsFile.includes(hashFunction) || !jsonHashedNames) {
     return jsFile;
   }
   // Get all the places where use getHashedClass
   const arrayClassNames: string[] = jsFile.match(/<%=[ ]{0,}getHashedClass.*?\(([^)]*)\)[ ]{0,}%>/gi);
   let hashedClassName: string; let className: string;
   if (arrayClassNames) {
     for (let classToReplace of arrayClassNames) {
       className = classToReplace.replace(/<%=[ ]{0,}getHashedClass.*?\(/gi, '').replace(/\)[ ]{0,}%>/gi, '');
       if (className) {
         className = className.replace(/`/gi, '').replace(/'/gi, '').replace(/"/gi, '');
         hashedClassName = jsonHashedNames[className];
         if (hashedClassName) {
           jsFile = jsFile.replace(classToReplace, hashedClassName);
         }
       }
     }
   }
   return jsFile;
 } catch (e) {
   throw new Error(`ModifyTemplate: Fail modifying file: ${jsFile}. Error: ${e}`);
 }
}
 
function modifyStyles(jsFile: string, jsonHashedNames: any, regexMatch: RegExp, regexReplace: RegExp) {
 try {
   // If it doesn't have getHashedClass we do not need to do anything
   if (!jsFile || !jsonHashedNames) {
     return jsFile;
   }
   // Get all the places where use getHashedClass
   const styles: string[] = jsFile.match(regexMatch);
   let scssContent: string; let classRules: string[]; let className: string;
   let hashedClassName: string; let styleHashed: string; let newClassName: string;
   if (!styles) {
     return jsFile;
   }
   for (let style of styles) {
     styleHashed = JSON.parse(JSON.stringify(style));
     scssContent = styleHashed.replace(regexReplace, '').replace(/"\]/gi, '');
     if (scssContent) {
       classRules = scssContent.match(/\.-?[_a-zA-Z]+[_a-zA-Z0-9-]*\s*\{/gi);
       if (classRules) {
         for (let classRule of classRules) {
           className = classRule.replace(/\./, '').replace(/[ ]{0,}\{/, '');
           if (className) {
             hashedClassName = jsonHashedNames[className];
             if (hashedClassName) {
               newClassName = `.${hashedClassName}{`;
               styleHashed = styleHashed.replace(classRule, newClassName);
             }
           }
         }
       }
     }
     // If the style changed, I will replace it.
     if (JSON.stringify(style) !== JSON.stringify(styleHashed)) {
       jsFile = jsFile.replace(style, styleHashed);
     }
   }
   return jsFile;
 } catch (e) {
   throw new Error(`ModifyStyles: Fail modifying file: ${jsFile}. Error: ${e}`);
 }
}
 
function updateLibraryMetadata(jsonHashedNames: any) {
 try {
   if (!fs.existsSync(scriptsConfig.LIBRARY_JSON_METADATA_PATH) || !jsonHashedNames) {
     return;
   }
   let jsonMetadata = fs.readFileSync(scriptsConfig.LIBRARY_JSON_METADATA_PATH).toString();
   jsonMetadata = modifyStyles(jsonMetadata, jsonHashedNames, regexStyleInMetadata, regexReplaceStyleInMetadata);
   fs.writeFileSync(scriptsConfig.LIBRARY_JSON_METADATA_PATH, jsonMetadata);
 } catch (e) {
   throw new Error(`ModifyStyles: Fail modifying file: ${scriptsConfig.LIBRARY_JSON_METADATA_PATH}. Error: ${e}`);
 }
}
 
export function replaceHashedClassNames() {
 try {
   const globalHashedClassNames = importJsonFile(scriptsConfig.JSON_GLOBAL_STYLES_PATH);
   const encapsulatedHashedClassNames = importJsonFile(scriptsConfig.JSON_ENCAPSULATED_STYLES_PATH);
   const jsonHashedClassNames = Object.assign({}, globalHashedClassNames, encapsulatedHashedClassNames);
   const jsFilesPath = filesFromPath(scriptsConfig.LIBRARY_DIST_PATH, '.js');
   let jsFile: string;
   let jsFileModified: string;
   for (let jsFilePath of jsFilesPath) {
     if (jsFilePath && fs.existsSync(jsFilePath)) {
       jsFile = fs.readFileSync(jsFilePath).toString();
       jsFileModified = modifyTemplate(jsFile, jsonHashedClassNames);
       jsFileModified = modifyStyles(jsFileModified, jsonHashedClassNames, regexStyleInJs, regexReplaceStyleInJs);
       if (jsFile !== jsFileModified) {
         fs.writeFileSync(jsFilePath, jsFileModified);
       }
     }
   }
   // Then we have to update the styles in the my-lib.metadata.json
   updateLibraryMetadata(jsonHashedClassNames);
 } catch (e) {
   throw new Error(`Failing executing replaceHashedClassNames(). Error: ${e}`);
 }
}
 
replaceHashedClassNames();

To execute this script before the build I use the post hook of npm:

package.json

…
 "scripts": {
   "ng": "ng",
   "clean": "yarn rimraf ./dist/**",
   "build": "yarn clean && ng build my-lib",
   "compile:hash-global-styles": "ts-node  --compilerOptions '{\"module\":\"commonjs\"}' ./scripts/prebuild/hash-global-styles.ts",
   "compile:hash-component-styles": "ts-node  --compilerOptions '{\"module\":\"commonjs\"}' ./scripts/prebuild/generate-hashed-class-names.ts",
   "compile:css-module-replace": "ts-node  --compilerOptions '{\"module\":\"commonjs\"}' ./scripts/postbuild/css-module-replacer.ts",
   "prebuild": "yarn compile:hash-component-styles && yarn compile:hash-global-styles",
   "postbuild": "yarn compile:css-module-replace"
 },
...

That’s it

If you have any doubt or something is not clear, please tell me and I will improve my explanation.

Also, using the @jeb library @angular-builders/custom-webpack I added custom loaders to modify the webpack compilation in order to execute the library with or without css modules when I run the library in my local environment. I do not add those custom loaders to this reply because It’s already too long, but if you would like to see them I can add them too in another reply.

1 Like