Skip to content

Commit

Permalink
Merge pull request #10 from vordgi/ncm-9
Browse files Browse the repository at this point in the history
ncm-9 Disable minimizer for dev mode
  • Loading branch information
vordgi authored Sep 9, 2023
2 parents 1468c21 + 3bc6b74 commit 484ecd3
Show file tree
Hide file tree
Showing 10 changed files with 589 additions and 363 deletions.
24 changes: 20 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Next classnames minifier

Library for configuring style _(css/scss/sass)_ modules to generate compressed classes
Library for configuring style _(css/scss/sass)_ modules to generate compressed classes (`.header` -> `.a`, `.nav` -> `.b`, ..., `.footer` -> `.aad`, etc.)

## Reasons
*Compressing classes* can reduce the size of the generated html and css by up to *20%*, which will have a positive effect on page rendering and metrics (primarily [FCP](https://web.dev/first-contentful-paint/))
Expand All @@ -25,7 +25,7 @@ Create `next.config.js` file in your project and apply the library.
```js
const withClassnamesMinifier = require('next-classnames-minifier').default;

module.exports = withClassnamesMinifier({
module.exports = withClassnamesMinifier()({
// next.js config
});
```
Expand All @@ -36,10 +36,26 @@ const withClassnamesMinifier = require('next-classnames-minifier').default;
const withPlugins = require('next-compose-plugins');

module.exports = withPlugins([
[withClassnamesMinifier]
[withClassnamesMinifier()]
], nextConfig);
```

## Configuration
next-classname-minifier has 3 types of changing classnames:

* minified - the main option. It is highly not recommended to use this option for development mode (_it is too unstable in dev mode_);
* detailed - can be used for debugging, default for development mode;
* none — use the default CSS modules option;

You can choose different options for development and production.

Configuration example:
```js
module.exports = withPlugins([
[withClassnamesMinifier({ dev: 'none', prod: 'minified' })]
], nextConfig);
```

## License

[MIT](https://github.com/vordgi/next-classnames-minifier/blob/main/LICENSE)
[MIT](https://github.com/vordgi/next-classnames-minifier/blob/main/LICENSE)
823 changes: 521 additions & 302 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "next-classnames-minifier",
"version": "0.1.2",
"version": "1.0.0",
"description": "Library for configuring style modules to generate compressed classes",
"main": "dist/withClassnamesMinifier.js",
"types": "dist/withClassnamesMinifier.d.ts",
Expand Down
4 changes: 4 additions & 0 deletions src/lib/constants/minifiers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const MINIFIED = 'minified';
export const DETAILED = 'detailed';
export const NONE = 'none';
export const VALID_MINIFIERS_KEYS = [MINIFIED, DETAILED, NONE] as const;
2 changes: 1 addition & 1 deletion src/lib/converters/ConverterBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ abstract class BaseConverter {
abstract getLocalIdent({ resourcePath }: LoaderContext<any>, _localIdent: string, origName: string): string;
};

export default BaseConverter;
export default BaseConverter;
2 changes: 1 addition & 1 deletion src/lib/converters/ConverterDetailed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ class ConverterDetailed implements ConverterBase {
}
};

export default ConverterDetailed;
export default ConverterDetailed;
39 changes: 1 addition & 38 deletions src/lib/converters/ConverterMinified.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
import type { LoaderContext } from 'webpack/types';
import type ConverterBase from './ConverterBase';
import fs from 'fs';
import path from 'path';

const cacheFolderPath = path.join(process.cwd(), '.next/cache/next-classnames-minifier');
const cacheFilePath = path.join(cacheFolderPath, 'minified.xml');

class ConverterMinified implements ConverterBase {
cache: {[resource: string]: {[className: string]: string}} = {};

cacheFile;

symbols: string[] = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
Expand All @@ -25,35 +18,6 @@ class ConverterMinified implements ConverterBase {

nameMap = [0];

constructor() {
if (!fs.existsSync(cacheFolderPath)) fs.mkdirSync(cacheFolderPath, {recursive: true});

if (fs.existsSync(cacheFilePath)) {
this.cacheFile = fs.createWriteStream(cacheFilePath, {flags: 'a'})
const cacheRow = fs.readFileSync(cacheFilePath, {encoding: 'utf-8'});
cacheRow.split('\n').forEach(row => {
if (!row) return;
const matched = row.match(/<resource>(.*)<\/resource><name>(.*)<\/name><class>(.*)<\/class>/);
if (!matched) return;
const [_match, resource, name, className] = matched;

if (!this.cache[resource]) this.cache[resource] = {};
this.cache[resource][name] = className;

const nameMap = className.split('').map((s) => this.symbols.indexOf(s));
const lastIndex = nameMap.reduce((acc, cur) => acc + cur, 0);
if (lastIndex > this.lastIndex) {
this.lastIndex = lastIndex;
this.nameMap = nameMap;
this.currentLoopLength = nameMap.length - 1;
this.nextLoopEndsWith = this.lastIndex < 26 ? 26 : Math.pow(62, this.nameMap.length);
}
});
} else {
this.cacheFile = fs.createWriteStream(cacheFilePath);
}
}

getClassName() {
const symbolsCount = 62;
if (this.lastIndex >= this.nextLoopEndsWith) {
Expand Down Expand Up @@ -85,10 +49,9 @@ class ConverterMinified implements ConverterBase {

const minifiedClassName = this.getClassName();
currentCache[origName] = minifiedClassName;
this.cacheFile.write(`<resource>${resourcePath}</resource><name>${origName}</name><class>${minifiedClassName}</class>\n`);
this.lastIndex += 1;
return minifiedClassName;
}
};

export default ConverterMinified;
export default ConverterMinified;
12 changes: 6 additions & 6 deletions src/lib/injectConverter.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { RuleSetRule, RuleSetUseItem } from "webpack";
import type { RuleSetUseItem, ModuleOptions } from "webpack";
import type ConverterBase from "./converters/ConverterBase";
import modifyCssLoader from "./modifyCssLoader";

const injectConverter = (converter: ConverterBase, rules?: (RuleSetRule | "...")[]) => {
const injectConverter = (converter: ConverterBase, rules?: ModuleOptions['rules']) => {
if (!rules) return;
const getLocalIdent = converter.getLocalIdent.bind(converter)

const oneOfRule = rules?.find(
(rule) => typeof rule === 'object' && typeof rule.oneOf === 'object'
(rule) => typeof rule === 'object' && typeof rule?.oneOf === 'object'
);

if (oneOfRule && typeof oneOfRule === 'object') {
Expand All @@ -18,9 +18,9 @@ const injectConverter = (converter: ConverterBase, rules?: (RuleSetRule | "...")
loaderObj.options.modules
);
oneOfRule.oneOf?.forEach(rule => {
if (Array.isArray(rule.use)) {
if (rule && Array.isArray(rule.use)) {
rule.use.forEach((loaderObj) => {
if (testCssLoaderWithModules(loaderObj)) {
if (loaderObj && testCssLoaderWithModules(loaderObj)) {
modifyCssLoader(getLocalIdent, loaderObj);
}
});
Expand All @@ -29,4 +29,4 @@ const injectConverter = (converter: ConverterBase, rules?: (RuleSetRule | "...")
}
}

export default injectConverter
export default injectConverter;
2 changes: 1 addition & 1 deletion src/lib/modifyCssLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ const modifyCssLoader = (getLocalIdent: ConverterBase['getLocalIdent'], cssLoade
cssLoaderObj.options.modules.getLocalIdent = getLocalIdent;
}

export default modifyCssLoader;
export default modifyCssLoader;
42 changes: 33 additions & 9 deletions src/withClassnamesMinifier.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,50 @@
import type { Configuration } from 'webpack/types';
import type ConverterBase from './lib/converters/ConverterBase';
import { DETAILED, MINIFIED, VALID_MINIFIERS_KEYS } from './lib/constants/minifiers';
import ConverterMinified from './lib/converters/ConverterMinified';
import ConverterDetailed from './lib/converters/ConverterDetailed';
import injectConverter from './lib/injectConverter';

const minifiers = {
[MINIFIED]: ConverterMinified,
[DETAILED]: ConverterDetailed,
}

let classnamesMinifier: ConverterBase;
let infoMessageShown = false;

type Options = {dev?: typeof VALID_MINIFIERS_KEYS[number], prod?: typeof VALID_MINIFIERS_KEYS[number]};

const withClassnameMinifier = (nextConfig: any = {}) => ({
const withClassnameMinifier = (pluginOptions: Options = {}) => (nextConfig: any = {}) => ({
...nextConfig,
webpack: (config: Configuration, options: any) => {
const { classesConverter = 'minified' } = nextConfig;
const { dev = 'detailed', prod = 'minified' } = pluginOptions;
const isProd = process.env.NODE_ENV === 'production';
const minifierType = isProd ? prod : dev;
const isDisabled = minifierType === 'none';

if (!infoMessageShown) {
if (!VALID_MINIFIERS_KEYS.includes(prod)) {
console.log(`next-classnames-minifier. Invalid key for prod env: ${dev}, valid keys are: ${VALID_MINIFIERS_KEYS.join(', ')}`);
process.kill(0);
process.exit();
}
if (!VALID_MINIFIERS_KEYS.includes(dev)) {
console.log(`next-classnames-minifier. Invalid key for dev env: ${dev}, valid keys are: ${VALID_MINIFIERS_KEYS.join(', ')}`);
process.kill(0);
process.exit();
} else if (dev === 'minified') {
console.log(`next-classnames-minifier. Do not use "minified" variant for dev mode. It's to unstable, use "detailed" or "none" instead`);
}
infoMessageShown = true;
}

if (classesConverter === 'minified' || classesConverter === 'detailed') {
if (!isDisabled && minifierType in minifiers) {
if (!classnamesMinifier) {
if (classesConverter === 'minified') {
classnamesMinifier = new ConverterMinified();
} else {
classnamesMinifier = new ConverterDetailed();
}
classnamesMinifier = new minifiers[minifierType as keyof typeof minifiers]();
}

injectConverter(classnamesMinifier, config.module?.rules)
injectConverter(classnamesMinifier, config.module?.rules);
}

if (typeof nextConfig.webpack === 'function') {
Expand Down

0 comments on commit 484ecd3

Please sign in to comment.