Skip to content

Commit

Permalink
Merge b373ed6 into 22b8126
Browse files Browse the repository at this point in the history
  • Loading branch information
JPeer264 committed Aug 9, 2020
2 parents 22b8126 + b373ed6 commit 5f5da58
Show file tree
Hide file tree
Showing 9 changed files with 306 additions and 0 deletions.
64 changes: 64 additions & 0 deletions __tests__/optimize/optimize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import rcs from '../../lib';
import sortSelectors from '../../lib/optimize/sortSelectors';

jest.mock('../../lib/optimize/sortSelectors');

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockedSortSelectors = (sortSelectors as any) as jest.Mock<typeof sortSelectors>;

beforeEach(() => {
rcs.cssVariablesLibrary.setAlphabet('#abcdefghijklmnopqrstuvwxyz');
rcs.cssVariablesLibrary.reset();
rcs.keyframesLibrary.setAlphabet('#abcdefghijklmnopqrstuvwxyz');
rcs.keyframesLibrary.reset();
rcs.selectorsLibrary.setAlphabet('#abcdefghijklmnopqrstuvwxyz');
rcs.selectorsLibrary.reset();

mockedSortSelectors.mockImplementation((array) => (
array.map(([selector]) => selector).sort()
));
});

test('should optimize', () => {
rcs.mapping.load({
selectors: {
'#test': 'a',
'.ca': 'a',
'.ba': 'b',
'.aa': 'c',
},
});
rcs.statistics.load({
ids: {
unused: [],
usageCount: {
test: 2,
},
},
classes: {
unused: [],
usageCount: {
'my-selector': 2,
},
},
keyframes: {
unused: [],
usageCount: {},
},
cssVariables: {
unused: [],
usageCount: {},
},
});

rcs.optimize();

expect(rcs.mapping.generate()).toEqual({
selectors: {
'#test': 'a',
'.aa': 'a',
'.ba': 'b',
'.ca': 'c',
},
});
});
38 changes: 38 additions & 0 deletions __tests__/optimize/separateMappingSelectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import separateMappingSelectors from '../../lib/optimize/separateMappingSelectors';

test('should do nothing', () => {
expect(separateMappingSelectors()).toEqual({
cssVariables: [],
keyframes: [],
classes: [],
ids: [],
});
});

test('should separate selectors correctly', () => {
expect(separateMappingSelectors({
'.test': 'a',
'#my-id': 'a',
'#my-other-id': 'b',
})).toEqual({
cssVariables: [],
keyframes: [],
classes: [['test', 'a']],
ids: [['my-id', 'a'], ['my-other-id', 'b']],
});
});

test('should separate selectors, keyframes and cssVariables correctly', () => {
expect(separateMappingSelectors({
'.test': 'a',
'#my-id': 'a',
'#my-other-id': 'b',
'-my-var': 'a',
'@my-keyframe': 'a',
})).toEqual({
cssVariables: [['my-var', 'a']],
keyframes: [['my-keyframe', 'a']],
classes: [['test', 'a']],
ids: [['my-id', 'a'], ['my-other-id', 'b']],
});
});
31 changes: 31 additions & 0 deletions __tests__/optimize/sortSelectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import sortSelectors from '../../lib/optimize/sortSelectors';

test('should optimize', () => {
const result = sortSelectors(
[],
{
unused: [],
usageCount: {
'short-selector': 14,
'a-very-long-selector': 2,
},
},
);

expect(result).toEqual(['short-selector', 'a-very-long-selector']);
});

test('should optimize', () => {
const result = sortSelectors(
[],
{
unused: [],
usageCount: {
'short-selector': 2,
'a-very-long-selector': 14,
},
},
);

expect(result).toEqual(['a-very-long-selector', 'short-selector']);
});
2 changes: 2 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as nameGenerator from './nameGenerator';
import selectorsLibrary from './selectorsLibrary';
import keyframesLibrary from './keyframesLibrary';
import cssVariablesLibrary from './cssVariablesLibrary';
import optimize from './optimize';

