├── .gitignore ├── test ├── fixtures │ ├── import-in-css.css │ ├── import-in-js.css │ ├── link-in-html.css │ ├── link-tag-js.css │ ├── link-tag-html.html │ ├── style-tag-html.html │ ├── inline-style-html.html │ ├── style-tag-js.html │ ├── link-tag-js.html │ ├── inline-style-js.html │ └── kitchen-sink.html ├── css-in-js.test.js ├── style-in-html.test.js ├── link-in-html.test.js ├── style-created-with-js.test.js ├── link-created-with-js.test.js ├── inline.test.js └── index.js ├── .github ├── funding.yml ├── workflows │ ├── labels.yml │ ├── test.yml │ └── publish.yml └── labels.json ├── package.json ├── license ├── readme.md └── src └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /test/fixtures/import-in-css.css: -------------------------------------------------------------------------------- 1 | .css-imported-with-css {} -------------------------------------------------------------------------------- /test/fixtures/import-in-js.css: -------------------------------------------------------------------------------- 1 | .css-imported-with-js {} -------------------------------------------------------------------------------- /test/fixtures/link-in-html.css: -------------------------------------------------------------------------------- 1 | @import url("import-in-css.css"); 2 | 3 | .link-in-html { } -------------------------------------------------------------------------------- /test/fixtures/link-tag-js.css: -------------------------------------------------------------------------------- 1 | @import url("import-in-css.css"); 2 | 3 | .link-tag-created-with-js {} 4 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: bartveneman 2 | patreon: bartveneman 3 | open_collective: projectwallace 4 | custom: ['https://www.projectwallace.com/sponsor', 'https://www.paypal.me/bartveneman'] 5 | -------------------------------------------------------------------------------- /.github/workflows/labels.yml: -------------------------------------------------------------------------------- 1 | name: Create Default Labels 2 | 3 | on: 4 | issues: 5 | type: [ opened ] 6 | 7 | jobs: 8 | labels: 9 | name: Create Default Labels 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@1.0.0 15 | - uses: lannonbr/issue-label-manager-action@2.0.0 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /test/fixtures/link-tag-html.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |

<link> tag in HTML

11 |
imported
12 | 13 | -------------------------------------------------------------------------------- /test/fixtures/style-tag-html.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 15 | 16 | 17 | 18 |

<style> tag in HTML

19 | 20 | 21 | -------------------------------------------------------------------------------- /test/fixtures/inline-style-html.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |

Inline style in HTML

10 |

Another inline styled element

11 |

Element with empty style attribute

12 |

Another empty style attribute

13 | 14 | -------------------------------------------------------------------------------- /test/fixtures/style-tag-js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |

<style> tag in JS

12 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/fixtures/link-tag-js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 |

<link> tag in JS

12 |
imported
13 | 19 | 20 | -------------------------------------------------------------------------------- /test/css-in-js.test.js: -------------------------------------------------------------------------------- 1 | const { suite } = require('uvu') 2 | const assert = require('uvu/assert') 3 | const createTestServer = require('create-test-server') 4 | const sirv = require('sirv') 5 | 6 | const Test = suite('CSS in JS') 7 | const extractCss = require('..') 8 | 9 | let server 10 | 11 | Test.before(async () => { 12 | server = await createTestServer() 13 | server.use(sirv('test/fixtures')) 14 | }) 15 | 16 | Test.after(async () => { 17 | await server.close() 18 | }) 19 | 20 | Test('finds CSS directly from \'ed file', async () => { 21 | const actual = await extractCss(server.url + '/css-in-js.html') 22 | 23 | const expected = '.bcMPWx { color: blue; }' 24 | assert.equal(actual, expected) 25 | }) 26 | 27 | Test.run() 28 | -------------------------------------------------------------------------------- /test/fixtures/inline-style-js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 |

Inline styles with JS

10 |

Another inliner

