Security News
GitHub Removes Malicious Pull Requests Targeting Open Source Repositories
GitHub removed 27 malicious pull requests attempting to inject harmful code across multiple open source repositories, in another round of low-effort attacks.
An exceptionally fast, thorough and tiny (~10 KB min) unused-CSS cleaner (MIT Licensed)
DropCSS takes your HTML and CSS as input and returns only the used CSS as output. Its custom HTML and CSS parsers are highly optimized for the 99% use case and thus avoid the overhead of handling malformed markup or stylesheets, so you must provide well-formed input. There is minimal handling for complex escaping rules, so there will always exist cases of valid input that cannot be processed by DropCSS; for these infrequent cases, please start a discussion.
It's also a good idea to run your CSS through a structural optimizer like clean-css, csso, cssnano or crass to re-group selectors, merge redundant rules, etc. It probably makes sense to do this after DropCSS, which can leave redundant blocks, e.g. .foo, .bar { color: red; } .bar { width: 50%; }
-> .bar { color: red; } .bar { width: 50%; }
if .foo
is absent from your markup.
More on this project's backstory & discussions: v0.1.0 alpha: /r/javascript, Hacker News and v1.0.0 release: /r/javascript.
npm install -D dropcss
const dropcss = require('dropcss');
let html = `
<html>
<head></head>
<body>
<p>Hello World!</p>
</body>
</html>
`;
let css = `
.card {
padding: 8px;
}
p:hover a:first-child {
color: red;
}
`;
const whitelist = /#foo|\.bar/;
let dropped = new Set();
let cleaned = dropcss({
html,
css,
shouldDrop: (sel) => {
if (whitelist.test(sel))
return false;
else {
dropped.add(sel);
return true;
}
},
});
console.log(cleaned.css);
console.log(dropped);
The shouldDrop
hook is called for every CSS selector that could not be matched in the html
. Return false
to retain the selector or true
to drop it.
@font-face
and @keyframes
blocks.:root {
--font-style: italic;
--font-weight: bold;
--line-height: var(--height)em;
--font-family: 'Open Sans';
--font: var(--font-style) var(--font-weight) 1em/var(--line-height) var(--font-family);
--height: 1.6;
}
@font-face {
font-family: var(--font-family);
src: url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2"),
url("/fonts/OpenSans-Regular-webfont.woff") format("woff");
}
body {
font: var(--font);
}
*
- universal<tag>
- tag#
- id.
- class
- descendant>
- child+
- adjacent sibling~
- general sibling[attr]
- attribute[attr=val]
[attr*=val]
[attr^=val]
[attr$=val]
[attr~=val]
:not()
:first-child
:last-child
:only-child
:nth-child()
:nth-last-child()
:first-of-type
:last-of-type
:only-of-type
:nth-of-type()
:nth-last-of-type()
test.html
document.querySelectorAll("*").length
styles.min.css
lib size w/deps | output size | reduction | time elapsed | unused bytes (test.html coverage) | |
---|---|---|---|---|---|
DropCSS |
58.4 KB 6 Files, 2 Folders | 6.58 KB | 76.15% | 21 ms | 575 / 8.5% |
UnCSS |
13.5 MB 2,829 Files, 301 Folders | 6.72 KB | 75.71% | 385 ms | 638 / 9.3% |
Purgecss |
2.69 MB 560 Files, 119 Folders | 8.01 KB | 71.05% | 88 ms | 1,806 / 22.0% |
PurifyCSS |
3.46 MB 792 Files, 207 Folders | 15.46 KB | 44.34% | 173 ms | 9,440 / 59.6% |
Notes
A full Stress Test is also available.
DropCSS does not load external resources or execute <script>
tags, so your HTML must be fully formed (or SSR'd). Alternatively, you can use Puppeteer and a local http server to get full <script>
execution.
Here's a 35 line script which does exactly that:
const httpServer = require('http-server');
const puppeteer = require('puppeteer');
const fetch = require('node-fetch');
const dropcss = require('dropcss');
const server = httpServer.createServer({root: './www'});
server.listen(8080);
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('http://127.0.0.1:8080/index.html');
const html = await page.content();
const styleHrefs = await page.$$eval('link[rel=stylesheet]', els => Array.from(els).map(s => s.href));
await browser.close();
await Promise.all(styleHrefs.map(href =>
fetch(href).then(r => r.text()).then(css => {
let start = +new Date();
let clean = dropcss({
css,
html,
});
console.log({
stylesheet: href,
cleanCss: clean.css,
elapsed: +new Date() - start,
});
})
));
server.close();
})();
Perhaps you want to take one giant CSS file and purge it against multiple HTML sources, thus retaining any selectors that appear in any HTML source. This also applies when using Puppeteer to invoke different application states to ensure that DropCSS takes every state into account before cleaning the CSS. The idea is rather simple:
See /demos/accumulate.js:
const dropcss = require('dropcss');
// super mega-huge combined stylesheet
let css = `
em {
color: red;
}
p {
font-weight: bold;
}
.foo {
font-size: 10pt;
}
`;
// html of page (or state) A
let htmlA = `
<html>
<head></head>
<body>
<em>Hello World!</em>
</body>
</html>
`;
// html of page (or state) B
let htmlB = `
<html>
<head></head>
<body>
<p>Soft Kitties!</p>
</body>
</html>
`;
// whitelist
let whitelist = new Set();
let resA = dropcss({
css,
html: htmlA,
});
// accumulate retained A selectors
resA.sels.forEach(sel => whitelist.add(sel));
let resB = dropcss({
css,
html: htmlB,
});
// accumulate retained B selectors
resB.sels.forEach(sel => whitelist.add(sel));
// final purge relying only on accumulated whitelist
let cleaned = dropcss({
html: '',
css,
shouldDrop: sel => !whitelist.has(sel),
});
console.log(cleaned.css);
DropCSS is stupid and will choke on unusual selectors, like the ones used by the popular Tailwind CSS framework:
class
attributes can look like this:
<div class="px-6 pt-6 overflow-y-auto text-base lg:text-sm lg:py-12 lg:pl-6 lg:pr-8 sticky?lg:h-(screen-16)"></div>
<div class="px-2 -mx-2 py-1 transition-fast relative block hover:translate-r-2px hover:text-gray-900 text-gray-600 font-medium"></div>
...and the CSS looks like this:
.sticky\?lg\:h-\(screen-16\){...}
.lg\:text-sm{...}
.lg\:focus\:text-green-700:focus{...}
Ouch.
The solution is to temporarily replace the escaped characters in the HTML and CSS with some unique strings which match /[\w-]/
. This allows DropCSS's tokenizer to consider the classname as one contiguous thing. After processing, we simply reverse the operation.
// remap
let css2 = css
.replace(/\\\:/gm, '__0')
.replace(/\\\//gm, '__1')
.replace(/\\\?/gm, '__2')
.replace(/\\\(/gm, '__3')
.replace(/\\\)/gm, '__4');
let html2 = html.replace(/class=["'][^"']*["']/gm, m =>
m
.replace(/\:/gm, '__0')
.replace(/\//gm, '__1')
.replace(/\?/gm, '__2')
.replace(/\(/gm, '__3')
.replace(/\)/gm, '__4')
);
let res = dropcss({
css: css2,
html: html2,
});
// undo
res.css = res.css
.replace(/__0/gm, '\\:')
.replace(/__1/gm, '\\/')
.replace(/__2/gm, '\\?')
.replace(/__3/gm, '\\(')
.replace(/__4/gm, '\\)');
This performant work-around allows DropCSS to process Tailwind without issues \o/ and is easily adaptable to support other "interesting" cases. One thing to keep in mind is that shouldDrop()
will be called with selectors containing the temp replacements rather than original selectors, so make sure to account for this if shouldDrop()
is used to test against some whitelist.
An+B
expression testing exactly right is frustrating. I got part-way there before discovering this tiny solution.FAQs
An exceptionally fast, thorough and tiny unused-CSS cleaner
The npm package dropcss receives a total of 1,085 weekly downloads. As such, dropcss popularity was classified as popular.
We found that dropcss demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
GitHub removed 27 malicious pull requests attempting to inject harmful code across multiple open source repositories, in another round of low-effort attacks.
Security News
RubyGems.org has added a new "maintainer" role that allows for publishing new versions of gems. This new permission type is aimed at improving security for gem owners and the service overall.
Security News
Node.js will be enforcing stricter semver-major PR policies a month before major releases to enhance stability and ensure reliable release candidates.