import generate from './mapping/generate';
import load from './mapping/load';
Expand Down Expand Up @@ -34,6 +35,7 @@ export default {
generate,
load,
},
optimize,
statistics: {
generate: generateStats,
load: loadStats,
Expand Down
3 changes: 3 additions & 0 deletions lib/optimize/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import optimize from './optimize';

export default optimize;
71 changes: 71 additions & 0 deletions lib/optimize/optimize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import generateMapping from '../mapping/generate';
import generateStatistics from '../statistics/generate';
import { getStatistics } from '../statistics';
import loadMapping from '../mapping/load';
import selectorsLibrary from '../selectorsLibrary';
import keyframesLibrary from '../keyframesLibrary';
import cssVariablesLibrary from '../cssVariablesLibrary';
import separateMappingSelectors from './separateMappingSelectors';
import sortSelectors from './sortSelectors';

const optimize = (): void => {
const mapping = generateMapping();
const statistics = getStatistics();

if (!statistics) {
return;
}

// sorting
// renaming into new mapping
const separateMapping = separateMappingSelectors(mapping.selectors);
const optimizedMapping: { [key in keyof ReturnType<typeof generateStatistics>]?: string[] } = {};

// optimize each library
Object.entries(separateMapping).forEach(([key, selectors]) => {
const statisticsData = statistics[key as 'ids'];
const newSortedSelectors = sortSelectors(selectors, statisticsData);

optimizedMapping[key as 'ids'] = newSortedSelectors;
});

// could be a cold start but
// reset everything just in case
selectorsLibrary.reset();
keyframesLibrary.reset();
cssVariablesLibrary.reset();

// fill libraries with optimized mapping
Object.entries(optimizedMapping).forEach(([key, selectors]) => {
if (!selectors) {
return;
}

switch (key) {
case 'ids':
selectors.forEach((selector) => selectorsLibrary.set(`#${selector}`));

break;

case 'keyframes':
selectors.forEach((selector) => keyframesLibrary.set(`@${selector}`));

break;

case 'cssVariables':
selectors.forEach((selector) => cssVariablesLibrary.set(`--${selector}`));

break;

case 'classes':
default:
selectors.forEach((selector) => selectorsLibrary.set(`.${selector}`));
}
});

// load with the same attribute selectors
// these cannot be optimized yet
loadMapping({ attributeSelectors: mapping.attributeSelectors });
};

export default optimize;
44 changes: 44 additions & 0 deletions lib/optimize/separateMappingSelectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import generate from '../mapping/generate';
import generateStatistics from '../statistics/generate';

type Result = { [key in keyof ReturnType<typeof generateStatistics>]: string[][] };

const separateMappingSelectors = (mappingSelectors: ReturnType<typeof generate>['selectors'] = {}): Result => {
const selectors = Object.entries(mappingSelectors);
const result: Result = {
ids: [],
classes: [],
keyframes: [],
cssVariables: [],
};

selectors.forEach(([selector, renamedSelector]) => {
const plainSelector = selector.slice(1);
const toPush = [plainSelector, renamedSelector];

switch (selector.charAt(0)) {
case '-':
result.cssVariables.push(toPush);

break;

case '@':
result.keyframes.push(toPush);

break;

case '#':
result.ids.push(toPush);

break;

case '.':
default:
result.classes.push(toPush);
}
});

return result;
};

export default separateMappingSelectors;
48 changes: 48 additions & 0 deletions lib/optimize/sortSelectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Statistic } from '../statistics/generate';

/**
* the main logic for the optimization
*
* returns a list of sorted selectors to rename
*
* weight selectors and sort them based on their weight
* the weight (W) is length of the selector (L)
* muliplied by the appearences of the selector (C)
*
* Formular: L * C = W
*
* Example with two selectors 'a-very-long-selector' and 'short-selector':
*
* Apperiances:
* 'short-selector': 10 times
* 'a-very-long-selector': 2 times
*
* Length:
* 'short-selector': 14
* 'a-very-long-selector': 20
*
* In this case 'short-selector' is used much more often
* so multiplying with the length will output the total
* amount of saved chars
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
const sortSelectors = (selectors: string[][], statistic: Statistic): string[] => {
const { unused, usageCount } = statistic;

const weightedSelectors = Object.entries(usageCount)
// generate weights
.reduce<[string, number][]>((prev, [selector, count]) => [
...prev,
[selector, selector.length * count],
], [])
// sort based on weights
.sort((a, b) => (
b[1] - a[1]
))
.map(([selector]) => selector);

// put unsuded to the end
return [...weightedSelectors, ...unused];
};

export default sortSelectors;
5 changes: 5 additions & 0 deletions lib/statistics/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ export interface UsageCount {
[s: string]: number;
}

export interface Statistic {
unused: string[];
usageCount: UsageCount;
}

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const generate = () => {
const classSelector = selectorsLibrary.getClassSelector();
Expand Down

0 comments on commit 5f5da58

Please sign in to comment.