Skip to content

Commit

Permalink
Merge pull request #39 from theKashey/minimizing-extraction
Browse files Browse the repository at this point in the history
Minimizing css extraction
  • Loading branch information
theKashey authored Mar 31, 2022
2 parents 5bd36a8 + 0b83f8e commit 65e5f99
Show file tree
Hide file tree
Showing 10 changed files with 159 additions and 8 deletions.
6 changes: 6 additions & 0 deletions __tests__/__snapshots__/ast.spec.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ Object {
"declaration": 1,
"hash": ".b .c-1sc6hdk-1gpll6f0",
"media": Array [],
"parents": Array [
"b",
],
"pieces": Array [
"c",
],
Expand All @@ -179,6 +182,9 @@ Object {
"declaration": 2,
"hash": ".d ~ .e:not(focused)-1u2ggaf-o0go610",
"media": Array [],
"parents": Array [
"d",
],
"pieces": Array [
"e",
],
Expand Down
60 changes: 59 additions & 1 deletion __tests__/extraction.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getCriticalRules, loadStyleDefinitions, StyleDefinition } from '../src';
import { alterProjectStyles, getCriticalRules, loadStyleDefinitions, StyleDefinition } from '../src';

describe('extraction stories', () => {
it('handles duplicated selectors', async () => {
Expand Down Expand Up @@ -95,4 +95,62 @@ describe('extraction stories', () => {
"
`);
});

it('reducing styles', async () => {
const styles: StyleDefinition = loadStyleDefinitions(
() => ['test.css'],
() => `
.button {
display: inline-block;
}
.button:focus {
padding: 10px;
}
`
);
await styles;

const extracted = getCriticalRules(
'<div class="button">',
alterProjectStyles(styles, {
pruneSelector: (selector) => selector.includes(':focus'),
})
);

expect(extracted).toMatchInlineSnapshot(`
"
/* test.css */
.button { display: inline-block; }
"
`);
});

it('opening styles styles', async () => {
const styles: StyleDefinition = loadStyleDefinitions(
() => ['test.css'],
() => `
.parent {
display: inline-block;
}
.child {
padding: 10px;
}
.parent .child {
padding: 10px;
}
`
);
await styles;

const extracted = getCriticalRules('<div class="child">', styles);

expect(extracted).toMatchInlineSnapshot(`
"
/* test.css */
.child { padding: 10px; }
"
`);
});
});
29 changes: 28 additions & 1 deletion __tests__/mapStyles.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mapSelector } from '../src/parser/utils';
import { extractParents, mapSelector } from '../src/parser/utils';

describe('test map selector', () => {
it('should return the single style', () => {
Expand Down Expand Up @@ -26,3 +26,30 @@ describe('test map selector', () => {
expect(mapSelector('.item+.item:before')).toEqual(['item']);
});
});

describe('test parent selector', () => {
it('should return the single style', () => {
expect(extractParents('.a')).toEqual([]);
});

it('should return the double style', () => {
expect(extractParents('.a.b c')).toEqual(['a.b']);
});

it('should keep the first style; drop last', () => {
expect(extractParents('.a .b')).toEqual(['a']);
});

it('should drop tag, keep both', () => {
expect(extractParents('.a .b input')).toEqual(['a', 'b']);
});

it('should handle pseudo', () => {
expect(extractParents('.a .b:focus .c')).toEqual(['a', 'b']);
});

it('edge cases', () => {
expect(extractParents('.a input>.b:focus>input.c')).toEqual(['a']);
expect(extractParents('.item+.item:before')).toEqual([]);
});
});
18 changes: 14 additions & 4 deletions src/operations.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import { pruneSelector } from './operations/prune-selector';
import { StyleAst } from './parser/ast';
import { StyleDefinition } from './types';
import { assertIsReady } from './utils/async';

export interface AlterOptions {
// filters available styles
filter(fileName: string): boolean;
/**
* filters available styles sources/files
* @param fileName
*/
filter?(fileName: string): boolean;

/**
* filters available rule
* @param styleName
*/
pruneSelector?(selector: string): boolean;
}

/**
Expand All @@ -23,14 +33,14 @@ export const alterProjectStyles = (def: StyleDefinition, options: AlterOptions):
...def,
ast: Object.keys(def.ast).reduce((acc, file) => {
const astFile = def.ast[file];
const shouldRemove = !options.filter || !options.filter(file);
const shouldRemove = options.filter && !options.filter(file);

// dont add this file to the result file list
if (shouldRemove) {
return acc;
}

acc[file] = astFile;
acc[file] = options.pruneSelector ? pruneSelector(astFile, options.pruneSelector) : astFile;

return acc;
}, {} as StyleAst),
Expand Down
9 changes: 9 additions & 0 deletions src/operations/prune-selector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { SingleStyleAst } from '../parser/ast';

export const pruneSelector = (
ast: Readonly<SingleStyleAst>,
filter: (name: string) => boolean
): Readonly<SingleStyleAst> => ({
...ast,
selectors: ast.selectors.filter((selector) => !filter(selector.selector)),
});
1 change: 1 addition & 0 deletions src/parser/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface StyleSelector {

pieces: string[];
media: string[];
parents?: string[];

declaration: number;
hash: string;
Expand Down
7 changes: 6 additions & 1 deletion src/parser/toAst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { AtRule, Rule } from 'postcss';

import { AtRules, SingleStyleAst, StyleBodies, StyleBody, StyleSelector } from './ast';
import { createRange, localRangeMax, localRangeMin, rangesIntervalEqual } from './ranges';
import { mapSelector } from './utils';
import { extractParents, mapSelector } from './utils';

const getAtRule = (rule: AtRule | Rule): string[] => {
if (rule && rule.parent && 'name' in rule.parent && rule.parent.name === 'media') {
Expand Down Expand Up @@ -104,6 +104,11 @@ export const buildAst = (CSS: string, file = ''): SingleStyleAst => {
declaration: 0,
hash: selector,
};
const parents = extractParents(selector);

if (parents.length > 0) {
stand.parents = parents;
}

const delc: StyleBody = {
id: NaN,
Expand Down
20 changes: 19 additions & 1 deletion src/parser/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,29 @@ export const mapStyles = (styles: string): string[] =>
.map((x) => x.replace(/[\s,.>~+$]+/, ''))
.map((x) => x.replace(/[.\s.:]+/, ''));

export const extractParents = (selector: string): string[] => {
// replace `something:not(.something)` to `something:not`
const cleanSelector = selector.replace(/\(([^)])*\)/g, '').replace(/(\\\+)/g, 'PLUS_SYMBOL');
const parts = cleanSelector.split(' ');
// remove the last part
parts.pop();

const ruleSelection =
// anything like "class"
parts.join(' ').match(/\.([^>~+$:{\[\s]+)?/g) || [];

const effectiveMatcher = ruleSelection.filter(classish);

const selectors = effectiveMatcher.map((x) => x.replace(/[.\s.:]+/, '').replace(/PLUS_SYMBOL/g, '+')).filter(Boolean);

return selectors;
};

export const mapSelector = (selector: string): string[] => {
// replace `something:not(.something)` to `something:not`
const cleanSelector = selector.replace(/\(([^)])*\)/g, '').replace(/(\\\+)/g, 'PLUS_SYMBOL');
const ruleSelection =
// anything like "style"
// anything like "class"
cleanSelector.match(/\.([^>~+$:{\[\s]+)?/g) || [];

ruleSelection.reverse();
Expand Down
7 changes: 7 additions & 0 deletions src/utils/__tests__/class-extraction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { getStylesInReactText } from '../string';

test('extract classes from html', () => {
expect(getStylesInReactText('<div />')).toEqual([]);
expect(getStylesInReactText('<div class="a"/>')).toEqual(['a']);
expect(getStylesInReactText('<div class="a b"/>')).toEqual(['a b']);
});
10 changes: 10 additions & 0 deletions src/utils/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,20 @@ export const createUsedFilter = () => {
const usedSelectors = new Set<string>();

return (_: any, rule: StyleSelector) => {
// if rule is already seen - skip
if (usedSelectors.has(rule.hash)) {
return false;
}

// if one of the parents of this rule has not been introduced yed - skip
const parents = rule.parents;

if (parents) {
if (!parents.every((parent) => usedSelectors.has(parent))) {
return false;
}
}

usedSelectors.add(rule.hash);

return true;
Expand Down

0 comments on commit 65e5f99

Please sign in to comment.