Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minimizing css extraction #39

Merged
merged 2 commits into from
Mar 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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