11 | 12 | 25 | 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [12.x, 14.x, 16.x, 18.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm ci 27 | - run: npm test 28 | env: 29 | CI: true 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extract-css-core", 3 | "description": "Extract all CSS from a given url, both server side and client side rendered.", 4 | "version": "3.0.0", 5 | "homepage": "https://www.projectwallace.com/oss", 6 | "repository": "https://github.com/projectwallace/extract-css-core", 7 | "issues": "https://github.com/projectwallace/extract-css-core/issues", 8 | "license": "MIT", 9 | "author": "Bart Veneman", 10 | "keywords": [ 11 | "extract", 12 | "css", 13 | "scrape", 14 | "get-css" 15 | ], 16 | "scripts": { 17 | "test": "uvu test" 18 | }, 19 | "files": [ 20 | "src" 21 | ], 22 | "main": "src/index.js", 23 | "engines": { 24 | "node": ">=12.0" 25 | }, 26 | "devDependencies": { 27 | "create-test-server": "3.0.1", 28 | "sirv": "1.0.19", 29 | "uvu": "0.5.3" 30 | }, 31 | "dependencies": { 32 | "normalize-url": "6.1.0", 33 | "puppeteer": "13.7.0" 34 | } 35 | } -------------------------------------------------------------------------------- /.github/labels.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "name": "🐛 defect", "color": "b62020", "description": "Something isn't working as expected" }, 3 | { "name": "🚨 breaking change", "color": "b62020", "description": "changes that require a major version bump" }, 4 | { "name": "📚 dependencies", "color": "0854c4", "description": "pull requests that update a dependency file" }, 5 | { "name": "👯‍♂️ duplicate", "color": "9eacb3", "description": "this issue or pull request already exists" }, 6 | { "name": "✨ enhancement", "color": "29c87d", "description": "New feature or request" }, 7 | { "name": "🚸 help wanted", "color": "0854c4", "description": "issue needs help" }, 8 | { "name": "🗣 feedback wanted", "color": "0854c4", "description": "community feedback wanted" }, 9 | { "name": "⚠️ tests", "color": "f10e69", "description": "issues regarding test suite" }, 10 | { "name": "🛑 wontfix", "color": "f8f8f8", "description": "not in scope of this package or can be achieved otherwise" } 11 | ] 12 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: NPM Publish 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 12 18 | - run: npm ci 19 | - run: npm test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v1 27 | with: 28 | node-version: 12 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 34 | -------------------------------------------------------------------------------- /test/style-in-html.test.js: -------------------------------------------------------------------------------- 1 | const { suite } = require('uvu') 2 | const assert = require('uvu/assert') 3 | const createTestServer = require('create-test-server') 4 | const sirv = require('sirv') 5 | 6 | const Test = suite('CSS in 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 | 24 | 25 | 31 | 32 | 33 | 38 | 39 | 40 | 44 | 48 | 49 | 50 | 54 | 55 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const { suite } = require('uvu') 2 | const assert = require('uvu/assert') 3 | const createTestServer = require('create-test-server') 4 | const sirv = require('sirv') 5 | 6 | const Test = suite('Extract CSS') 7 | const extractCss = require('..') 8 | 9 | let server 10 | 11 | Test.before(async () => { 12 | server = await createTestServer() 13 | server.use(sirv('test/fixtures')) 14 | }) 15 | 16 | Test.after(async () => { 17 | await server.close() 18 | }) 19 | 20 | Test('it finds CSS implemented in a mixed methods (inline, links, style tags)', async () => { 21 | const actual = await extractCss(server.url + '/kitchen-sink.html') 22 | 23 | assert.ok(actual.includes('@import url("import-in-css.css")')) 24 | assert.ok(actual.includes('.css-imported-with-css { }')) 25 | assert.ok(actual.includes('[x-extract-css-inline-style]')) 26 | assert.ok(actual.includes('[x-extract-css-inline-style] { background-image: url(\'background-image-inline-style-attribute-in-html\'); }')) 27 | assert.ok(actual.includes('[x-extract-css-inline-style] { background-image: url("background-image-inline-style-js-cssText"); }')) 28 | assert.ok(actual.includes('[x-extract-css-inline-style] { background-image: url("background-image-inline-style-js-with-prop"); }')) 29 | }) 30 | 31 | Test('it yields an array of entries when the `origins` option equals `include`', async () => { 32 | const actual = await extractCss(server.url + '/kitchen-sink.html', { 33 | origins: 'include' 34 | }) 35 | 36 | assert.ok(Array.isArray(actual), 'Result should be an array when { origins: `include` }') 37 | assert.is(actual.length, 12) 38 | 39 | function isString(item) { 40 | return typeof item === 'string' 41 | } 42 | 43 | assert.ok(actual.every(item => isString(item.type) && ['link', 'import', 'style', 'inline'].includes(item.type))) 44 | assert.ok(actual.every(item => isString(item.href))) 45 | assert.ok(actual.every(item => item.href.startsWith('http://localhost:') && /\.(html|css)$/.test(item.href))) 46 | assert.ok(actual.every(item => isString(item.css))) 47 | 48 | // Cannot snapshot due to changing port numbers in `create-test-server` 49 | }) 50 | 51 | Test('it returns a direct link to a CSS file', async () => { 52 | const actual = await extractCss(server.url + '/import-in-css.css') 53 | 54 | assert.equal(actual, '.css-imported-with-css {}') 55 | }) 56 | 57 | Test('it rejects if the url has an HTTP error status', async () => { 58 | server.get('/404-page', (req, res) => { 59 | res.status(404).send() 60 | }) 61 | const urlWith404 = server.url + '/404-page' 62 | 63 | try { 64 | await extractCss(urlWith404) 65 | assert.unreachable('should have thrown') 66 | } catch (error) { 67 | assert.instance(error, Error) 68 | assert.is(error.message, `There was an error retrieving CSS from ${urlWith404}.\n\tHTTP status code: 404 (Not Found)`) 69 | } 70 | }) 71 | 72 | Test('it rejects on an invalid url', async () => { 73 | try { 74 | await extractCss('site.example') 75 | assert.unreachable('should have thrown') 76 | } catch (error) { 77 | assert.instance(error, Error) 78 | } 79 | }) 80 | 81 | Test.run() 82 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |
2 |

extract-css-core

3 |

Extract all CSS from a given url, both server side and client side rendered.

4 |
5 | 6 | [![NPM Version](https://img.shields.io/npm/v/extract-css-core.svg)](https://www.npmjs.com/package/extract-css-core) 7 | ![Node.js CI](https://github.com/bartveneman/extract-css-core/workflows/Node.js%20CI/badge.svg) 8 | ## Usage 9 | 10 | ```js 11 | const extractCss = require('extract-css-core') 12 | 13 | const css = await extractCss('https://www.projectwallace.com') 14 | //=> html{font-size:100%} etc. 15 | ``` 16 | 17 | Or, if you want more details: 18 | 19 | ```js 20 | const entries = await extractCss('https://www.projectwallace.com', { 21 | origins: 'include' 22 | }) 23 | 24 | // entries will look something like this 25 | [ 26 | { 27 | href: 'https://www.projectwallace.com', 28 | type: 'link', 29 | css: '@font-face{font-display:swap;font-family:Teko;...' 30 | }, 31 | { 32 | href: 'https://www.projectwallace.com/client/Seo.0f4fe72f.css', 33 | type: 'style', 34 | css: '.hero__text.svelte-qhblau a{color:var(--teal-400)}...' 35 | }, 36 | { 37 | href: 'https://www.projectwallace.com/client/some-css-file.css', 38 | type: 'import', 39 | css: '.some-css {}' 40 | }, 41 | { 42 | href: 'https://www.projectwallace.com', 43 | type: 'inline', 44 | css: '[x-extract-css-inline-style] { position: absolute; }' 45 | } 46 | ] 47 | ``` 48 | 49 | ## Installation 50 | 51 | ```sh 52 | npm install extract-css-core 53 | # or 54 | yarn add extract-css-core 55 | ``` 56 | 57 | ## Motivation, solution and shortcomings 58 | 59 | ### Motivation 60 | 61 | Existing packages like 62 | [get-css](https://github.com/cssstats/cssstats/tree/master/packages/get-css) 63 | look at a server-generated piece of HTML and get all the `` and `