Full Code of addyosmani/critical for AI

master 29651ee367f0 cached
126 files
592.7 KB
178.1k tokens
62 symbols
1 requests
Download .txt
Showing preview only (629K chars total). Download the full file or copy to clipboard to get everything.
Repository: addyosmani/critical
Branch: master
Commit: 29651ee367f0
Files: 126
Total size: 592.7 KB

Directory structure:
gitextract_6mprb1p7/

├── .editorconfig
├── .gitattributes
├── .github/
│   └── workflows/
│       ├── docker.yml
│       └── test.yml
├── .gitignore
├── .npmrc
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Dockerfile
├── README.md
├── cli.js
├── index.js
├── license
├── package.json
├── src/
│   ├── array.js
│   ├── config.js
│   ├── core.js
│   ├── errors.js
│   └── file.js
└── test/
    ├── array.test.js
    ├── blackbox.test.js
    ├── cli.test.js
    ├── config.test.js
    ├── core.test.js
    ├── expected/
    │   ├── generate-adaptive-useragent.css
    │   ├── generate-adaptive.css
    │   ├── generate-default-nostyle.css
    │   ├── generate-default-nostyle.html
    │   ├── generate-default.css
    │   ├── generate-ignore.css
    │   ├── generate-ignorefont.css
    │   ├── generate-ignorefont.html
    │   ├── generate-image-absolute.css
    │   ├── generate-image-big.css
    │   ├── generate-image-relative-subfolder.css
    │   ├── generate-image-relative.css
    │   ├── generate-image-skip.css
    │   ├── generate-image.css
    │   ├── generateInline-external-extract.html
    │   ├── generateInline-external-extract2.html
    │   ├── generateInline-external-minified.html
    │   ├── generateInline-external-minified2.html
    │   ├── generateInline-extract.html
    │   ├── generateInline-svg.html
    │   ├── generateInline.html
    │   ├── ignore.css
    │   ├── inline-image.html
    │   ├── inline-minified.html
    │   ├── inline.html
    │   ├── issue-192.css
    │   ├── issue-395.css
    │   ├── issue-566.css
    │   ├── main.css
    │   ├── path-prefix.css
    │   └── streams-default.html
    ├── file.test.js
    ├── fixtures/
    │   ├── 403-css.html
    │   ├── 404-css.html
    │   ├── error.html
    │   ├── folder/
    │   │   ├── generate-default.html
    │   │   ├── generate-image.html
    │   │   ├── index.html
    │   │   ├── relative-different.html
    │   │   ├── relative.html
    │   │   ├── styles/
    │   │   │   └── issue-566.css
    │   │   └── subfolder/
    │   │       ├── generate-image-absolute.html
    │   │       ├── head.html
    │   │       ├── issue-216.css
    │   │       └── relative.html
    │   ├── generate-adaptive-base64.html
    │   ├── generate-adaptive-inline.html
    │   ├── generate-adaptive.html
    │   ├── generate-default-nostyle.html
    │   ├── generate-default-querystring.html
    │   ├── generate-default.html
    │   ├── generate-ignorefont.html
    │   ├── generate-image.html
    │   ├── generateInline-external.html
    │   ├── generateInline-external2.html
    │   ├── generateInline-svg.html
    │   ├── generateInline.html
    │   ├── head.html
    │   ├── ignore.html
    │   ├── ignoreInlinedStyles.html
    │   ├── include.html
    │   ├── inline-image.html
    │   ├── inline.html
    │   ├── issue-192.html
    │   ├── issue-304-nostyle.html
    │   ├── issue-304.html
    │   ├── issue-314.html
    │   ├── issue-395.html
    │   ├── issue-415.html
    │   ├── issue-562.html
    │   ├── issue-566.html
    │   ├── media-attr.html
    │   ├── path-prefix.html
    │   ├── preload.html
    │   ├── print.html
    │   ├── relative-different.html
    │   ├── remote-different.html
    │   ├── streams-default.html
    │   ├── styles/
    │   │   ├── adaptive.css
    │   │   ├── bootstrap.css
    │   │   ├── critical-image-pregenerated.css
    │   │   ├── critical-pregenerated.css
    │   │   ├── font.css
    │   │   ├── ignore.css
    │   │   ├── image-absolute.css
    │   │   ├── image-big.css
    │   │   ├── image-relative.css
    │   │   ├── include.css
    │   │   ├── issue-192.css
    │   │   ├── issue-304.css
    │   │   ├── issue-415.css
    │   │   ├── issue-562.css
    │   │   ├── main.css
    │   │   ├── media-attr.css
    │   │   ├── path-prefix.css
    │   │   ├── print.css
    │   │   └── some/
    │   │       └── path/
    │   │           └── image.css
    │   └── useragent/
    │       ├── generate-default-useragent.html
    │       └── styles/
    │           ├── bootstrap.css
    │           └── main.css
    ├── helper/
    │   └── index.js
    └── index.test.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.css]
insert_final_newline = false

[package.json]
indent_style = space
indent_size = 2

[*.md]
trim_trailing_whitespace = false


================================================
FILE: .gitattributes
================================================
# Enforce Unix newlines
* text=auto eol=lf


================================================
FILE: .github/workflows/docker.yml
================================================
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

# GitHub recommends pinning actions to a commit SHA.
# To get a newer version, you will need to update the SHA.
# You can also reference a tag or branch, but the action may change without warning.

name: Create and publish a Docker image

on:
  push:
    branches:
    - master

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push-image:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: Log in to the Container registry
        uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

      - name: Build and push Docker image
        uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}


================================================
FILE: .github/workflows/test.yml
================================================
name: Tests

on: [push, pull_request]

env:
  CI: true

jobs:
  run:
    name: Node ${{ matrix.node }} on ${{ matrix.os }}
    runs-on: ${{ matrix.os }}

    strategy:
      fail-fast: false
      matrix:
        node: [18, 20]
        os: [ubuntu-latest, windows-latest]

    steps:
      - name: Clone repository
        uses: actions/checkout@v3
        with:
          persist-credentials: false

      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node }}
          cache: npm

      - name: Install npm dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Patch lcov.info
        run: sed -i -- 's/SF:..\//SF:.\//g' ./coverage/lcov.info

      - name: Coveralls Parallel
        uses: coverallsapp/github-action@master
        env:
          NODE_COVERALLS_DEBUG: 1
        with:
          github-token: ${{ secrets.github_token }}
          flag-name: run-${{ matrix.os }}-${{ matrix.node }}
          parallel: true

  finish:
    needs: run
    runs-on: ubuntu-latest
    steps:
    - name: Coveralls Finished
      uses: coverallsapp/github-action@master
      with:
        github-token: ${{ secrets.github_token }}
        parallel-finished: true


================================================
FILE: .gitignore
================================================
/coverage/
/node_modules/
/test/fixture/bower_components/bootstrap/dist/css/*
/test/fixture/test-*
/test/fixture/tmp-*
/test/fixtures/styles/bootstrap.beffebca.css
/test/fixtures/styles/main.d41d8cd9.css


================================================
FILE: .npmrc
================================================
lockfile-version=2


================================================
FILE: CHANGELOG.md
================================================
# v2.0.0 / 2020-06-16

- Drop support for Node.js < 10
- Bump dependencies
- Use Jest for testing
- Drop `include` and `timeout` options as they can be specified in the `penthouse` options.
- Drop options `styleTarget` & `dest` in favour of `target`
  You can specify either a **css** file, an **html** file or an object `{css: dest.css, html: dest.html}` if you want to store both. We may also add an extract target here in a future release.
- Drop options `destFolder`, `folder` and `pathPrefix`. We tried our best to improve the way critical auto-detects the paths to used assets in the critical css which should suit for most cases. If this doesn't work out you can use the new `rebase` option to either specify the location of the css & the html file like this: `{from: '/styles/main.css', to: '/en/test.html'}`. You can also pass a callback function to dynamically compute the path or specify a cdn for example. We utilize [`postcss-url`](https://github.com/postcss/postcss-url#options-list) for this task.
- Due to some limitations with modern css features we replaced `filter-css` as the library of choice for handling ignores with [postcss-discard](https://github.com/bezoerb/postcss-discard/). We tried to keep things backwards compatible but you may have to change your `ignore` configuration.
- Add `concurrency` option to specify how many operations can run in parallel.
- Add the ability to specify used css files using file globs. See supported `minimatch` [patterns](https://github.com/isaacs/minimatch#usage).

# v1.3.4 / 2018-07-19

- fix: return `Promise.reject` instead of re-throw
- fix: handle `PAGE_UNLOADED_DURING_EXECUTION` error (#314)
- output warning on invalid extract setting
- Add user agent option (#316)
- Bump dependencies
- npm audit fix

# v1.3.3 / 2018-06-06

- Bump dependencies
- Docs: fix typo (#310)
- Reduced vulnerabilities (#308)

# v1.3.2 / 2018-05-15

- Switched to async-exit-hook

# v1.3.1 / 2018-05-14

- Bump dependencies
- Removed `process.exit` on cleanup
- Adding html-webpack-critical-plugin to README (#306)

# v1.3.0 / 2018-05-02

- Add basic auth option (#295)

# v1.2.2 / 2018-04-02

- Improved handling of protocol-relative asset URLs (#288)
- Adjust test files according to (#293)
- Improve error reporting (#258)
- Replace gutil with fancy-log (#297)
- Update README.md (#296)

# v1.2.1 / 2018-03-26

- Add license file

# v1.2.0 / 2018-03-19

- Allow external stylesheets to be passed as css option (#290)
- Add Tests for #277

# v1.1.1 / 2018-03-15

- Bump dependencies

# v1.1.0 / 2017-12-02

- 1.1.0
- Remove temporary files
- Bump inline-critical
- Fix corrupted `File.contents` (#191, #218)

# v1.0.0 / 2017-11-06

- Bump dependencies
- Removed deprecated methods
- Don't enforce strict SSL for external assets (#171)
- Allow http 2xx response codes (#244)
- Replace `|` with its HTML character entity reference (#248)
- Headless chrome (#246)
- Add `folder` option to readme (#245)

# v0.9.1 / 2017-09-04

- AppVeyor tweaks
- Use yarn with AppVeyor
- Added package missing in AppVeyor
- Remove AppVeyor cache
- Try to reinstall "css" dependencies
- Upgrade Penthouse
- Update readme according to #220

# v0.9.0 / 2017-07-19

- Bump dependencies
- Library options (#178)
- Ignore print styles (#113) (#221)
- Prefer `let` & `const` + arrow functions
- Run tests on Node.js 8
- Support for passing CSS files as Vinyl objects. (#204)

# v0.8.4 / 2017-03-01

- Better remote handling (#198)
- Bump inline-critical

# v0.8.3 / 2017-02-17

- Fixed multi-dimension critical-path CSS

# v0.8.2 / 2017-02-11

- Bump dependencies
- Update README.md

# v0.8.1 / 2016-11-24

- Added missing comma
- Add tmpfile to garbage collector
- Bump dependencies
- Vinyl (#120)

# v0.8.0 / 2016-08-30

- Revise production-use messaging.
- Consistent CSS capitalization in README.
- Remove object.assign; require Node.js 4.
- Fix all tests to run on Windows.
- Enforce LF.
- Fix xo errors.
- Update dependencies.
- Fix test failures. (#155)
- Travis: add explicitly Node.js 4 and 6. (#154)
- Update .gitignore.
- package.json: remove duplicate dep. (#153)
- Remove JSHint leftovers. (#152)
- Update README.md (#151)
- Update appveyor.yml (#150)
- added penthouse timeout option (#140)
- CSS Rel Preload support (#129)

# v0.7.3 / 2016-05-30

- Bump package.json version
- Add test for 404 case
- Remove trailing whitespace
- Fix silly typo
- Ignore 404 requests, reject promise with `Error` not `String`
- Fixed #130
- Better error message for unresolved css files
- cli: exit after `stdout.write`
- Remove `uncaughtException` listener; log error instead
- Fixed import-order
- Bump dependencies
- Added changelog (#123)

# v0.7.2 / 2016-03-17

- Add include option (#125)

# v0.7.1 / 2016-02-26

- Dropped JSHint and added xo
- Adjust tests for penthouse 0.8.4
- Bump dependencies
- Remove listeners on exit
- Update Readme

# v0.7.0 / 2015-12-22

- bump penthouse
- Test #79
- some debug logs
- trigger cleanup
- added missing deps
- Switch to http server for local files (#94)
- ignore generated css
- tests adjusted for penthouse 0.7.1
- minor tweaks
- Fix AppVeyor tests
- local url for phantomjs (#94)
- penthouse bump
- Bump dependencies
- Bump inline-critical
- Update README.md
- use default base
- add a test for query string in file name
- fix local files query string `ENOENT` exception
- fixed tests for bumped deps
- Bump dependencies
- AppVeyor file tweaks
- Actually Emit Critical Error in Stream
- cleanup
- Switched to postcss-image-inliner
- bump inline-critical
- AppVeyor tweaks
- cleanup
- added gc to address #82
- Added CLI remote test
- some cleanup
- fixed phantom on missing file extension
- use loadCSS 0.1.8
- allow remote resources
- Bump dependencies

# v0.6.0 / 2015-07-07

- added testcase for #88
- testcase for bc53420 issue
- Fixed issue from bc53420
- Update README.md
- backwards compatibility
- drop Node.js 0.10
- simplify CLI help creation
- minor style tweaks
- Merged master
- Fixed tests & locked clean-css version
- Bump filter-css
- Fixed CLI tests
- minor package.json tweaks
- Bump devDependencies
- Correct expectation for adaptive
- Updated tests for new clean-css 3.2.7
- some cleanup
- Bump dependencies
- Update README.md
- Don't encode entities
- Removed parallel testcase
- Add 'ignore' option
- Deprecated some things
- deprecated htmltarget & styletarget for CLI and introduced --inline
- Added pathPrefix support for CLI
- normalize newlines
- added test for pathPrefix option
- allows pathPrefix to be set through options. Updates README
- Added stream wrapper

# v0.5.7 / 2015-04-12

- AppVeyor tweaks
- Automated Windows tests using AppVeyor
- Fixed tests on Windows
- Added some badges
- Bump dependencies
- cleancss syntax change
- modified tests to use new cleancss output

# v0.5.6 / 2015-03-16

- catch cancellation
- Fix callbacks on error

# v0.5.5 / 2015-03-03

- Fixed CLI error codes
- renaming
- Added jshint
- Added tests for #63 & #64
- Bump dependencies
- up dimensions used in tests, update expected result files
- fix typo
- up dimensions used for generate in index.js
- up dimensions used in README examples
- Fix multi test
- bump dependency
- fix #67
- Add support for multi-dimension critical css.
- improve file structure
- readme tweaks
- fix .gitignore
- codestyle
- Bump dependencies
- updated tests for penthouse 0.3.0

# v0.5.4 / 2015-02-09

- Update .travis.yml
- Use `os.tmpdir()` folder for temporary css
- add `preferGlobal` prop to package.json

# v0.5.3 / 2015-01-18

- Bump dependencies

# v0.5.2 / 2015-01-12

- #56 Locked penthouse version

# v0.5.1 / 2014-12-28

- Fixed tests
- inline-critical version bump
- Fixed CLI Tests for Windows
- Added tests and additional CLI fixes for #52
- Fix for #52

# v0.5.0 / 2014-11-28

- inline critical version bump
- Increased mocha timeout
- Fixed newline character in css to address #14
- Updated version of inline-critical to address #14
- Added bin/critical to files #49
- added CLI / changed structure
- Update README.md
- Remove inlined CSS rules from source stylesheets #39
- Fixed backslash in rebased paths on Windows
- fixed fa77c44
- Return critical css even if unlinking of the temporary file fails
- Ignores external stylesheets

# v0.4.0 / 2014-10-04

- Add build tasks
- Update UUID dep
- Changed inlineImages default to false
- Fixed tests for #35

# v0.3.1 / 2014-09-16

- Fixed parallel calls mentioned in #34

# v0.3.0 / 2014-09-09

- Update fixtures to account for dep. bump
- Bump dependencies

# v0.2.0 / 2014-08-30

- fixed implementation in #30
- Skipped max size for inlined images
- Added image inlining to generate
- removed dynamic test file
- Adds a maxImageFileSize for inlined images and rebases relative css resource paths

# v0.1.6 / 2014-07-30

- Update to Penthouse 0.2.5 to addr raised issues
- change penthouse test to critical css test
- some code formatting
- Fixed tests
- fixed fixtures
- changed test size to only include header nav
- prevent catching callback test errors
- Format code
- Make CSS files/path configurable
- CSS Images fix
- Add more demo projects.
- Add demo projects.
- Move viewport settings up.
- Improve formatting of first example.

# v0.1.5 / 2014-07-16

- Improve the Critical / Penthouse section
- Readme corrections
- Add contributing guide
- Readme revisions
- Add mention of criticalCSS module.
- More edits
- Infra revisions
- Add note about unit tests.
- Add better comments to inline-styles.
- Tweaks to readme.
- Minor revisions.

# v0.1.4 / 2014-07-11

- Add note about sample project
- Strap update
- improve tests
- Tweak to readme.
- Update README.md
- fix all the things
- Attempt to fix builds
- README.md: break long lines.
- Lint fixes.
- Whitespace normalization
- package.json: Add missing properties.

# v0.1.3 / 2014-07-04

- Add support for generateInline

# v0.1.2 / 2014-07-04

- Address path issues post-integration testing

# v0.1.1 / 2014-07-04

- Add missing file to package
- Update to latest Oust, API
- Add syntax highlighting to code blocks

# v0.1.0 / 2014-06-30

- Consistency of example order
- Add minification for inline styles
- Fix some style, cb issues
- Revisions for minification
- Add support for minification
- Add options to readme
- Fixes #9 - adds defaults for w/h
- Add note about FAQs, license
- Expand on joined paths
- Move reads
- Improve test descriptions
- Improve callbacks, add more tests
- Fixes #2, passes errors
- Path joins for #6, test > fixture for #10, other fixes
- Fixes #4 - drop log statements
- Fixes #5 - switch to readFile/writeFile only
- Fixes #7 - throw if src/base not specified
- Should fix #1 - only write to disk if dest specified
- Switch to integers
- Readme revisions

# v0.0.1 / 2014-06-28

- API revisions, readme updates, cleanup
- Various fixes
- Add implementation.
- Add tests.
- Add testing rig.
- Add README.
- Initial package.
- Initial commit


================================================
FILE: CONTRIBUTING.md
================================================
Critical is an open source project. It is licensed using the
[Apache Software License 2.0](http://www.apache.org/licenses/LICENSE-2.0.html).
We really appreciate pull requests and bug reports, here are our guidelines:

1. If filing a bug report, please verify the issue is with Critical first. A good sanity check is:
does the issue have to do with styles not being correctly captured? If so, test with [Penthouse](https://github.com/pocketjoso/penthouse).
If it works with Penthouse then it's a Critical bug and we encourage you to open up a [new ticket](https://github.com/addyosmani/critical/issues/new) with details.
Does the bug have to do with inlining styles, general module failures or installation issues? Those are also
possibly Critical bugs and we will strive to take a look at them.
1. Working on a patch? File a bug at https://github.com/addyosmani/critical/issues (if there
isn’t one already). If your patch is going to be large it might be a good idea
to get the discussion started early. We are happy to discuss it in a new issue beforehand.
1. Make sure that patches provide justification for why they should be merged.


================================================
FILE: Dockerfile
================================================
FROM node:20-slim

ARG CRITICAL_VERSION=5.0.4

ARG PACKAGES="\
  libx11-6\
  libx11-xcb1\
  libxcomposite1\
  libxcursor1\
  libxdamage1\
  libxext6\
  libxi6\
  libxtst6\
  libglib2.0-0\
  libnss3\
  libcups2\
  libxss1\
  libexpat1\
  libxrandr2\
  libasound2\
  libatk1.0-0\
  libatk-bridge2.0-0\
  libpangocairo-1.0-0\
  libgtk-3-0\
  "
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
# hadolint ignore=DL3008
RUN --mount=type=cache,id=build-apt-cache,sharing=locked,target=/var/cache/apt \
    --mount=type=cache,id=build-apt-lib,sharing=locked,target=/var/lib/apt \
    apt-get update -qq \
    && apt-get install --no-install-recommends -y ${PACKAGES} \
    && rm -rf /var/lib/apt/lists /var/cache/apt/archives

RUN --mount=type=cache,id=build-npm-cache,sharing=locked,target=/root/.npm \
  npm install -g critical@${CRITICAL_VERSION}

WORKDIR /site

CMD ["critical", "--help"]


================================================
FILE: README.md
================================================
[![NPM version][npm-image]][npm-url] [![Build Status][ci-image]][ci-url] [![Coverage][coveralls-image]][coveralls-url]

# critical

Critical extracts & inlines critical-path (above-the-fold) CSS from HTML

<img src="https://raw.githubusercontent.com/addyosmani/critical/master/preview.png" alt="Preview" width="378">

## Install

```sh
npm i -D critical
```

## Build plugins

- [grunt-critical](https://github.com/bezoerb/grunt-critical)
- Gulp users should use Critical directly
- For Webpack use [html-critical-webpack-plugin](https://github.com/anthonygore/html-critical-webpack-plugin)

## Demo projects

- [Optimize a basic page with Gulp](https://github.com/addyosmani/critical-path-css-demo) with a [tutorial](https://github.com/addyosmani/critical-path-css-demo#tutorial)
- [Optimize an Angular boilerplate with Gulp](https://github.com/addyosmani/critical-path-angular-demo)
- [Optimize a Weather app with Gulp](https://github.com/addyosmani/critical-css-weather-app)

## Usage

Include:

```js
import {generate} from 'critical';
```

Full blown example with available options:

```js
generate({
  // Inline the generated critical-path CSS
  // - true generates HTML
  // - false generates CSS
  inline: true,

  // Your base directory
  base: 'dist/',

  // HTML source
  html: '<html>...</html>',

  // HTML source file
  src: 'index.html',

  // Your CSS Files (optional)
  css: ['dist/styles/main.css'],

  // Viewport width
  width: 1300,

  // Viewport height
  height: 900,

  // Output results to file
  target: {
    css: 'critical.css',
    html: 'index-critical.html',
    uncritical: 'uncritical.css',
  },

  // Extract inlined styles from referenced stylesheets
  extract: true,

  // ignore CSS rules
  ignore: {
    atrule: ['@font-face'],
    rule: [/some-regexp/],
    decl: (node, value) => /big-image\.png/.test(value),
  },
});
```

### Generate and inline critical-path CSS

Basic usage:

```js
generate({
  inline: true,
  base: 'test/',
  src: 'index.html',
  target: 'index-critical.html',
  width: 1300,
  height: 900,
});
```

### Generate critical-path CSS

Basic usage:

```js
generate({
  base: 'test/',
  src: 'index.html',
  target: 'styles/main.css',
  width: 1300,
  height: 900,
});
```

Generate and minify critical-path CSS:

```js
generate({
  base: 'test/',
  src: 'index.html',
  target: 'styles/styles.min.css',
  width: 1300,
  height: 900,
});
```

Generate, minify and inline critical-path CSS:

```js
generate({
  inline: true,
  base: 'test/',
  src: 'index.html',
  target: {
    html: 'index-critical.html',
    css: 'critical.css',
  },
  width: 1300,
  height: 900,
});
```

Generate and return output via callback:

```js
generate({
    base: 'test/',
    src: 'index.html',
    width: 1300,
    height: 900,
    inline: true
}, (err, {css, html, uncritical}) => {
    // You now have critical-path CSS as well as the modified HTML.
    // Works with and without target specified.
    ...
});
```

Generate and return output via promise:

```js
generate({
    base: 'test/',
    src: 'index.html',
    width: 1300,
    height: 900
}).then(({ css, html, uncritical }) => {
    // You now have critical-path CSS as well as the modified HTML.
    // Works with and without target specified.
}).catch(err => {
    // …
});
```

Generate and return output via async function:

```js
const {css, html, uncritical} = await generate({
  base: 'test/',
  src: 'index.html',
  width: 1300,
  height: 900,
});
```

### Generate critical-path CSS with multiple resolutions

When your site is adaptive and you want to deliver critical CSS for multiple screen resolutions this is a useful option.
_note:_ (your final output will be minified as to eliminate duplicate rule inclusion)

```js
generate({
  base: 'test/',
  src: 'index.html',
  target: {
    css: 'styles/main.css',
  },
  dimensions: [
    {
      height: 200,
      width: 500,
    },
    {
      height: 900,
      width: 1200,
    },
  ],
});
```

### Generate critical-path CSS and ignore specific selectors

This is a useful option when you e.g. want to defer loading of webfonts or background images.

```js
generate({
  base: 'test/',
  src: 'index.html',
  target: {
    css: 'styles/main.css',
  },
  ignore: {
    atrule: ['@font-face'],
    decl: (node, value) => /url\(/.test(value),
  },
});
```

### Generate critical-path CSS and specify asset rebase behaviour

```js
generate({
  base: 'test/',
  src: 'index.html',
  target: {
    css: 'styles/main.css',
  },
  rebase: {
    from: '/styles/main.css',
    to: '/folder/subfolder/index.html',
  },
});
```

```js
generate({
  base: 'test/',
  src: 'index.html',
  target: {
    css: 'styles/main.css',
  },
  rebase: (asset) => `https://my-cdn.com${asset.absolutePath}`,
});
```

### Options

| Name                | Type                   | Default                                                                                                                                                                                | Description                                                                                                                                                                                                                                                                                                                                                                     |
| ------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| inline              | `boolean`\|`object`    | `false`                                                                                                                                                                                | Inline critical-path CSS using filamentgroup's loadCSS. Pass an object to configure [`inline-critical`](https://github.com/bezoerb/inline-critical#inlinehtml-styles-options)                                                                                                                                                                                                   |
| base                | `string`               | `path.dirname(src)` or `process.cwd()`                                                                                                                                                 | Base directory in which the source and destination are to be written                                                                                                                                                                                                                                                                                                            |
| html                | `string`               |                                                                                                                                                                                        | HTML source to be operated against. This option takes precedence over the `src` option.                                                                                                                                                                                                                                                                                         |
| css                 | `array`                | `[]`                                                                                                                                                                                   | An array of paths to css files, file globs, [Vinyl](https://www.npmjs.com/package/vinyl) file objects or source CSS strings.                                                                                                                                                                                                                                                                        |
| src                 | `string`               |                                                                                                                                                                                        | Location of the HTML source to be operated against                                                                                                                                                                                                                                                                                                                              |
| target              | `string` or `object`   |                                                                                                                                                                                        | Location of where to save the output of an operation. Use an object with 'html' and 'css' props if you want to store both                                                                                                                                                                                                                                                       |
| width               | `integer`              | `1300`                                                                                                                                                                                 | Width of the target viewport                                                                                                                                                                                                                                                                                                                                                    |
| height              | `integer`              | `900`                                                                                                                                                                                  | Height of the target viewport                                                                                                                                                                                                                                                                                                                                                   |
| dimensions          | `array`                | `[]`                                                                                                                                                                                   | An array of objects containing height and width. Takes precedence over `width` and `height` if set                                                                                                                                                                                                                                                                              |
| extract             | `boolean`              | `false`                                                                                                                                                                                | Remove the inlined styles from any stylesheets referenced in the HTML. It generates new references based on extracted content so it's safe to use for multiple HTML files referencing the same stylesheet. Use with caution. Removing the critical CSS per page results in a unique async loaded CSS file for every page. Meaning you can't rely on cache across multiple pages |
| inlineImages        | `boolean`              | `false`                                                                                                                                                                                | Inline images                                                                                                                                                                                                                                                                                                                                                                   |
| assetPaths          | `array`                | `[]`                                                                                                                                                                                   | List of directories/urls where the inliner should start looking for assets                                                                                                                                                                                                                                                                                                      |
| maxImageFileSize    | `integer`              | `10240`                                                                                                                                                                                | Sets a max file size (in bytes) for base64 inlined images                                                                                                                                                                                                                                                                                                                       |
| rebase              | `object` or `function` | `undefined`                                                                                                                                                                            | Critical tries it's best to rebase the asset paths relative to the document. If this doesn't work as expected you can always use this option to control the rebase paths. See [`postcss-url`](https://github.com/postcss/postcss-url) for details. (https://github.com/pocketjoso/penthouse#usage-1).                                                                           |
| ignore              | `array` or `object`    | `undefined`                                                                                                                                                                            | Ignore CSS rules. See [`postcss-discard`](https://github.com/bezoerb/postcss-discard) for usage examples. If you pass an array all rules will be applied to atrules, rules and declarations;                                                                                                                                                                                    |
| ignoreInlinedStyles | `boolean`              | `false`                                                                                                                                                                                | Ignore inlined stylesheets                                                                                                                                                                                                                                                                                                                                                      |
| userAgent           | `string`               | `''`                                                                                                                                                                                   | User agent to use when fetching a remote src                                                                                                                                                                                                                                                                                                                                    |
| penthouse           | `object`               | `{}`                                                                                                                                                                                   | Configuration options for [`penthouse`](https://github.com/pocketjoso/penthouse).                                                                                                                                                                                                                                                                                               |
| request             | `object`               | `{}`                                                                                                                                                                                   | Configuration options for [`got`](https://github.com/sindresorhus/got).                                                                                                                                                                                                                                                                                                         |
| cleanCSS            | `object`               | `{level: {  1: { all: true }, 2: { all: false, removeDuplicateFontRules: true, removeDuplicateMediaBlocks: true, removeDuplicateRules: true, removeEmpty: true, mergeMedia: true } }}` | Configuration options for [`CleanCSS`](https://github.com/clean-css/clean-css) which let's you configure the optimization level for the generated critical css                                                                                                                                                                                                                  |
| user                | `string`               | `undefined`                                                                                                                                                                            | RFC2617 basic authorization: user                                                                                                                                                                                                                                                                                                                                               |
| pass                | `string`               | `undefined`                                                                                                                                                                            | RFC2617 basic authorization: pass                                                                                                                                                                                                                                                                                                                                               |
| strict              | `boolean`              | `false`                                                                                                                                                                                | Throw an error on css parsing errors or if no css is found.                                                                                                                                                                                                                                                                                                                     |

## CLI

```sh
npm install -g critical
```

critical works well with standard input.

```sh
cat test/fixture/index.html | critical --base test/fixture --inline > index.critical.html
```

Or on Windows:

```bat
type test\fixture\index.html | critical --base test/fixture --inline > index.critical.html
```

You can also pass in the critical CSS file as an option.

```sh
critical test/fixture/index.html --base test/fixture > critical.css
```

## Gulp

```js
import gulp from 'gulp';
import log from 'fancy-log';
import {stream as critical} from 'critical';

// Generate & Inline Critical-path CSS
gulp.task('critical', () => {
  return gulp
    .src('dist/*.html')
    .pipe(
      critical({
        base: 'dist/',
        inline: true,
        css: ['dist/styles/components.css', 'dist/styles/main.css'],
      })
    )
    .on('error', (err) => {
      log.error(err.message);
    })
    .pipe(gulp.dest('dist'));
});
```

## Why?

### Why is critical-path CSS important?

> CSS is required to construct the render tree for your pages and JavaScript
> will often block on CSS during initial construction of the page.
> You should ensure that any non-essential CSS is marked as non-critical
> (e.g. print and other media queries), and that the amount of critical CSS
> and the time to deliver it is as small as possible.

### Why should critical-path CSS be inlined?

> For best performance, you may want to consider inlining the critical CSS
> directly into the HTML document. This eliminates additional roundtrips
> in the critical path and if done correctly can be used to deliver a
> “one roundtrip” critical path length where only the HTML is a blocking resource.

## FAQ

### Are there any sample projects available using Critical?

Why, yes!. Take a look at [this](https://github.com/addyosmani/critical-path-css-demo) Gulp project
which demonstrates using Critical to generate and inline critical-path CSS. It also includes a mini-tutorial
that walks through how to use it in a simple webapp.

### When should I just use Penthouse directly?

The main differences between Critical and [Penthouse](https://github.com/pocketjoso/penthouse), a module we
use, are:

- Critical will automatically extract stylesheets from your HTML from which to generate critical-path CSS from,
  whilst other modules generally require you to specify this upfront.
- Critical provides methods for inlining critical-path CSS (a common logical next-step once your CSS is generated)
- Since we tackle both generation and inlining, we're able to abstract away some of the ugly boilerplate otherwise
  involved in tackling these problems separately.

That said, if your site or app has a large number of styles or styles which are being dynamically injected into
the DOM (sometimes common in Angular apps) I recommend using Penthouse directly. It will require you to supply
styles upfront, but this may provide a higher level of accuracy if you find Critical isn't serving your needs.

### What other alternatives to Critical are available?

FilamentGroup maintain a [criticalCSS](https://github.com/filamentgroup/criticalCSS) node module, which
similar to [Penthouse](https://github.com/pocketjoso/penthouse) will find and output the critical-path CSS for
your pages. The PageSpeed Optimization modules for nginx, apache, IIS, ATS, and Open Lightspeed can do all the heavy
lifting automatically when you enable the [prioritize_critical_css](https://developers.google.com/speed/docs/insights/OptimizeCSSDelivery) filter

### Is Critical stable and suitable for production use?

Critical has been used on a number of production sites that have found it stable for everyday use.
That said, we welcome you to try it out on your project and report bugs if you find them.

## Can I contribute?

Of course. We appreciate all of our [contributors](https://github.com/addyosmani/critical/graphs/contributors) and
welcome contributions to improve the project further. If you're uncertain whether an addition should be made, feel
free to open up an issue and we can discuss it.

## Maintainers

This module is brought to you and maintained by the following people:

- Addy Osmani - Creator ([Github](https://github.com/addyosmani) / [Twitter](https://twitter.com/addyosmani))
- Ben Zörb - Primary maintainer ([Github](https://github.com/bezoerb) / [Twitter](https://twitter.com/bezoerb))

## License

[Apache-2.0 © Addy Osmani, Ben Zörb](license)

[npm-url]: https://www.npmjs.com/package/critical
[npm-image]: https://img.shields.io/npm/v/critical.svg
[ci-url]: https://github.com/addyosmani/critical/actions?workflow=Tests
[ci-image]: https://github.com/addyosmani/critical/workflows/Tests/badge.svg
[coveralls-url]: https://coveralls.io/github/addyosmani/critical?branch=master
[coveralls-image]: https://img.shields.io/coveralls/github/addyosmani/critical/master.svg


================================================
FILE: cli.js
================================================
#!/usr/bin/env node
import os from 'node:os';
import process from 'node:process';
import stdin from 'get-stdin';
import groupArgs from 'group-args';
import indentString from 'indent-string';
import {escapeRegExp, isObject, isString, reduce} from 'lodash-es';
import meow from 'meow';
import pico from 'picocolors';
import {validate} from './src/config.js';
import {generate} from './index.js';

const help = `
Usage: critical <input> [<option>]

Options:
  -b, --base              Your base directory
  -c, --css               Your CSS Files (optional)
  -w, --width             Viewport width
  -h, --height            Viewport height
  -i, --inline            Generate the HTML with inlined critical-path CSS
  -e, --extract           Extract inlined styles from referenced stylesheets

  --inlineImages          Inline images
  --dimensions            Pass dimensions e.g. 1300x900
  --ignore                RegExp, @type or selector to ignore
  --ignore-[OPTION]       Pass options to postcss-discard. See https://goo.gl/HGo5YV
  --ignoreInlinedStyles   Ignore inlined stylesheets
  --include               RegExp, @type or selector to include
  --include-[OPTION]      Pass options to inline-critical. See https://goo.gl/w6SHJM
  --assetPaths            Directories/Urls where the inliner should start looking for assets
  --user                  RFC2617 basic authorization user
  --pass                  RFC2617 basic authorization password
  --penthouse-[OPTION]    Pass options to penthouse. See https://goo.gl/PQ5HLL
  --ua, --userAgent       User agent to use when fetching remote src
  --strict                Throw an error on css parsing errors or if no css is found
`;

const meowOpts = {
  importMeta: import.meta,
  flags: {
    base: {
      type: 'string',
      shortFlag: 'b',
    },
    css: {
      type: 'string',
      shortFlag: 'c',
      isMultiple: true,
    },
    width: {
      shortFlag: 'w',
    },
    height: {
      shortFlag: 'h',
    },
    inline: {
      type: 'boolean',
      shortFlag: 'i',
    },
    extract: {
      type: 'boolean',
      shortFlag: 'e',
      default: false,
    },
    inlineImages: {
      type: 'boolean',
    },
    ignoreInlinedStyles: {
      type: 'boolean',
      default: false,
    },
    ignore: {
      type: 'string',
    },
    user: {
      type: 'string',
    },
    strict: {
      type: 'boolean',
      default: false,
    },
    pass: {
      type: 'string',
    },
    userAgent: {
      type: 'string',
      shortFlag: 'ua',
    },
    dimensions: {
      type: 'string',
      isMultiple: true,
    },
  },
};

const cli = meow(help, meowOpts);

const groupKeys = ['ignore', 'inline', 'penthouse', 'target', 'request'];
// Group args for inline-critical and penthouse
const grouped = {
  ...cli.flags,
  ...groupArgs(
    groupKeys,
    {
      delimiter: '-',
    },
    meowOpts
  ),
};

/**
 * Check if key is an alias
 * @param {string} key Key to check
 * @returns {boolean} True for alias
 */
const isAlias = (key) => {
  if (isString(key) && key.length > 1) {
    return false;
  }

  const aliases = Object.keys(meowOpts.flags)
    .filter((k) => meowOpts.flags[k].shortFlag)
    .map((k) => meowOpts.flags[k].shortFlag);

  return aliases.includes(key);
};

/**
 * Check if value is an empty object
 * @param {mixed} val Value to check
 * @returns {boolean} Whether or not this is an empty object
 */
const isEmptyObj = (val) => isObject(val) && Object.keys(val).length === 0;

/**
 * Check if value is transformed to {default: val}
 * @param {mixed} val Value to check
 * @returns {boolean} True if it's been converted to {default: value}
 */
const isGroupArgsDefault = (val) => isObject(val) && Object.keys(val).length === 1 && val.default;

/**
 * Return regex if value is a string like this: '/.../g'
 * @param {mixed} val Value to process
 * @returns {mixed} Mapped values
 */
const mapRegExpStr = (val) => {
  if (isString(val)) {
    const {groups} = val.match(/^\/(?<regex>[^/]+)(?:\/?(?<flags>[igmy]+))?\/$/) || {};
    const {regex, flags} = groups || {};

    return (groups && new RegExp(escapeRegExp(regex), flags)) || val;
  }

  if (Array.isArray(val)) {
    return val.map((v) => mapRegExpStr(v));
  }

  return val;
};

const normalizedFlags = reduce(
  grouped,
  (res, val, key) => {
    // Cleanup groupArgs mess ;)
    if (groupKeys.includes(key)) {
      // An empty object means param without value, just true
      if (isEmptyObj(val)) {
        val = true;
      } else if (isGroupArgsDefault(val)) {
        val = val.default;
      }
    }

    // Cleanup camelized group keys
    if (groupKeys.some((k) => key.includes(k)) && !validate(key, val)) {
      return res;
    }

    if (!isAlias(key)) {
      res[key] = mapRegExpStr(val);
    }

    return res;
  },
  {}
);

function showError(err) {
  process.stderr.write(indentString(pico.red('Error: ') + err.message || err, 3));
  process.stderr.write(os.EOL);
  process.stderr.write(indentString(help, 3));
  process.exit(1);
}

function run(data) {
  const {_: inputs = [], css, ...opts} = {...normalizedFlags};

  // Detect css globbing
  const cssBegin = process.argv.findIndex((el) => ['--css', '-c'].includes(el));
  const cssEnd = process.argv.findIndex((el, index) => index > cssBegin && el.startsWith('-'));
  const cssCheck = cssBegin >= 0 ? process.argv.slice(cssBegin, cssEnd > 0 ? cssEnd : undefined) : [];
  const additionalCss = inputs.filter((file) => cssCheck.includes(file));
  // Just take the first html input as we don't support multiple html sources for
  const [input] = inputs.filter((file) => !additionalCss.includes(file)); // eslint-disable-line unicorn/prefer-array-find

  if (Array.isArray(opts.dimensions)) {
    opts.dimensions = opts.dimensions.reduce(
      (result, data) => [
        ...result,
        ...data.split(',').map((dimension) => {
          const [width, height] = dimension.split('x');
          return {width: Number.parseInt(width, 10), height: Number.parseInt(height, 10)};
        }),
      ],
      []
    );
  }

  if (Array.isArray(css)) {
    opts.css = [...css, ...additionalCss].filter(Boolean);
  } else if (css || additionalCss.length > 0) {
    opts.css = [css, ...additionalCss].filter(Boolean);
  }

  if (data) {
    opts.html = data;
  } else {
    opts.src = input;
  }

  try {
    generate(opts, (error, val) => {
      if (error) {
        showError(error);
      } else if (opts.inline) {
        process.stdout.write(val.html, process.exit);
      } else if (opts.extract) {
        process.stdout.write(val.uncritical, process.exit);
      } else {
        process.stdout.write(val.css, process.exit);
      }
    });
  } catch (error) {
    showError(error);
  }
}

if (cli.input[0]) {
  run();
} else {
  const data = await stdin();
  run(data);
}


================================================
FILE: index.js
================================================
import path from 'node:path';
import {Buffer} from 'node:buffer';
import process from 'node:process';
import through2 from 'through2';
import PluginError from 'plugin-error';
import replaceExtension from 'replace-ext';
import {create} from './src/core.js';
import {outputFileAsync} from './src/file.js';
import {getOptions} from './src/config.js';

/**
 * Critical path CSS generation
 * @param  {object} params Options
 * @param  {function} cb Callback
 * @return {Promise<object>} Result object with html, css & optional extracted original css
 */
export async function generate(params, cb) {
  try {
    const options = await getOptions(params);
    const {target = {}, base = process.cwd()} = options;
    const result = await create(options);
    // Store generated css
    if (target.css) {
      await outputFileAsync(path.resolve(base, target.css), result.css);
    }

    // Store generated html
    if (target.html) {
      await outputFileAsync(path.resolve(base, target.html), result.html);
    }

    // Store extracted css
    if (target.uncritical) {
      await outputFileAsync(path.resolve(base, target.uncritical), result.uncritical);
    }

    if (typeof cb === 'function') {
      cb(null, result);
      return;
    }

    return result;
  } catch (error) {
    if (typeof cb === 'function') {
      cb(error);
      return;
    }

    throw error;
  }
}

/**
 * Streams wrapper for critical
 *
 * @param {object} params Critical options
 * @returns {stream} Gulp stream
 */
export function stream(params) {
  // Return stream
  return through2.obj(function (file, enc, cb) {
    if (file.isNull()) {
      return cb(null, file);
    }

    if (file.isStream()) {
      return this.emit('error', new PluginError('critical', 'Streaming not supported'));
    }

    Promise.resolve()
      .then(() => generate({...params, src: file}))
      .then(({css, html}) => {
        // Rename file if not inlined
        if (params.inline) {
          file.contents = Buffer.from(html);
        } else {
          file.path = replaceExtension(file.path, '.css');
          file.contents = Buffer.from(css);
        }

        cb(null, file);
      })
      .catch((error) => cb(new PluginError('critical', error.message)));
  });
}

generate.stream = stream;


================================================
FILE: license
================================================
                              Apache License
                        Version 2.0, January 2004
                     http://www.apache.org/licenses/

TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

1. Definitions.

   "License" shall mean the terms and conditions for use, reproduction,
   and distribution as defined by Sections 1 through 9 of this document.

   "Licensor" shall mean the copyright owner or entity authorized by
   the copyright owner that is granting the License.

   "Legal Entity" shall mean the union of the acting entity and all
   other entities that control, are controlled by, or are under common
   control with that entity. For the purposes of this definition,
   "control" means (i) the power, direct or indirect, to cause the
   direction or management of such entity, whether by contract or
   otherwise, or (ii) ownership of fifty percent (50%) or more of the
   outstanding shares, or (iii) beneficial ownership of such entity.

   "You" (or "Your") shall mean an individual or Legal Entity
   exercising permissions granted by this License.

   "Source" form shall mean the preferred form for making modifications,
   including but not limited to software source code, documentation
   source, and configuration files.

   "Object" form shall mean any form resulting from mechanical
   transformation or translation of a Source form, including but
   not limited to compiled object code, generated documentation,
   and conversions to other media types.

   "Work" shall mean the work of authorship, whether in Source or
   Object form, made available under the License, as indicated by a
   copyright notice that is included in or attached to the work
   (an example is provided in the Appendix below).

   "Derivative Works" shall mean any work, whether in Source or Object
   form, that is based on (or derived from) the Work and for which the
   editorial revisions, annotations, elaborations, or other modifications
   represent, as a whole, an original work of authorship. For the purposes
   of this License, Derivative Works shall not include works that remain
   separable from, or merely link (or bind by name) to the interfaces of,
   the Work and Derivative Works thereof.

   "Contribution" shall mean any work of authorship, including
   the original version of the Work and any modifications or additions
   to that Work or Derivative Works thereof, that is intentionally
   submitted to Licensor for inclusion in the Work by the copyright owner
   or by an individual or Legal Entity authorized to submit on behalf of
   the copyright owner. For the purposes of this definition, "submitted"
   means any form of electronic, verbal, or written communication sent
   to the Licensor or its representatives, including but not limited to
   communication on electronic mailing lists, source code control systems,
   and issue tracking systems that are managed by, or on behalf of, the
   Licensor for the purpose of discussing and improving the Work, but
   excluding communication that is conspicuously marked or otherwise
   designated in writing by the copyright owner as "Not a Contribution."

   "Contributor" shall mean Licensor and any individual or Legal Entity
   on behalf of whom a Contribution has been received by Licensor and
   subsequently incorporated within the Work.

2. Grant of Copyright License. Subject to the terms and conditions of
   this License, each Contributor hereby grants to You a perpetual,
   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
   copyright license to reproduce, prepare Derivative Works of,
   publicly display, publicly perform, sublicense, and distribute the
   Work and such Derivative Works in Source or Object form.

3. Grant of Patent License. Subject to the terms and conditions of
   this License, each Contributor hereby grants to You a perpetual,
   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
   (except as stated in this section) patent license to make, have made,
   use, offer to sell, sell, import, and otherwise transfer the Work,
   where such license applies only to those patent claims licensable
   by such Contributor that are necessarily infringed by their
   Contribution(s) alone or by combination of their Contribution(s)
   with the Work to which such Contribution(s) was submitted. If You
   institute patent litigation against any entity (including a
   cross-claim or counterclaim in a lawsuit) alleging that the Work
   or a Contribution incorporated within the Work constitutes direct
   or contributory patent infringement, then any patent licenses
   granted to You under this License for that Work shall terminate
   as of the date such litigation is filed.

4. Redistribution. You may reproduce and distribute copies of the
   Work or Derivative Works thereof in any medium, with or without
   modifications, and in Source or Object form, provided that You
   meet the following conditions:

   (a) You must give any other recipients of the Work or
       Derivative Works a copy of this License; and

   (b) You must cause any modified files to carry prominent notices
       stating that You changed the files; and

   (c) You must retain, in the Source form of any Derivative Works
       that You distribute, all copyright, patent, trademark, and
       attribution notices from the Source form of the Work,
       excluding those notices that do not pertain to any part of
       the Derivative Works; and

   (d) If the Work includes a "NOTICE" text file as part of its
       distribution, then any Derivative Works that You distribute must
       include a readable copy of the attribution notices contained
       within such NOTICE file, excluding those notices that do not
       pertain to any part of the Derivative Works, in at least one
       of the following places: within a NOTICE text file distributed
       as part of the Derivative Works; within the Source form or
       documentation, if provided along with the Derivative Works; or,
       within a display generated by the Derivative Works, if and
       wherever such third-party notices normally appear. The contents
       of the NOTICE file are for informational purposes only and
       do not modify the License. You may add Your own attribution
       notices within Derivative Works that You distribute, alongside
       or as an addendum to the NOTICE text from the Work, provided
       that such additional attribution notices cannot be construed
       as modifying the License.

   You may add Your own copyright statement to Your modifications and
   may provide additional or different license terms and conditions
   for use, reproduction, or distribution of Your modifications, or
   for any such Derivative Works as a whole, provided Your use,
   reproduction, and distribution of the Work otherwise complies with
   the conditions stated in this License.

5. Submission of Contributions. Unless You explicitly state otherwise,
   any Contribution intentionally submitted for inclusion in the Work
   by You to the Licensor shall be under the terms and conditions of
   this License, without any additional terms or conditions.
   Notwithstanding the above, nothing herein shall supersede or modify
   the terms of any separate license agreement you may have executed
   with Licensor regarding such Contributions.

6. Trademarks. This License does not grant permission to use the trade
   names, trademarks, service marks, or product names of the Licensor,
   except as required for reasonable and customary use in describing the
   origin of the Work and reproducing the content of the NOTICE file.

7. Disclaimer of Warranty. Unless required by applicable law or
   agreed to in writing, Licensor provides the Work (and each
   Contributor provides its Contributions) on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
   implied, including, without limitation, any warranties or conditions
   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
   PARTICULAR PURPOSE. You are solely responsible for determining the
   appropriateness of using or redistributing the Work and assume any
   risks associated with Your exercise of permissions under this License.

8. Limitation of Liability. In no event and under no legal theory,
   whether in tort (including negligence), contract, or otherwise,
   unless required by applicable law (such as deliberate and grossly
   negligent acts) or agreed to in writing, shall any Contributor be
   liable to You for damages, including any direct, indirect, special,
   incidental, or consequential damages of any character arising as a
   result of this License or out of the use or inability to use the
   Work (including but not limited to damages for loss of goodwill,
   work stoppage, computer failure or malfunction, or any and all
   other commercial damages or losses), even if such Contributor
   has been advised of the possibility of such damages.

9. Accepting Warranty or Additional Liability. While redistributing
   the Work or Derivative Works thereof, You may choose to offer,
   and charge a fee for, acceptance of support, warranty, indemnity,
   or other liability obligations and/or rights consistent with this
   License. However, in accepting such obligations, You may act only
   on Your own behalf and on Your sole responsibility, not on behalf
   of any other Contributor, and only if You agree to indemnify,
   defend, and hold each Contributor harmless for any liability
   incurred by, or claims asserted against, such Contributor by reason
   of your accepting any such warranty or additional liability.

END OF TERMS AND CONDITIONS

APPENDIX: How to apply the Apache License to your work.

   To apply the Apache License to your work, attach the following
   boilerplate notice, with the fields enclosed by brackets "[]"
   replaced with your own identifying information. (Don't include
   the brackets!)  The text should be enclosed in the appropriate
   comment syntax for the file format. We also recommend that a
   file or class name and description of purpose be included on the
   same "printed page" as the copyright notice for easier
   identification within third-party archives.

Copyright Addy Osmani

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.


================================================
FILE: package.json
================================================
{
  "name": "critical",
  "version": "7.2.1",
  "description": "Extract & Inline Critical-path CSS from HTML",
  "author": "Addy Osmani",
  "license": "Apache-2.0",
  "repository": "addyosmani/critical",
  "type": "module",
  "scripts": {
    "jest": "cross-env NODE_OPTIONS=--experimental-vm-modules jest",
    "xo": "xo",
    "test": "npm run xo && npm run jest"
  },
  "files": [
    "cli.js",
    "index.js",
    "src"
  ],
  "exports": {
    ".": "./index.js",
    "./file.js": "./src/file.js",
    "./errors.js": "./src/errors.js"
  },
  "bin": "cli.js",
  "keywords": [
    "critical",
    "path",
    "css",
    "optimization"
  ],
  "engines": {
    "node": ">=18.18"
  },
  "dependencies": {
    "@adobe/css-tools": "^4.4.0",
    "async-traverse-tree": "^1.1.0",
    "clean-css": "^5.3.3",
    "common-tags": "^1.8.2",
    "css-url-parser": "^1.1.4",
    "data-uri-to-buffer": "^6.0.2",
    "debug": "^4.3.7",
    "find-up": "^7.0.0",
    "get-stdin": "^9.0.0",
    "globby": "^14.0.2",
    "got": "^13.0.0",
    "group-args": "^0.1.0",
    "indent-string": "^5.0.0",
    "inline-critical": "^12.1.0",
    "is-glob": "^4.0.3",
    "joi": "^17.13.3",
    "lodash": "^4.17.21",
    "lodash-es": "^4.17.21",
    "make-dir": "^5.0.0",
    "meow": "^13.2.0",
    "oust": "^2.0.4",
    "p-all": "^5.0.0",
    "penthouse": "^2.3.3",
    "picocolors": "^1.1.0",
    "plugin-error": "^2.0.1",
    "postcss": "^8.4.47",
    "postcss-discard": "^2.0.0",
    "postcss-image-inliner": "^7.0.1",
    "postcss-url": "^10.1.3",
    "replace-ext": "^2.0.0",
    "slash": "^5.1.0",
    "tempy": "^3.1.0",
    "through2": "^4.0.2",
    "vinyl": "^3.0.0"
  },
  "devDependencies": {
    "async": "^3.2.6",
    "cross-env": "^7.0.3",
    "finalhandler": "^1.3.1",
    "get-port": "^7.1.0",
    "jest": "^29.7.0",
    "nock": "^13.5.5",
    "normalize-newline": "^4.1.0",
    "serve-static": "^1.15.0",
    "stream-array": "^1.1.2",
    "stream-assert": "^2.0.3",
    "vinyl-source-stream": "^2.0.0",
    "xo": "^0.59.3"
  },
  "xo": {
    "space": 2,
    "prettier": true,
    "rules": {
      "capitalized-comments": "off",
      "promise/prefer-await-to-then": "warn",
      "unicorn/prevent-abbreviations": "off",
      "unicorn/string-content": "off",
      "unicorn/no-reduce": "off",
      "unicorn/no-array-reduce": "off",
      "unicorn/no-array-for-each": "off",
      "unicorn/no-fn-reference-in-iterator": "off"
    },
    "overrides": [
      {
        "files": "test/*.js",
        "envs": [
          "jest"
        ]
      }
    ]
  },
  "prettier": {
    "trailingComma": "es5",
    "singleQuote": true,
    "printWidth": 120,
    "bracketSpacing": false
  },
  "jest": {
    "transform": {},
    "collectCoverage": true
  }
}


================================================
FILE: src/array.js
================================================
export async function mapAsync(array = [], callback = (a) => a) {
  const result = [];
  for (const index of array.keys()) {
    const mapped = await callback(array[index], index, array); /* eslint-disable-line no-await-in-loop */
    result.push(mapped);
  }

  return result;
}

export async function forEachAsync(array = [], callback = () => {}) {
  for (const index of array.keys()) {
    await callback(array[index], index, array); /* eslint-disable-line no-await-in-loop */
  }
}

export async function filterAsync(array = [], filter = (a) => a) {
  const result = [];
  for (const index of array.keys()) {
    const active = await filter(array[index], index, array); /* eslint-disable-line no-await-in-loop */
    if (active) {
      result.push(array[index]);
    }
  }

  return result;
}

export async function reduceAsync(initial, array = [], reducer = (r) => r) {
  for (const index of array.keys()) {
    initial = await reducer(initial, array[index], index); /* eslint-disable-line no-await-in-loop */
  }

  return initial;
}


================================================
FILE: src/config.js
================================================
import process from 'node:process';
import Joi from 'joi';
import debugBase from 'debug';
import {traverse, STOP} from 'async-traverse-tree';
import {ConfigError} from './errors.js';

const debug = debugBase('critical:config');

export const DEFAULT = {
  width: 1300,
  height: 900,
  timeout: 30_000,
  maxImageFileSize: 10_240,
  inline: false,
  strict: false,
  extract: false,
  inlineImages: false,
  ignoreInlinedStyles: false,
  concurrency: Number.POSITIVE_INFINITY,
  include: [],
};

const schema = Joi.object()
  .keys({
    html: Joi.string(),
    src: [Joi.string(), Joi.object()],
    css: [Joi.string(), Joi.array()],
    base: Joi.string(),
    strict: Joi.boolean().default(DEFAULT.strict),
    ignoreInlinedStyles: Joi.boolean().default(DEFAULT.ignoreInlinedStyles),
    extract: Joi.boolean().default(DEFAULT.extract),
    inlineImages: Joi.boolean().default(DEFAULT.inlineImages),
    postcss: Joi.array(),
    ignore: [Joi.array(), Joi.object().unknown(true)],
    width: Joi.number().default(DEFAULT.width),
    height: Joi.number().default(DEFAULT.height),
    dimensions: Joi.array().items({width: Joi.number(), height: Joi.number()}),
    inline: [Joi.boolean().default(DEFAULT.inline), Joi.object().unknown(true)],
    maxImageFileSize: Joi.number().default(DEFAULT.maxImageFileSize),
    include: Joi.any().default(DEFAULT.include),
    concurrency: Joi.number().default(DEFAULT.concurrency),
    user: Joi.string(),
    pass: Joi.string(),
    request: Joi.object().unknown(true),
    penthouse: Joi.object()
      .keys({
        url: Joi.any().forbidden(),
        css: Joi.any().forbidden(),
        width: Joi.any().forbidden(),
        height: Joi.any().forbidden(),
        timeout: Joi.number().default(DEFAULT.timeout),
        forceInclude: Joi.any(),
        maxEmbeddedBase64Length: Joi.number(),
      })
      .unknown(true),
    rebase: [
      Joi.object().keys({
        from: Joi.string(),
        to: Joi.string(),
      }),
      Joi.func(),
      Joi.boolean(),
    ],
    target: [
      Joi.string(),
      Joi.object().keys({
        css: Joi.string(),
        html: Joi.string(),
        uncritical: Joi.string(),
      }),
    ],
    assetPaths: Joi.array().items(Joi.string()),
    userAgent: Joi.string(),
    cleanCSS: Joi.object().unknown(true),
  })
  .label('options')
  .xor('html', 'src');

export async function getOptions(options = {}) {
  const parsedOptions = await traverse(options, (key, value) => {
    if (['css', 'html', 'src'].includes(key)) {
      return STOP;
    }

    if (typeof value === 'string') {
      try {
        return JSON.parse(value);
      } catch {}
    }

    return value;
  });

  const {error, value} = schema.validate(parsedOptions);

  const {inline, dimensions, penthouse = {}, target, ignore} = value || {};

  if (error) {
    const {details = []} = error;
    const [detail = {}] = details;
    const {message = 'invalid options'} = detail;

    throw new ConfigError(message);
  }

  if (!dimensions || dimensions.length === 0) {
    value.dimensions = [
      {
        width: options.width || DEFAULT.width,
        height: options.height || DEFAULT.height,
      },
    ];
  }

  if (typeof target === 'string') {
    const key = /\.css$/.test(target) ? 'css' : 'html';
    value.target = {[key]: target};
  }

  // Set inline options
  value.inline = Boolean(inline) && {
    basePath: value.base || process.cwd(),
    ...(inline === true ? {strategy: 'media'} : inline),
  };

  if (value.inline.replaceStylesheets !== undefined && !Array.isArray(value.inline.replaceStylesheets)) {
    if (value.inline.replaceStylesheets === 'false') {
      value.inline.replaceStylesheets = false;
    } else if (typeof value.inline.replaceStylesheets !== 'function') {
      value.inline.replaceStylesheets = [value.inline.replaceStylesheets];
    }
  }

  // Set penthouse options
  value.penthouse = {
    forceInclude: value.include,
    timeout: DEFAULT.timeout,
    maxEmbeddedBase64Length: value.maxImageFileSize,
    ...penthouse,
  };

  if (ignore && Array.isArray(ignore)) {
    value.ignore = {
      atrule: ignore,
      rule: ignore,
      decl: ignore,
    };
  }

  if (target && target.uncritical) {
    value.extract = true;
  }

  debug(value);

  return value;
}

export const validate = (key, val) => {
  const {error} = schema.validate({[key]: val, html: '<html/>'});
  if (error) {
    return false;
  }

  return true;
};


================================================
FILE: src/core.js
================================================
import {EOL} from 'node:os';
import {Buffer} from 'node:buffer';
import process from 'node:process';
import path from 'node:path';
import pico from 'picocolors';
import CleanCSS from 'clean-css';
import {invokeMap} from 'lodash-es';
import pAll from 'p-all';
import debugBase from 'debug';
import postcss from 'postcss';
import discard from 'postcss-discard';
import imageInliner from 'postcss-image-inliner';
import penthouse from 'penthouse';
import {PAGE_UNLOADED_DURING_EXECUTION_ERROR_MESSAGE} from 'penthouse/lib/core.js';
import {inline as inlineCritical} from 'inline-critical';
import {removeDuplicateStyles} from 'inline-critical/css';
import parseCssUrls from 'css-url-parser';
import {reduceAsync} from './array.js';
import {NoCssError} from './errors.js';
import {getDocument, getDocumentFromSource, token, getAssetPaths, isRemote, normalizePath} from './file.js';

const debug = debugBase('critical:core');

/**
 * Returns a string of combined and deduped css rules.
 * @param {array} cssArray Array with css strings
 * @returns {String} combined and deduped css rules
 */
function combineCss(cssArray) {
  if (cssArray.length === 1) {
    return cssArray[0].toString();
  }

  return new CleanCSS().minify(invokeMap(cssArray, 'toString').join(' ')).styles;
}

/**
 * Let penthouse compute the critical css
 * @param {vinyl} document Vinyl representation of the HTML document
 * @param {object} options Options passed to critical
 * @returns {string} Critical css for various dimensions combined and deduped
 */
function callPenthouse(document, options) {
  const {dimensions, width, height, userAgent, user, pass, penthouse: params = {}} = options;
  const {customPageHeaders = {}} = params;
  const {css: cssString, url} = document;
  const config = {...params, cssString, url};
  // Dimensions need to be sorted from small to wide. Otherwise the order gets corrupted
  const sizes = Array.isArray(dimensions)
    ? [...dimensions].sort((a, b) => (a.width || 0) - (b.width || 0))
    : [{width, height}];

  if (userAgent) {
    config.userAgent = userAgent;
  }

  if (user && pass) {
    config.customPageHeaders = {...customPageHeaders, Authorization: `Basic ${token(user, pass)}`};
  }

  return sizes.map(({width, height}) => () => {
    const result = penthouse({...config, width, height});
    debug('Call penthouse with:', {
      ...config,
      width,
      height,
      cssString: `${(cssString || '').slice(0, 10)} ... ${(cssString || '').slice(-10)}`,
    });

    return result;
  });
}

/**
 * Critical path CSS generation
 * @param  {object} options Options
 * @accepts src, base, width, height, dimensions, dest
 * @return {Promise<object>} Object with critical css & html
 */
export async function create(options = {}) {
  const {
    base,
    src,
    html,
    inline,
    ignore,
    extract,
    target = {},
    inlineImages,
    maxImageFileSize,
    postcss: postProcess = [],
    strict,
    cleanCSS: cleanCSSOptions,
    concurrency = Number.POSITIVE_INFINITY,
    assetPaths = [],
  } = options;

  // Create vinyl representation for the document with normalized filepath and normalized styles
  const document = src ? await getDocument(src, options) : await getDocumentFromSource(html, options);

  if (!document.css || !document.css.toString()) {
    if (strict) {
      throw new NoCssError();
    }

    return {
      css: '',
      html: document.contents.toString(),
    };
  }

  // Generate critical css
  let criticalCSS;
  try {
    const tasks = callPenthouse(document, options);
    const criticalStyles = await pAll(tasks, {concurrency});
    criticalCSS = combineCss(criticalStyles);
  } catch (error) {
    if (error.message === PAGE_UNLOADED_DURING_EXECUTION_ERROR_MESSAGE) {
      process.stderr.write(pico.yellow(PAGE_UNLOADED_DURING_EXECUTION_ERROR_MESSAGE) + EOL);
      return {
        css: '',
        html: document.contents.toString(),
      };
    }

    throw error;
  }

  // Add postprocess configuration
  if (ignore) {
    postProcess.push(discard(ignore));
  }

  if (inlineImages) {
    const refAssets = [...parseCssUrls(criticalCSS), ...document.stylesheets];
    const refAssetPaths = refAssets.reduce((res, file) => [...res, path.dirname(file)], []);

    const searchpaths = await reduceAsync([], [...new Set(refAssetPaths)], async (res, file) => {
      const paths = await getAssetPaths(document, file, options, false);
      return [...new Set([...res, ...paths])];
    });

    const filtered = searchpaths.filter((p) => isRemote(p) || p.includes(process.cwd()) || (base && p.includes(base)));

    const inlineOptions = {
      assetPaths: [...filtered, ...assetPaths],
      maxFileSize: maxImageFileSize,
    };

    debug('Inline images:', inlineOptions, refAssets);

    postProcess.push(imageInliner(inlineOptions));
  }

  // Post-process critical css
  if (postProcess.length > 0) {
    criticalCSS = await postcss(postProcess)
      .process(criticalCSS, {from: undefined})
      .then((contents) => contents.css);
  }

  // Minify or prettify
  const cleanCSS = new CleanCSS(
    cleanCSSOptions || {
      level: {
        1: {
          all: true,
        },
        2: {
          all: false,
          removeDuplicateFontRules: true,
          removeDuplicateMediaBlocks: true,
          removeDuplicateRules: true,
          removeEmpty: true,
          mergeMedia: true,
        },
      },
    }
  );
  criticalCSS = cleanCSS.minify(criticalCSS).styles;

  const result = {
    css: criticalCSS,
  };

  // Define uncritical as lazy evaluated property
  const lazyUncritical = (orig, diff) =>
    function () {
      this._uncritical ||= removeDuplicateStyles(orig, diff);

      return this._uncritical;
    };

  Object.defineProperty(result, 'uncritical', {
    get: lazyUncritical(document.css, criticalCSS),
  });

  // Inline
  if (inline) {
    const {replaceStylesheets} = inline;

    if (typeof replaceStylesheets === 'function') {
      inline.replaceStylesheets = await replaceStylesheets(document, result.uncritical);
    }

    // If replaceStylesheets is not set via option and and uncritical is empty
    if (extract && replaceStylesheets === undefined && result.uncritical.trim() === '') {
      inline.replaceStylesheets = [];
    }

    if (target.uncritical) {
      const uncriticalHref = normalizePath(path.relative(document.cwd, path.resolve(base, target.uncritical)));
      // Only replace stylesheets if the uncriticalHref is inside document.cwd and replaceStylesheets is not set via options
      if (!/^\.\.\//.test(uncriticalHref) && replaceStylesheets === undefined) {
        inline.replaceStylesheets = [`/${uncriticalHref}`];
      }
    } else {
      inline.extract = extract;
    }

    const inlined = inlineCritical(document.contents.toString(), criticalCSS, {...inline, basePath: document.cwd});
    document.contents = Buffer.from(inlined);
  }

  // Clean tempfiles
  await document.cleanup();

  result.html = document.contents.toString();

  // Cleanup output
  return result;
}


================================================
FILE: src/errors.js
================================================
import process from 'node:process';
import pico from 'picocolors';
import {stripIndents, stripIndent} from 'common-tags';

export class FileNotFoundError extends Error {
  constructor(file = '', paths = [], ...params) {
    const message = pico.red(stripIndent`
      Error: File not found: ${file}
             Current working directory: ${process.cwd()}
             Searched in: ${paths.length > 0 ? paths.join(', ') : '-'}
    `);

    super([message, ...params]);

    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, FileNotFoundError);
    }

    this.file = file;
  }
}

export class NoCssError extends Error {
  constructor(...params) {
    const message = pico.red(stripIndents`
      Error: No stylesheets found in document and no css was specified in the options
    `);

    super([message, ...params]);

    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, FileNotFoundError);
    }
  }
}

export class ConfigError extends Error {
  constructor(msg, ...params) {
    const message = pico.red(stripIndents`
      ConfigError: ${msg}
    `);

    super([message, ...params]);

    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, FileNotFoundError);
    }
  }
}


================================================
FILE: src/file.js
================================================
/* eslint-disable complexity */
import {Buffer} from 'node:buffer';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import process from 'node:process';
import url from 'node:url';
import {promisify} from 'node:util';
import parseCssUrls from 'css-url-parser';
import {dataUriToBuffer} from 'data-uri-to-buffer';
import debugBase from 'debug';
import {findUpMultiple} from 'find-up';
import {globby} from 'globby';
import {parse} from '@adobe/css-tools';
import got from 'got';
import isGlob from 'is-glob';
import {makeDirectory} from 'make-dir';
import oust from 'oust';
import pico from 'picocolors';
import postcss from 'postcss';
import postcssUrl from 'postcss-url';
import slash from 'slash';
import {temporaryDirectory, temporaryFile} from 'tempy';
import Vinyl from 'vinyl';
import {filterAsync, forEachAsync, mapAsync, reduceAsync} from './array.js';
import {FileNotFoundError} from './errors.js';

const debug = debugBase('critical:file');

export const BASE_WARNING = `${pico.yellow(
  'Warning:'
)} Missing base path. Consider 'base' option. https://goo.gl/PwvFVb`;

const warn = (text) => process.stderr.write(pico.yellow(`${text}${os.EOL}`));

const unlinkAsync = promisify(fs.unlink);
const readFileAsync = promisify(fs.readFile);
const writeFileAsync = promisify(fs.writeFile);

export const checkCssOption = (css) => Boolean((!Array.isArray(css) && css) || (Array.isArray(css) && css.length > 0));

export async function outputFileAsync(file, data) {
  const dir = path.dirname(file);

  if (!fs.existsSync(dir)) {
    await makeDirectory(dir);
  }

  return writeFileAsync(file, data);
}

/**
 * Fixup slashes in file paths for Windows and remove volume definition in front
 * @param {string} str Path
 * @returns {string} Normalized path
 */
export function normalizePath(str) {
  return process.platform === 'win32' ? slash(str.replace(/^[a-zA-Z]:/, '')) : str;
}

/**
 * Check whether a resource is external or not
 * @param {string} href Path
 * @returns {boolean} True if the path is remote
 */
export function isRemote(href) {
  return typeof href === 'string' && /(^\/\/)|(:\/\/)/.test(href) && !href.startsWith('file:');
}

/**
 * Parse Url
 * @param {string} str The URL
 * @returns {URL|object} return new URL Object
 */
export function urlParse(str = '') {
  if (/^\w+:\/\//.test(str)) {
    return new URL(str);
  }

  if (/^\/\//.test(str)) {
    return new URL(str, 'https://ba.se');
  }

  return {pathname: str};
}

/**
 * Get file uri considering OS
 * @param {string} file Absolute filepath
 * @returns {string} file uri
 */
function getFileUri(file) {
  if (!isAbsolute(file)) {
    throw new Error('Path must be absolute to compute file uri. Received: ' + file);
  }

  const fileUrl = process.platform === 'win32' ? new URL(`file:///${file}`) : new URL(`file://${file}`);

  return fileUrl.href;
}

/**
 * Resolve Url
 * @param {string} from Resolve from
 * @param {string} to Resolve to
 * @returns {string} The resolved url
 */
export function urlResolve(from = '', to = '') {
  if (isRemote(from)) {
    const {href: base} = urlParse(from);
    const {href} = new URL(to, base);
    return href;
  }

  if (isAbsolute(to)) {
    return to;
  }

  return path.join(from.replace(/[^/]+$/, ''), to);
}

function isFilePath(href) {
  return typeof href === 'string' && !isRemote(href);
}

export function isAbsolute(href) {
  return isFilePath(href) && path.isAbsolute(href);
}

/**
 * Check whether a resource is relative or not
 * @param {string} href Path
 * @returns {boolean} True if the path is relative
 */
function isRelative(href) {
  return isFilePath(href) && !isAbsolute(href);
}

/**
 * Wrapper for File.isVinyl to detect vinyl objects generated by gulp (vinyl < v0.5.6)
 * @param {*} file Object to check
 * @returns {boolean} True if it's a valid vinyl object
 */
function isVinyl(file) {
  return (
    Vinyl.isVinyl(file) ||
    file instanceof Vinyl ||
    (file && /function File\(/.test(file.constructor.toString()) && file.contents && file.path)
  );
}

/**
 * Check if a file exists (remote & local)
 * @param {string} href Path
 * @param {object} options Critical options
 * @returns {Promise<boolean>} Resolves to true if the file exists
 */
export async function fileExists(href, options = {}) {
  if (isVinyl(href)) {
    return !href.isNull();
  }

  if (Buffer.isBuffer(href)) {
    return true;
  }

  if (isRemote(href)) {
    const {request = {}} = options;
    const method = request.method || 'head';
    try {
      const response = await fetch(href, {...options, request: {...request, method}});
      const {statusCode} = response;

      if (method === 'head') {
        return Number.parseInt(statusCode, 10) < 400;
      }

      return Boolean(response);
    } catch {
      return false;
    }
  }

  return fs.existsSync(href) || fs.existsSync(href.replace(/\?.*$/, ''));
}

/**
 * Remove temporary files
 * @param {array} files Array of temp files
 * @returns {Promise<void>|*} Promise resolves when all files removed
 */
const getCleanup = (files) => () =>
  forEachAsync(files, (file) => {
    try {
      unlinkAsync(file);
    } catch {
      debug(`${file} was already deleted`);
    }
  });

/**
 * Path join considering urls
 * @param {string} base Base path part
 * @param {string} part Path part to append
 * @returns {string} Joined path/url
 */
export function joinPath(base, part) {
  if (!part) {
    return base;
  }

  if (isRemote(base)) {
    return urlResolve(base, part);
  }

  return path.join(base, part.replace(/\?.*$/, ''));
}

/**
 * Resolve path
 * @param {string} href Path
 * @param {[string]} search Paths to search in
 * @param {object} options Critical options
 * @returns {Promise<string>} Resolves to found path, rejects with FileNotFoundError otherwise
 */
export async function resolve(href, search = [], options = {}) {
  let exists = await fileExists(href, options);
  if (exists) {
    return href;
  }

  for (const ref of search) {
    const checkPath = joinPath(ref, href);
    exists = await fileExists(checkPath, options); /* eslint-disable-line no-await-in-loop */
    if (exists) {
      return checkPath;
    }
  }

  throw new FileNotFoundError(href, search);
}

/**
 * Glob pattern
 * @param {array|string} pattern Glob pattern
 * @param {string} base Critical base option
 * @returns {Promise<[string]>} Found files
 */
function glob(pattern, {base} = {}) {
  // Evaluate globs based on base path
  const patterns = Array.isArray(pattern) ? pattern : [pattern];
  // Prepend base if it's not empty & not remote
  const prependBase = (pattern) => (base && !isRemote(base) ? [path.join(base, pattern)] : []);

  return reduceAsync([], patterns, async (files, pattern) => {
    if (isGlob(pattern)) {
      const result = await globby([...prependBase(pattern), pattern]);
      return [...files, ...result];
    }

    return [...files, pattern];
  });
}

/**
 * Rebase image url in css
 *
 * @param {Buffer|string} css Stylesheet
 * @param {string} from Rebase from url
 * @param {string} to Rebase to url
 * @param {opject} options
 *    method: {string|function} method Rebase method. See https://github.com/postcss/postcss-url#options-combinations
 *    strict: fail on invalid css
 *    inlined: boolean flag indicating inlined css
 * @param {boolean} strict fail on invalid css
 * @returns {Buffer} Rebased css
 */
async function rebaseAssets(css, from, to, options = {}) {
  const {method = 'rebase', strict = false, inlined = false} = options;
  let rebased = css.toString();

  debug('Rebase assets', {from, to});

  if (/\/$/.test(to)) {
    to += 'temp.html';
  }

  if (/\/$/.test(from)) {
    from += 'temp.css';
  }

  if (isRemote(from)) {
    const {pathname} = urlParse(from);
    from = pathname;
  }

  try {
    if (typeof method === 'function') {
      const transform = (asset, ...rest) => {
        const assetNormalized = {
          ...asset,
          absolutePath: normalizePath(asset.absolutePath),
          relativePath: normalizePath(asset.relativePath),
        };

        return method(assetNormalized, ...rest);
      };

      const result = await postcss()
        .use(postcssUrl({url: transform}))
        .process(css, {from, to});
      rebased = result.css;
    } else if (from && to) {
      const result = await postcss()
        .use(postcssUrl({url: method}))
        .process(css, {from, to});
      rebased = result.css;
    }
  } catch (error) {
    if (strict) {
      if (inlined) {
        error.message = error.message.replace(from, 'Inlined stylesheet');
      }

      throw error;
    }

    debug(`CSS parse error: ${error.message}`);
    rebased = '';
  }

  return Buffer.from(rebased);
}

/**
 * Token generated by concatenating username and password with `:` character within a base64 encoded string.
 * @param  {String} user User identifier.
 * @param  {String} pass Password.
 * @returns {String} Base64 encoded authentication token.
 */
export const token = (user, pass) => Buffer.from([user, pass].join(':')).toString('base64');

/**
 * Get external resource. Try https and falls back to http
 * @param {string} uri Source uri
 * @param {object} options Options passed to critical
 * @param {boolean} secure Use https?
 * @returns {Promise<Buffer|response>} Resolves to fetched content or response object for HEAD request
 */
async function fetch(uri, options = {}, secure = true) {
  const {user, pass, userAgent, request: requestOptions = {}} = options;
  const {headers = {}, method = 'get', https} = requestOptions;
  let resourceUrl = uri;
  let protocolRelative = false;

  // Consider protocol-relative urls
  if (/^\/\//.test(uri)) {
    protocolRelative = true;
    resourceUrl = urlResolve(`http${secure ? 's' : ''}://te.st`, uri);
  }

  requestOptions.https = {rejectUnauthorized: true, ...https};
  if (user && pass) {
    headers.Authorization = `Basic ${token(user, pass)}`;
  }

  if (userAgent) {
    headers['User-Agent'] = userAgent;
  }

  debug(`Fetching resource: ${resourceUrl}`, {...requestOptions, headers});

  try {
    const response = await got(resourceUrl, {...requestOptions, headers});
    if (method === 'head') {
      return response;
    }

    return Buffer.from(response.body || '');
  } catch (error) {
    // Try again with http
    if (secure && protocolRelative) {
      debug(`${error.message} - trying again over http`);
      return fetch(uri, options, false);
    }

    debug(`${resourceUrl} failed: ${error.message}`);

    if (method === 'head') {
      return error.response;
    }

    if (error.response) {
      return Buffer.from(error.response.body || '');
    }

    throw error;
  }
}

/**
 * Extract stylesheet urls from html document
 * @param {Vinyl} file Vinyl file object (document)
 * @param {object} options Options passed to critical
 * @returns {[string]} Stylesheet urls from document source
 */
function getStylesheetObjects(file, options) {
  const {ignoreInlinedStyles} = options || {};
  if (!isVinyl(file)) {
    throw new Error('Parameter file needs to be a vinyl object');
  }

  // Already computed stylesheetObjects
  if (file.stylesheetObjects) {
    return file.stylesheetObjects;
  }

  const stylesheets = oust.raw(file.contents.toString(), ['stylesheets', 'preload', 'styles']);

  const isNotPrint = (el) =>
    el.attr('media') !== 'print' || (Boolean(el.attr('onload')) && el.attr('onload').includes('media'));

  const isMediaQuery = (media) => typeof media === 'string' && !['all', 'print', 'screen'].includes(media);

  const allowedInlinedStylesheet = (type) => type !== 'styles' || !ignoreInlinedStyles;

  const objects = stylesheets
    .filter((link) => isNotPrint(link.$el) && Boolean(link.value) && allowedInlinedStylesheet(link.type))
    .map((link) => {
      const media = isMediaQuery(link.$el.attr('media')) ? link.$el.attr('media') : '';

      // support base64 encoded styles
      if (link.value.startsWith('data:')) {
        const parsed = dataUriToBuffer(link.value);
        return {
          media,
          value: Buffer.from(parsed.buffer),
        };
      }

      if (link.type === 'styles') {
        return {
          media,
          value: Buffer.from(link.value),
        };
      }

      return {
        media,
        value: link.value,
      };
    });

  const isEqual = (a, b) => Buffer.from(a).compare(Buffer.from(b)) === 0;
  const compare = (a, b) => isEqual(a.media, b.media) && isEqual(a.value, b.value);
  // Make objects unique
  const stylesheetObjects = objects.filter((a, index, array) => {
    return array.findIndex((b) => compare(a, b)) === index;
  });

  // cache them for later use
  file.stylesheetObjects = stylesheetObjects;

  return stylesheetObjects;
}

/**
 * Extract stylesheet urls from html document
 * @param {Vinyl} file Vinyl file object (document)
 * @param {object} options Options passed to critical
 * @returns {[string]} Stylesheet urls from document source
 */
export function getStylesheetHrefs(file, options) {
  return getStylesheetObjects(file, options).map((object) => object.value);
}

/**
 * Extract stylesheet urls from html document
 * @param {Vinyl} file Vinyl file object (document)
 * @param {object} options Options passed to critical
 * @returns {[string]} Stylesheet urls from document source
 */
export function getStylesheetsMedia(file, options) {
  return getStylesheetObjects(file, options).map((object) => object.media);
}

/**
 * Extract asset urls from stylesheet
 * @param {Vinyl} file Vinyl file object (stylesheet)
 * @returns {[string]} Asset urls from stylesheet source
 */
export function getAssets(file) {
  if (!isVinyl(file)) {
    throw new Error('Parameter file needs to be a vinyl object');
  }

  return parseCssUrls(file.contents.toString());
}

/**
 * Compute Path to Html document based on docroot
 * @param {Vinyl} file The file we want to check
 * @param {object} options Critical options object
 * @returns {Promise<string>} Computed path
 */
export async function getDocumentPath(file, options = {}) {
  let {base} = options;

  // Check remote
  if (file.remote) {
    let {pathname} = file.urlObj;
    if (/\/$/.test(pathname)) {
      pathname += 'index.html';
    }

    return pathname;
  }

  // If we don't have a file path and
  if (!file.path) {
    return '';
  }

  if (base) {
    base = path.resolve(base);
    return normalizePath(`/${path.relative(base, file.path || base)}`);
  }

  // Check local and assume base path based on relative stylesheets
  if (file.stylesheets) {
    const relativeRefs = file.stylesheets.filter((href) => isRelative(href));
    const absoluteRefs = file.stylesheets.filter((href) => isAbsolute(href));

    // If we have no stylesheets inside, fall back to path relative to process cwd
    if (relativeRefs.length === 0 && absoluteRefs.length === 0) {
      process.stderr.write(BASE_WARNING);

      return normalizePath(`/${path.relative(process.cwd(), file.path)}`);
    }

    // Compute base path based on absolute links
    if (relativeRefs.length === 0) {
      const [ref] = absoluteRefs;
      const paths = await getAssetPaths(file, ref, options);
      try {
        const filepath = await resolve(ref, paths, options);
        return normalizePath(`/${path.relative(normalizePath(filepath).replace(ref, ''), file.path)}`);
      } catch {
        process.stderr.write(BASE_WARNING);

        return normalizePath(`/${path.relative(process.cwd(), file.path)}`);
      }
    }

    // Compute path based on relative stylesheet links
    const dots = relativeRefs.reduce((res, href) => {
      const match = /^(\.\.\/)+/.exec(href);

      return match && match[0].length > res.length ? match[0] : res;
    }, './');

    const tmpBase = path.resolve(path.dirname(file.path), dots);

    return normalizePath(`/${path.relative(tmpBase, file.path)}`);
  }

  return '';
}

/**
 * Get path for remote stylesheet. Compares document host with stylesheet host
 * @param {object} fileObj Result of urlParse(style url)
 * @param {object} documentObj Result of urlParse(document url)
 * @param {string} filename Filename
 * @returns {string} Path to css (can be remote or local relative to document base)
 */
function getRemoteStylesheetPath(fileObj, documentObj, filename) {
  let {hostname: styleHost, port: stylePort, pathname} = fileObj;
  const {hostname: docHost, port: docPort} = documentObj || {};

  if (filename) {
    pathname = joinPath(path.dirname(pathname), path.basename(filename));
    fileObj.pathname = normalizePath(pathname);
  }

  if (`${styleHost}:${stylePort}` === `${docHost}:${docPort}`) {
    return pathname;
  }

  return url.format(fileObj);
}

/**
 * Get path to stylesheet based on docroot
 * @param {Vinyl} document Optional reference document
 * @param {Vinyl} file the file we want to check
 * @param {object} options Critical options object
 * @returns {Promise<string>} Computed path
 */
export function getStylesheetPath(document, file, options = {}) {
  let {base} = options;

  // Check inline styles
  if (file.inline) {
    return normalizePath(`${document.virtualPath}.css`);
  }

  // Check remote
  if (file.remote) {
    return getRemoteStylesheetPath(file.urlObj, document.urlObj);
  }

  // Generate path relative to document if stylesheet is referenced relative
  //
  if (isRelative(file.path) && document.virtualPath) {
    return normalizePath(joinPath(path.dirname(document.virtualPath), file.path));
  }

  if (base && path.resolve(file.path).includes(path.resolve(base))) {
    base = path.resolve(base);
    return normalizePath(`/${path.relative(path.resolve(base), path.resolve(file.path))}`);
  }

  // Try to compute path based on document link tags with same name
  const stylesheet = document.stylesheets
    .filter((href) => !Buffer.isBuffer(href))
    .find((href) => {
      const {pathname} = urlParse(href);
      const name = path.basename(pathname);
      return name === path.basename(file.path);
    });

  if (stylesheet && isRelative(stylesheet) && document.virtualPath) {
    return normalizePath(joinPath(path.dirname(document.virtualPath), stylesheet));
  }

  if (stylesheet && isRemote(stylesheet)) {
    return getRemoteStylesheetPath(urlParse(stylesheet), document.urlObj);
  }

  if (stylesheet) {
    return stylesheet;
  }

  // Try to find stylesheet path based on document link tags
  const [unsafestylesheet] = document.stylesheets
    .filter((href) => !Buffer.isBuffer(href))
    .sort((a) => (isRemote(a) ? 1 : -1));
  if (unsafestylesheet && isRelative(unsafestylesheet) && document.virtualPath) {
    return normalizePath(
      joinPath(path.dirname(document.virtualPath), joinPath(path.dirname(unsafestylesheet), path.basename(file.path)))
    );
  }

  if (unsafestylesheet && isRemote(unsafestylesheet)) {
    return getRemoteStylesheetPath(urlParse(unsafestylesheet), document.urlObj, path.basename(file.path));
  }

  if (stylesheet) {
    return stylesheet;
  }

  process.stderr.write(BASE_WARNING);
  if (document.virtualPath && file.path) {
    return normalizePath(joinPath(path.dirname(document.virtualPath), path.basename(file.path)));
  }

  return '';
}

/**
 * Get a list of possible asset paths
 * Guess this is rather expensive so this method should only be used if
 * there's no other possible way
 *
 * @param {Vinyl} document Html document
 * @param {string} file File path
 * @param {object} options Critical options
 * @param {boolean} strict Check for file existence
 * @returns {Promise<[string]>} List of asset paths
 */
export async function getAssetPaths(document, file, options = {}, strict = true) {
  const {base, rebase = {}, assetPaths = []} = options;
  const {history = [], url: docurl = '', urlObj} = document;
  const {from, to} = rebase;
  const {pathname: urlPath} = urlObj || {};
  const [docpath] = history;

  if (isVinyl(file)) {
    return [];
  }

  // consider base tag in document
  const baseTagHref = document?.contents?.toString()?.match(/<base\s+href=['"]([^'"]+)['"]/)?.[1];
  // Remove double dots in the middle
  const normalized = path.join(file);
  // Count directory hops
  const hops = normalized.split(path.sep).reduce((cnt, part) => (part === '..' ? cnt + 1 : cnt), 0);
  // Also findup first real dir path
  const [first] = normalized.split(path.sep).filter((p) => p && p !== '..'); // eslint-disable-line unicorn/prefer-array-find
  const mappedAssetPaths = base ? assetPaths.map((a) => joinPath(base, a)) : [];

  // Make a list of possible paths
  const paths = [
    ...new Set([
      base,
      baseTagHref,
      baseTagHref && !isRemote(baseTagHref) && path.join(base, baseTagHref),
      base && isRelative(base) && path.join(process.cwd(), base),
      docurl,
      urlPath && urlResolve(urlObj.href, path.dirname(urlPath)),
      urlPath && !/\/$/.test(path.dirname(urlPath)) && urlResolve(urlObj.href, `${path.dirname(urlPath)}/`),
      docurl && urlResolve(docurl, file),
      docpath && path.dirname(docpath),
      ...assetPaths,
      ...mappedAssetPaths,
      to,
      from,
      base && docpath && path.join(base, path.dirname(docpath)),
      base && to && path.join(base, path.dirname(to)),
      base && from && path.join(base, path.dirname(from)),
      base && isRelative(file) && hops ? path.join(base, ...Array.from({length: hops}).fill('tmpdir'), file) : '',
      process.cwd(),
    ]),
  ];

  // Filter non-existent paths
  const filtered = await filterAsync(paths, (f) => {
    if (!f || (isAbsolute(f) && !f?.includes(process.cwd()))) {
      return false;
    }

    return !strict || fileExists(f, options);
  });

  // Findup first directory in search path and add to the list if available
  const all = await reduceAsync(filtered, [...new Set(filtered)], async (result, cwd) => {
    if (isRemote(cwd)) {
      return [...result, cwd];
    }

    // const up = await findUp(first, {cwd, type: 'directory'});
    const up = await findUpMultiple(first, {cwd, type: 'directory', stopAt: process.cwd()});
    const additionalDirectories = up.flatMap((u) => {
      const upDir = path.dirname(u);

      if (hops) {
        // Add additional directories based on dirHops
        const additional = path.relative(upDir, cwd).split(path.sep).slice(0, hops);

        return [upDir, path.join(upDir, ...additional)];
      }

      return [upDir];
    });

    return [...result, ...additionalDirectories];
  });

  debug(`(getAssetPaths) Search file "${file}" in:`, [...new Set(all)]);

  // Return uniquq result
  return [...new Set(all)];
}

/**
 * Create vinyl object from filepath
 * @param {object} src File descriptor either pass "filepath" or "html"
 * @param {object} options Critical options
 * @returns {Promise<Vinyl>} The vinyl object
 */
export async function vinylize(src, options = {}) {
  const {filepath, html} = src;
  const {rebase = {}, request = {}} = options;
  const file = new Vinyl();
  file.cwd = '/';
  file.remote = false;
  file.inline = false;

  if (html) {
    const {to} = rebase;
    file.contents = Buffer.from(html);
    file.path = to || '';
    file.virtualPath = to || '';
  } else if (filepath && Buffer.isBuffer(filepath)) {
    file.path = '';
    file.virtualPath = '';
    file.contents = filepath;
    file.inline = true;
  } else if (filepath && isVinyl(filepath)) {
    return filepath;
  } else if (filepath && isRemote(filepath)) {
    let url = filepath;
    try {
      const response = await fetch(filepath, {...options, request: {...request, method: 'head'}});
      if (response.url !== url) {
        debug(`(vinylize) found redirect from ${url} to ${response.url}`);
        url = response.url;
      }
    } catch {}

    file.remote = true;
    file.url = url;
    file.urlObj = urlParse(url);
    file.contents = await fetch(url, options);
    file.virtualPath = file.urlObj.pathname;
  } else if (filepath && fs.existsSync(filepath)) {
    file.path = filepath;
    file.virtualPath = filepath;
    file.contents = await readFileAsync(filepath);
  } else {
    throw new FileNotFoundError(filepath);
  }

  return file;
}

/**
 * Get stylesheet file object
 * @param {Vinyl} document Document vinyl object
 * @param {string} filepath Path/Url to css file
 * @param {object} options Critical options
 * @returns {Promise<Vinyl>} Vinyl representation fo the stylesheet
 */
export async function getStylesheet(document, filepath, options = {}) {
  const {rebase = {}, css, strict, media} = options;
  const originalPath = filepath;

  const exists = await fileExists(filepath, options);

  if (!exists) {
    const searchPaths = await getAssetPaths(document, filepath, options);
    try {
      filepath = await resolve(filepath, searchPaths, options);
    } catch (error) {
      if (!isRemote(filepath) || strict) {
        throw error;
      }

      return new Vinyl();
    }
  }

  // Create absolute file paths for local files passed via css option
  // to prevent document relative stylesheet paths if they are not relative specified
  if (!Buffer.isBuffer(originalPath) && !isVinyl(filepath) && !isRemote(filepath) && checkCssOption(css)) {
    filepath = path.resolve(filepath);
  }

  const file = await vinylize({filepath}, options);
  if (media) {
    file.contents = Buffer.from(`@media ${media} { ${file.contents.toString()} }`);
  }

  // Restore original path for local files referenced from document and not from options
  if (!Buffer.isBuffer(originalPath) && !isRemote(originalPath) && !checkCssOption(css)) {
    file.path = originalPath;
  }

  // Get stylesheet path. Keeps stylesheet url if it differs from document url
  const stylepath = await getStylesheetPath(document, file, options);
  if (Buffer.isBuffer(originalPath)) {
    file.path = stylepath;
    file.virtualPath = stylepath;
  }

  debug('(getStylesheet) Virtual Stylesheet Path:', stylepath);
  // We can safely rebase assets if we have:
  // - a url to the stylesheet
  // - if rebase.from and rebase.to is specified
  // - a valid document path and a stylesheet path
  // - an absolute positioned stylesheet so we can make the images absolute
  // - and rebase is not disabled (#359)
  // First respect the user input
  if (rebase === false) {
    return file;
  }

  if (rebase.from && rebase.to) {
    file.contents = await rebaseAssets(file.contents, rebase.from, rebase.to, {
      method: 'rebase',
      strict: options.strict,
      inlined: Buffer.isBuffer(originalPath),
    });
  } else if (typeof rebase === 'function') {
    file.contents = await rebaseAssets(file.contents, stylepath, document.virtualPath, {
      method: rebase,
      strict: options.strict,
      inlined: Buffer.isBuffer(originalPath),
    });
    // Next rebase to the stylesheet url
  } else if (isRemote(rebase.to || stylepath)) {
    const from = rebase.from || stylepath;
    const to = rebase.to || stylepath;
    const method = (asset) => (isRemote(asset.originUrl) ? asset.originUrl : urlResolve(to, asset.originUrl));
    file.contents = await rebaseAssets(file.contents, from, to, {
      method,
      strict: options.strict,
      inlined: Buffer.isBuffer(originalPath),
    });

    // Use relative path to document (local)
  } else if (document.virtualPath) {
    file.contents = await rebaseAssets(file.contents, rebase.from || stylepath, rebase.to || document.virtualPath, {
      method: 'rebase',
      strict: options.strict,
      inlined: Buffer.isBuffer(originalPath),
    });
  } else if (document.remote) {
    const {pathname} = document.urlObj;
    file.contents = await rebaseAssets(file.contents, rebase.from || stylepath, rebase.to || pathname, {
      method: 'rebase',
      strict: options.strict,
      inlined: Buffer.isBuffer(originalPath),
    });

    // Make images absolute if we have an absolute positioned stylesheet
  } else if (isAbsolute(stylepath)) {
    file.contents = await rebaseAssets(file.contents, rebase.from || stylepath, rebase.to || '/index.html', {
      method: (asset) => normalizePath(asset.absolutePath),
      strict: options.strict,
      inlined: Buffer.isBuffer(originalPath),
    });
  } else {
    warn(`Not rebasing assets for ${originalPath}. Use "rebase" option`);
  }

  debug('(getStylesheet) Result:', file);

  return file;
}

const isCssSource = (string) => {
  try {
    parse(string);
    return true;
  } catch {
    return false;
  }
};

/**
 * Get css for document
 * @param {Vinyl} document Vinyl representation of HTML document
 * @param {object} options Critical options
 * @returns {Promise<string>} Css string unoptimized, Multiple stylesheets are concatenated with EOL
 */
export async function getCss(document, options = {}) {
  const {css} = options;
  let stylesheets = [];

  if (checkCssOption(css)) {
    const cssArray = Array.isArray(css) ? css : [css];

    // merge css files & css source strings passed as css option
    const filesRaw = await Promise.all(
      cssArray.map((value) => {
        if (isCssSource(value)) {
          return Buffer.from(value);
        }

        return glob(value, options);
      })
    );

    const files = filesRaw.flat();
    stylesheets = await mapAsync(files, (file) => getStylesheet(document, file, options));
    debug('(getCss) css option set', files, stylesheets);
  } else {
    stylesheets = await mapAsync(document.stylesheets, (file, index) => {
      const media = (document.stylesheetsMedia || [])[index];
      return getStylesheet(document, file, {...options, media});
    });
    debug('(getCss) extract from document', document.stylesheets, stylesheets);
  }

  return stylesheets
    .filter((stylesheet) => !stylesheet.isNull())
    .map((stylesheet) => stylesheet.contents.toString())
    .join(os.EOL);
}

/**
 * We need to make sure the html file is available alongside the relative css files
 * as they are required by penthouse/puppeteer to render the html correctly
 * @see https://github.com/pocketjoso/penthouse/issues/280
 *
 * @param {Vinyl} document Vinyl representation of HTML document
 * @returns {Promise<string>} File url to html file for use in penthouse
 */
async function preparePenthouseData(document) {
  const tmp = [];
  const stylesheets = document.stylesheets || [];
  const [stylesheet, ...canBeEmpty] = stylesheets
    .filter((file) => isRelative(file))
    .map((file) => file.replace(/\?.*$/, ''));

  // Make sure we go as deep inside the temp folder as required by relative stylesheet hrefs
  const subfolders = [stylesheet, ...canBeEmpty]
    .reduce((res, href) => {
      const match = /^(\.\.\/)+/.exec(href || '');
      return match && match[0].length > res.length ? match[0] : res;
    }, './')
    .replaceAll('../', 'sub/');
  const dir = path.join(temporaryDirectory(), subfolders);
  const filename = path.basename(temporaryFile({extension: 'html'}));
  const file = path.join(dir, filename);

  const htmlContent = document.contents.toString();
  // Inject all styles to make sure we have everything in place
  // because puppeteer doesn't seem to fetch protocol relative links
  // when served from file://
  const injected = htmlContent.replaceAll(/(<head(?:\s[^>]*)?>)/gi, `$1<style>${document.css.toString()}</style>`);
  // Write html to temp file
  await outputFileAsync(file, injected);

  tmp.push(file);

  // Write styles to first stylesheet
  if (stylesheet) {
    const filename = path.join(dir, stylesheet);
    tmp.push(filename);
    await outputFileAsync(filename, document.css);
  }

  // Write empty string to rest of the linked stylesheets
  await forEachAsync(canBeEmpty, (dummy) => {
    const filename = path.join(dir, dummy);
    tmp.push(filename);
    outputFileAsync(filename, '');
  });

  return [getFileUri(file), getCleanup(tmp)];
}

/**
 * Get document file object
 * @param {string} filepath Path/Url to html file
 * @param {object} options Critical options
 * @returns {Promise<Vinyl>} Vinyl representation of HTML document
 */
export async function getDocument(filepath, options = {}) {
  const {rebase = {}, base} = options;

  if (!isVinyl(filepath) && !isRemote(filepath) && !fs.existsSync(filepath) && base) {
    filepath = joinPath(base, filepath);
  }

  const document = await vinylize({filepath}, options);

  document.stylesheets = await getStylesheetHrefs(document, options);
  document.stylesheetsMedia = await getStylesheetsMedia(document, options);
  document.virtualPath = rebase.to || (await getDocumentPath(document, options));

  document.cwd = base || process.cwd();
  if (!base && document.path) {
    document.cwd = document.path.replace(document.virtualPath, '');
  }

  debug('(getDocument) Result: ', {
    path: document.path,
    url: document.url,
    remote: Boolean(document.remote),
    virtualPath: document.virtualPath,
    stylesheets: document.stylesheets,
    cwd: document.cwd,
  });

  document.css = await getCss(document, options);

  const [url, cleanup] = await preparePenthouseData(document);
  document.url = url;
  document.cleanup = cleanup;

  return document;
}

/**
 * Get document file object from raw html source
 * @param {string} html HTML source
 * @param {object} options Critical options
 * @returns {Promise<*>} Vinyl representation of HTML document
 */
export async function getDocumentFromSource(html, options = {}) {
  const {rebase = {}, base} = options;
  const document = await vinylize({html}, options);

  document.stylesheets = await getStylesheetHrefs(document);
  document.stylesheetsMedia = await getStylesheetsMedia(document);
  document.virtualPath = rebase.to || (await getDocumentPath(document, options));
  document.cwd = base || process.cwd();

  debug('(getDocumentFromSource) Result: ', {
    path: document.path,
    url: document.url,
    remote: Boolean(document.remote),
    virtualPath: document.virtualPath,
    stylesheets: document.stylesheets,
    cwd: document.cwd,
  });

  document.css = await getCss(document, options);

  const [url, cleanup] = await preparePenthouseData(document);
  document.url = url;
  document.cleanup = cleanup;

  return document;
}


================================================
FILE: test/array.test.js
================================================
import {mapAsync, reduceAsync, filterAsync, forEachAsync} from '../src/array.js';

const waitFor = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); // eslint-disable-line no-promise-executor-return
const waitRandom = () => waitFor(Math.floor(Math.random() * Math.floor(50)));

test('async map', async () => {
  const afunc = async (value) => {
    await waitRandom();
    return value * value;
  };

  const func = (value) => value * value;
  const array = [1, 2, 3, 4, 5, 6, 7, 8];
  const expected = array.map((value) => func(value));

  const result1 = await mapAsync(array, (v) => func(v));
  const result2 = await mapAsync(array, (v) => afunc(v));

  expect(result1).toEqual(expected);
  expect(result2).toEqual(expected);
});

test('async map (default)', async () => {
  const array = [1, 2, 3, 4, 5, 6, 7, 8];

  const result = await mapAsync(array);
  expect(result).toEqual(array);
});

test('async map (empty)', async () => {
  const result = await mapAsync();
  expect(result).toEqual([]);
});

test('async reduce', async () => {
  const afunc = async (res, value, index) => {
    await waitRandom();
    return [...res, value * index];
  };

  const func = (res, value, index) => [...res, value * index];
  const array = [1, 2, 3, 4, 5, 6, 7, 8];
  const expected = array.reduce((res, value, index) => func(res, value, index), []);

  const result1 = await reduceAsync([], array, func);
  const result2 = await reduceAsync([], array, afunc);

  expect(result1).toEqual(expected);
  expect(result2).toEqual(expected);
});

test('async reduce (default)', async () => {
  const array = [1, 2, 3, 4, 5, 6, 7, 8];

  const result = await reduceAsync(array);
  expect(result).toEqual(array);
});

test('async reduce (empty)', async () => {
  const result = await reduceAsync();
  expect(result).toEqual(undefined);
});

test('async filter', async () => {
  const afunc = async (value) => {
    await waitRandom();
    return value % 2;
  };

  const func = (value) => value % 2;
  const array = [1, 2, 3, 4, 5, 6, 7, 8];
  const expected = array.filter((value) => func(value));

  const result1 = await filterAsync(array, func);
  const result2 = await filterAsync(array, afunc);

  expect(result1).toEqual(expected);
  expect(result2).toEqual(expected);
});

test('async filter (default)', async () => {
  const array = [1, 0, 3, false, 5, undefined, 7, null];

  const result = await filterAsync(array);
  expect(result).toEqual([1, 3, 5, 7]);
});

test('async filter (empty)', async () => {
  const result = await filterAsync();
  expect(result).toEqual([]);
});

test('async forEach', async () => {
  const array = [1, 2, 3, 4, 5, 6, 7, 8];
  const expected = [];
  const result1 = [];
  const result2 = [];
  array.forEach((v) => expected.push(v));

  await forEachAsync(array, (v) => result1.push(v));
  await forEachAsync(array, async (v) => {
    await waitRandom();
    result2.push(v);
  });

  expect(result1).toEqual(expected);
  expect(result2).toEqual(expected);
});


================================================
FILE: test/blackbox.test.js
================================================
import path from 'node:path';
import {createServer} from 'node:http';
import {fileURLToPath} from 'node:url';
import {Buffer} from 'node:buffer';
import process from 'node:process';
import fs from 'node:fs';
import {jest} from '@jest/globals';
import getPort from 'get-port';
import Vinyl from 'vinyl';
import nock from 'nock';
import async from 'async';
import finalhandler from 'finalhandler';
import serveStatic from 'serve-static';
import nn from 'normalize-newline';
import {generate} from '../index.js';
import {read, readAndRemove} from './helper/index.js';

jest.setTimeout(100_000);

const __dirname = fileURLToPath(new URL('.', import.meta.url));
const FIXTURES_DIR = path.join(__dirname, '/fixtures/');

function assertCritical(target, expected, done, skipTarget) {
  return (err, {css, html} = {}) => {
    const output = /\.css$/.test(target) ? css : html;

    if (err) {
      console.log(err);
      done(err);
    }

    try {
      expect(err).toBeFalsy();
      expect(output).toBeDefined();
      if (!skipTarget) {
        const dest = readAndRemove(target);
        expect(dest).toBe(expected);
      }

      expect(nn(output)).toBe(expected);
    } catch (error) {
      done(error);
      return;
    }

    done();
  };
}

// Setup static fileserver to mimic remote requests
let server;
let port;
beforeAll(async () => {
  const serve = serveStatic(path.join(__dirname, 'fixtures'), {index: ['index.html', 'index.htm']});
  const serveUserAgent = serveStatic(path.join(__dirname, 'fixtures/useragent'), {
    index: ['index.html', 'index.htm'],
  });

  port = await getPort();

  server = createServer((req, res) => {
    if (req.headers['user-agent'] === 'custom agent') {
      return serveUserAgent(req, res, finalhandler(req, res));
    }

    serve(req, res, finalhandler(req, res));
  }).listen(port);
});

afterAll(() => server.close());

// Prepare stderr mock
let stderr;
beforeEach(() => {
  stderr = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
});

afterEach(() => {
  stderr.mockRestore();
});

describe('generate (local)', () => {
  test('generate critical-path CSS', (done) => {
    const expected = read('expected/generate-default.css');
    const target = path.resolve('.critical.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generate-default.html',
        target,
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test('generate critical-path CSS from CSS files passed as Vinyl objects', (done) => {
    const expected = read('expected/generate-default.css');
    const target = path.resolve('.critical.css');
    const stylesheets = ['fixtures/styles/main.css', 'fixtures/styles/bootstrap.css'].map((filePath) => {
      return new Vinyl({
        cwd: '/',
        base: '/fixtures/',
        path: filePath,
        contents: Buffer.from(fs.readFileSync(path.join(__dirname, filePath), 'utf8'), 'utf8'),
      });
    });

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generate-default-nostyle.html',
        target,
        css: stylesheets,
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should throw an error on timeout', (done) => {
    const target = path.join(__dirname, '.include.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generate-default.html',
        penthouse: {
          timeout: 1,
        },
        target,
        width: 1300,
        height: 900,
      },
      (err) => {
        expect(err).toBeInstanceOf(Error);
        done();
      }
    );
  });

  test('should throw a usable error when no stylesheets are found', (done) => {
    const target = path.join(__dirname, '.error.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'error.html',
        penthouse: {
          timeout: 1,
        },
        target,
        width: 1300,
        height: 900,
      },
      (err) => {
        expect(err).toBeInstanceOf(Error);
        fs.promises.unlink(target).then(() => done());
      }
    );
  });

  test('should generate critical-path CSS with query string in file name', (done) => {
    const expected = read('expected/generate-default.css');
    const target = path.resolve('.critical.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generate-default-querystring.html',
        target,
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should ignore stylesheets blocked due to 403', (done) => {
    const expected = '';
    const target = path.resolve('.403.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: '403-css.html',
        target,
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should ignore stylesheets blocked due to 404', (done) => {
    const expected = '';
    const target = path.resolve('.404.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: '404-css.html',
        target,
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should generate multi-dimension critical-path CSS', (done) => {
    const expected = read('expected/generate-adaptive.css', 'utf8');
    const target = path.resolve('.adaptive.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generate-adaptive.html',
        target,
        dimensions: [
          {
            width: 100,
            height: 70,
          },
          {
            width: 1000,
            height: 70,
          },
        ],
      },
      assertCritical(target, expected, done)
    );
  });

  test('should consider inline styles', (done) => {
    const expected = read('expected/generate-adaptive.css', 'utf8');
    const target = path.resolve('.adaptive-inline.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generate-adaptive-inline.html',
        target,
        dimensions: [
          {
            width: 100,
            height: 70,
          },
          {
            width: 1000,
            height: 70,
          },
        ],
      },
      assertCritical(target, expected, done)
    );
  });

  test('should consider data uris in stylesheet hrefs', (done) => {
    const expected = read('expected/generate-adaptive.css', 'utf8');
    const target = path.resolve('.adaptive-base64.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generate-adaptive-base64.html',
        target,
        dimensions: [
          {
            width: 100,
            height: 70,
          },
          {
            width: 1000,
            height: 70,
          },
        ],
      },
      assertCritical(target, expected, done)
    );
  });

  test('should generate minified critical-path CSS', (done) => {
    const expected = read('expected/generate-default.css', true);
    const target = path.resolve('.critical.min.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generate-default.html',
        target,
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should generate minified critical-path CSS successfully with external css file configured', (done) => {
    const expected = read('expected/generate-default.css', true);
    const target = path.resolve('.nostyle.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generate-default-nostyle.html',
        css: ['fixtures/styles/main.css', 'fixtures/styles/bootstrap.css'],
        target,
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should evaluate css passed as source string', (done) => {
    const expected = 'html{display:block}';
    const target = path.resolve('.source-string.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generate-default-nostyle.html',
        css: ['html{display:block}.someclass{color:red}'],
        target,
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should inline relative images', (done) => {
    const expected = read('expected/generate-image.css');
    const target = path.resolve('.image-relative.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generate-image.html',
        css: ['fixtures/styles/image-relative.css'],
        target,
        width: 1300,
        height: 900,
        inlineImages: true,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should inline relative images from folder', (done) => {
    const expected = read('expected/generate-image.css');
    const target = path.resolve('.image-relative.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'folder/generate-image.html',
        css: ['fixtures/styles/image-relative.css'],
        target,
        width: 1300,
        height: 900,
        inlineImages: true,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should rewrite relative images for html outside root', (done) => {
    const expected = read('expected/generate-image-relative.css');
    const target = path.resolve('fixtures/folder/.image-relative.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'folder/generate-image.html',
        css: ['fixtures/styles/image-relative.css'],
        target,
        width: 1300,
        height: 900,
        inlineImages: false,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should rewrite relative images for html outside root with css file', (done) => {
    const expected = read('expected/generate-image-relative-subfolder.css');
    const target = path.resolve('fixtures/folder/subfolder/.image-relative-subfolder.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'folder/subfolder/generate-image-absolute.html',
        target,
        width: 1300,
        height: 900,
        inlineImages: false,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should rewrite relative images for html outside root destFolder option', (done) => {
    const expected = read('expected/generate-image-relative-subfolder.css');
    const target = path.resolve('.image-relative-subfolder.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'folder/subfolder/generate-image-absolute.html',
        // destFolder: 'folder/subfolder',
        // Dest: target,
        width: 1300,
        height: 900,
        inlineImages: false,
      },
      assertCritical(target, expected, done, true)
    );
  });

  test('should rewrite relative images for html inside root', (done) => {
    const expected = read('expected/generate-image-skip.css');
    const target = path.resolve('.image-relative.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generate-image.html',
        css: ['fixtures/styles/image-relative.css'],
        target,
        // destFolder: '.',
        width: 1300,
        height: 900,
        inlineImages: false,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should inline absolute images', (done) => {
    const expected = read('expected/generate-image.css');
    const target = path.resolve('.image-absolute.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generate-image.html',
        css: ['fixtures/styles/image-absolute.css'],
        target,
        // destFolder: '.',
        width: 1300,
        height: 900,
        inlineImages: true,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should skip to big images', (done) => {
    const expected = read('expected/generate-image-big.css');
    const target = path.resolve('.image-big.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generate-image.html',
        css: ['fixtures/styles/image-big.css'],
        target,
        // destFolder: '.',
        width: 1300,
        height: 900,
        inlineImages: true,
      },
      assertCritical(target, expected, done)
    );
  });

  test('considers "inlineImages" option', (done) => {
    const expected = read('expected/generate-image-skip.css');
    const target = path.resolve('.image-skip.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generate-image.html',
        css: ['fixtures/styles/image-relative.css'],
        target,
        // destFolder: '.',
        width: 1300,
        height: 900,
        inlineImages: false,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should not screw up win32 paths', (done) => {
    const expected = read('expected/generate-image.css');
    const target = path.resolve('.image.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generate-image.html',
        css: ['fixtures/styles/some/path/image.css'],
        target,
        width: 1300,
        height: 900,
        inlineImages: true,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should respect pathPrefix', (done) => {
    const expected = read('expected/path-prefix.css');
    const target = path.resolve('.path-prefix1.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'path-prefix.html',
        css: ['fixtures/styles/path-prefix.css'],
        target,
        width: 1300,
        height: 900,
        // pathPrefix: ''
      },
      assertCritical(target, expected, done)
    );
  });

  test('should detect pathPrefix', (done) => {
    const expected = read('expected/path-prefix.css');
    const target = path.resolve('.path-prefix2.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'path-prefix.html',
        css: ['fixtures/styles/path-prefix.css'],
        target,
        // destFolder: '.',
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should generate and inline, if "inline" option is set', (done) => {
    const expected = read('expected/generateInline.html');
    const target = path.join(__dirname, '.generateInline1.html');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generateInline.html',
        // destFolder: '.',
        target,
        inline: true,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should generate and inline critical-path CSS', (done) => {
    const expected = read('expected/generateInline.html');
    const target = path.join(__dirname, '.generateInline2.html');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generateInline.html',
        // destFolder: '.',
        target,
        inline: true,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should generate and inline minified critical-path CSS', (done) => {
    const expected = read('expected/generateInline.html');
    const target = path.join(__dirname, '.generateInline-minified3.html');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generateInline.html',
        // destFolder: '.',
        target,
        inline: true,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should handle multiple calls', (done) => {
    const expected1 = read('expected/generateInline.html');
    const expected2 = read('expected/generateInline-svg.html');

    async.series(
      {
        first(cb) {
          generate(
            {
              base: FIXTURES_DIR,
              src: 'generateInline.html',
              inline: true,
            },
            cb
          );
        },
        second(cb) {
          generate(
            {
              base: FIXTURES_DIR,
              src: 'generateInline-svg.html',
              inline: true,
            },
            cb
          );
        },
      },
      (err, results) => {
        try {
          expect(err).toBeFalsy();
          expect(nn(results.first.html)).toBe(expected1);
          expect(nn(results.second.html)).toBe(expected2);
          done();
        } catch (error) {
          done(error);
        }
      }
    );
  });

  test('should inline critical-path CSS ignoring remote stylesheets', (done) => {
    const expected = read('expected/generateInline-external-minified.html');
    const target = path.resolve('.generateInline-external.html');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generateInline-external.html',
        inlineImages: false,
        target,
        inline: true,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should inline critical-path CSS with extract option ignoring remote stylesheets', (done) => {
    const expected = read('expected/generateInline-external-extract.html');
    const target = path.resolve('.generateInline-external-extract.html');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generateInline-external.html',
        inlineImages: false,
        extract: true,
        target,
        inline: true,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should inline critical-path CSS without screwing svg images ', (done) => {
    const expected = read('expected/generateInline-svg.html');
    const target = path.resolve('.generateInline-svg.html');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generateInline-svg.html',
        target,
        inline: true,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should inline and extract critical-path CSS', (done) => {
    const expected = read('expected/generateInline-extract.html');
    const target = path.resolve('.generateInline-extract.html');

    generate(
      {
        base: FIXTURES_DIR,
        extract: true,
        src: 'generateInline.html',
        target,
        inline: true,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should inline and extract critical-path CSS from html source', (done) => {
    const expected = read('expected/generateInline-extract.html');
    const target = path.resolve('.generateInline-extract-src.html');

    generate(
      {
        base: FIXTURES_DIR,
        extract: true,
        html: read('fixtures/generateInline.html'),
        target,
        inline: true,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should consider "ignore" option', (done) => {
    const expected = read('expected/generate-ignore.css');
    const target = path.resolve('.ignore.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generate-default.html',
        target,
        ignore: ['@media', '.header', /jumbotron/],

        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should handle empty "ignore" array', (done) => {
    const expected = read('expected/generate-default.css', true);
    const target = path.join(__dirname, '.ignore.min.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generate-default.html',
        target,
        ignore: [],
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should handle ignore "@font-face"', (done) => {
    const expected = read('expected/generate-ignorefont.css', true);
    const target = path.join(__dirname, '.ignorefont.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generate-ignorefont.html',
        target,
        ignore: ['@font-face'],
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should keep styles defined by the `include` option', (done) => {
    const expected = read('fixtures/styles/include.css');
    const target = path.join(__dirname, '.include.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'include.html',
        include: [/someRule/],
        target,
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test('#192 - include option - generate', (done) => {
    const expected = read('expected/issue-192.css');
    const target = path.join(__dirname, '.issue-192.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'issue-192.html',
        css: ['fixtures/styles/issue-192.css'],
        dimensions: [
          {
            width: 320,
            height: 480,
          },
          {
            width: 768,
            height: 1024,
          },
          {
            width: 1280,
            height: 960,
          },
          {
            width: 1920,
            height: 1080,
          },
        ],
        extract: false,
        ignore: ['@font-face', /url\(/],
        include: [/^\.main-navigation.*$/, /^\.hero-deck.*$/, /^\.deck.*$/, /^\.search-box.*$/],
        target,
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should not complain about missing css if the css is passed via options', (done) => {
    const expected = read('expected/generate-default-nostyle.css');
    const target = path.join(__dirname, '.generate-default-nostyle.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generate-default-nostyle.html',
        css: ['fixtures/styles/bootstrap.css'],
        target,
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should not complain about missing css if the css is passed via options (inline)', (done) => {
    const expected = read('expected/generate-default-nostyle.html');
    const target = path.join(__dirname, '.generate-default-nostyle.html');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'generate-default-nostyle.html',
        css: ['fixtures/styles/bootstrap.css'],
        target,
        inline: true,
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should handle PAGE_UNLOADED_DURING_EXECUTION error (inline)', (done) => {
    const expected = read('fixtures/issue-314.html');
    const target = path.join(__dirname, '.issue-314.html');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'issue-314.html',
        css: ['fixtures/styles/bootstrap.css'],
        target,
        inline: true,
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test.skip('should handle PAGE_UNLOADED_DURING_EXECUTION error', (done) => {
    const expected = '';
    const target = path.join(__dirname, '.issue-314.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'issue-314.html',
        css: ['fixtures/styles/bootstrap.css'],
        target,
        inline: false,
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  // external css changed
  test.skip('external CSS with absolute url', (done) => {
    const expected = read('expected/issue-395.css');
    const target = path.join(__dirname, '.issue-395.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'issue-395.html',
        target,
        inline: false,
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test('Correctly order css on multiple dimensions', (done) => {
    const dimensions = [700, 600, 100, 200, 250, 150, 350, 400, 450, 500, 300, 550, 50].map((width) => {
      return {width, height: 1000};
    });

    const expected = read('fixtures/styles/issue-415.css');
    const target = path.join(__dirname, '.issue-415.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'issue-415.html',
        target,
        inline: false,
        dimensions,
        concurrency: 10,
      },
      assertCritical(target, expected, done)
    );
  });

  test('Ignore inlined stylesheets (disabled)', (done) => {
    const inlineStyles = '.test-selector{color:#00f}';
    const target = path.join(__dirname, '.ignore-inlined-styles.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'ignoreInlinedStyles.html',
        target,
        ignoreInlinedStyles: false,
        inline: false,
        concurrency: 1,
      },
      assertCritical(target, inlineStyles, done)
    );
  });

  test('Ignore inlined stylesheets (enabled)', (done) => {
    const target = path.join(__dirname, '.ignore-inlined-styles.css');
    generate(
      {
        base: FIXTURES_DIR,
        src: 'ignoreInlinedStyles.html',
        target,
        ignoreInlinedStyles: true,
        inline: false,
        concurrency: 1,
      },
      assertCritical(target, '', done)
    );
  });

  test('issue #566 - consider base tag', (done) => {
    const expected = read('expected/issue-566.css');
    const target = path.join(__dirname, '.issue-566.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: 'issue-566.html',
        target,
        inline: false,
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });
});

describe('generate (remote)', () => {
  test('should generate critical-path CSS', (done) => {
    const expected = read('expected/generate-default.css');
    const target = path.join(__dirname, '.critical.css');

    generate(
      {
        src: `http://localhost:${port}/generate-default.html`,
        target,
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should generate multi-dimension critical-path CSS', (done) => {
    const expected = read('expected/generate-adaptive.css', 'utf8');
    const target = path.join(__dirname, '.adaptive.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: `http://localhost:${port}/generate-adaptive.html`,
        target,
        penthouse: {
          timeout: 10_000,
        },
        dimensions: [
          {
            width: 100,
            height: 70,
          },
          {
            width: 1000,
            height: 70,
          },
        ],
      },
      assertCritical(target, expected, done)
    );
  });

  test('should generate minified critical-path CSS', (done) => {
    const expected = read('expected/generate-default.css', true);
    const target = path.join(__dirname, '.critical.min.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: `http://localhost:${port}/generate-default.html`,
        target,
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should generate minified critical-path CSS successfully with external css file configured', (done) => {
    const expected = read('expected/generate-default.css', true);
    const target = path.join(__dirname, '.nostyle.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: `http://localhost:${port}/generate-default-nostyle.html`,
        css: ['fixtures/styles/main.css', 'fixtures/styles/bootstrap.css'],
        target,
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should inline relative images', (done) => {
    const expected = read('expected/generate-image.css');
    const target = path.join(__dirname, '.image-relative.css');
    try {
      generate(
        {
          src: `http://localhost:${port}/generate-image.html`,
          target,
          width: 1300,
          height: 900,
          inlineImages: true,
        },
        assertCritical(target, expected, done)
      );
    } catch (error) {
      console.log(error);
    }
  });

  test('should inline relative images fetched over http', (done) => {
    const expected = read('expected/generate-image.css');
    const target = path.join(__dirname, '.image-relative.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: `http://localhost:${port}/generate-image.html`,
        css: ['fixtures/styles/image-relative.css'],
        target,
        width: 1300,
        height: 900,
        inlineImages: true,
        //  assetPaths: [`http://localhost:${port}/`, `http://localhost:${port}/styles`]
      },
      assertCritical(target, expected, done)
    );
  });

  test('should inline absolute images', (done) => {
    const expected = read('expected/generate-image.css');
    const target = path.join(__dirname, '.image-absolute.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: `http://localhost:${port}/generate-image.html`,
        css: ['fixtures/styles/image-absolute.css'],
        target,
        width: 1300,
        height: 900,
        inlineImages: true,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should inline absolute images fetched over http', (done) => {
    const expected = read('expected/generate-image.css');
    const target = path.join(__dirname, '.image-absolute.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: `http://localhost:${port}/generate-image.html`,
        css: ['fixtures/styles/image-absolute.css'],
        target,
        width: 1300,
        height: 900,
        inlineImages: true,
        // assetPaths: [`http://localhost:${port}/`, `http://localhost:${port}/styles`]
      },
      assertCritical(target, expected, done)
    );
  });

  test('should skip to big images', (done) => {
    const expected = read('expected/generate-image-big.css');
    const target = path.join(__dirname, '.image-big.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: `http://localhost:${port}/generate-image.html`,
        css: ['fixtures/styles/image-big.css'],
        target,
        width: 1300,
        height: 900,
        inlineImages: true,
      },
      assertCritical(target, expected, done)
    );
  });

  test('considers "inlineImages" option', (done) => {
    const expected = read('expected/generate-image-skip.css');
    const target = path.join(__dirname, '.image-skip.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: `http://localhost:${port}/generate-image.html`,
        css: ['fixtures/styles/image-relative.css'],
        target,
        width: 1300,
        height: 900,
        inlineImages: false,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should not screw up win32 paths', (done) => {
    const expected = read('expected/generate-image.css');
    const target = path.join(__dirname, '.image.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: `http://localhost:${port}/generate-image.html`,
        css: ['fixtures/styles/some/path/image.css'],
        target,
        width: 1300,
        height: 900,
        inlineImages: true,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should respect pathPrefix', (done) => {
    const expected = read('expected/path-prefix.css');
    const target = path.join(__dirname, '.path-prefix.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: `http://localhost:${port}/path-prefix.html`,
        css: ['fixtures/styles/path-prefix.css'],
        target,
        width: 1300,
        height: 900,
        // Empty string most likely to candidate for failure if change in code results in checking option lazily,
        // pathPrefix: ''
      },
      assertCritical(target, expected, done)
    );
  });

  test('should detect pathPrefix', (done) => {
    const expected = read('expected/path-prefix.css');
    const target = path.join(__dirname, '.path-prefix.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: `http://localhost:${port}/path-prefix.html`,
        css: ['fixtures/styles/path-prefix.css'],
        target,
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should generate and inline, if "inline" option is set', (done) => {
    const expected = read('expected/generateInline.html');
    const target = path.join(__dirname, '.generateInline.html');

    generate(
      {
        base: FIXTURES_DIR,
        src: `http://localhost:${port}/generateInline.html`,
        target,
        inline: true,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should generate and inline critical-path CSS', (done) => {
    const expected = read('expected/generateInline.html');
    const target = path.join(__dirname, '.generateInline.html');

    generate(
      {
        base: FIXTURES_DIR,
        src: `http://localhost:${port}/generateInline.html`,
        target,
        inline: true,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should generate and inline minified critical-path CSS', (done) => {
    const expected = read('expected/generateInline.html');
    const target = path.join(__dirname, '.generateInline.html');

    generate(
      {
        base: FIXTURES_DIR,
        src: `http://localhost:${port}/generateInline.html`,
        target,
        inline: true,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should handle multiple calls', (done) => {
    const expected1 = read('expected/generateInline.html');
    const expected2 = read('expected/generateInline.html');
    async.series(
      {
        first(cb) {
          generate(
            {
              base: FIXTURES_DIR,
              src: `http://localhost:${port}/generateInline.html`,
              inline: true,
            },
            cb
          );
        },
        second(cb) {
          generate(
            {
              base: FIXTURES_DIR,
              src: `http://localhost:${port}/generateInline.html`,
              inline: true,
            },
            cb
          );
        },
      },
      (err, results) => {
        expect(err).toBeFalsy();
        expect(nn(results.first.html)).toBe(expected1);
        expect(nn(results.second.html)).toBe(expected2);
        done(err);
      }
    );
  });

  test('should inline critical-path CSS handling remote stylesheets', (done) => {
    const expected = read('expected/generateInline-external-minified2.html');
    const target = path.join(__dirname, '.generateInline-external2.html');

    generate(
      {
        base: FIXTURES_DIR,
        src: `http://localhost:${port}/generateInline-external2.html`,
        inlineImages: false,
        target,
        inline: true,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should inline critical-path CSS with extract option handling remote stylesheets', (done) => {
    const expected = read('expected/generateInline-external-extract2.html');
    const target = path.join(__dirname, '.generateInline-external-extract.html');

    generate(
      {
        base: FIXTURES_DIR,
        src: `http://localhost:${port}/generateInline-external2.html`,
        inlineImages: false,
        extract: true,
        target,
        inline: true,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should inline critical-path CSS without screwing svg images ', (done) => {
    const expected = read('expected/generateInline-svg.html');
    const target = path.join(__dirname, '.generateInline-svg.html');

    generate(
      {
        base: FIXTURES_DIR,
        src: `http://localhost:${port}/generateInline-svg.html`,
        target,
        inline: true,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should inline and extract critical-path CSS', (done) => {
    const expected = read('expected/generateInline-extract.html');
    const target = path.join(__dirname, '.generateInline-extract.html');

    generate(
      {
        base: FIXTURES_DIR,
        extract: true,
        src: `http://localhost:${port}/generateInline.html`,
        target,
        inline: true,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should consider "ignore" option', (done) => {
    const expected = read('expected/generate-ignore.css');
    const target = path.join(__dirname, '.ignore.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: `http://localhost:${port}/generate-default.html`,
        target,
        ignore: ['@media', '.header', /jumbotron/],

        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should handle empty "ignore" array', (done) => {
    const expected = read('expected/generate-default.css', true);
    const target = path.join(__dirname, '.ignore.min.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: `http://localhost:${port}/generate-default.html`,
        target,
        ignore: [],
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should handle ignore "@font-face"', (done) => {
    const expected = read('expected/generate-ignorefont.css', true);
    const target = path.join(__dirname, '.ignorefont.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: `http://localhost:${port}/generate-ignorefont.html`,
        target,
        ignore: ['@font-face'],
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should keep styles defined by the `include` option', (done) => {
    const expected = read('fixtures/styles/include.css');
    const target = path.join(__dirname, '.include.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: `http://localhost:${port}/include.html`,
        include: [/someRule/],
        target,
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });

  test('should use the provided user agent to get the remote src', (done) => {
    const expected = read('expected/generate-default.css');
    const target = path.join(__dirname, '.critical.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: `http://localhost:${port}/generate-default-useragent.html`,
        include: [/someRule/],
        target,
        width: 1300,
        height: 900,
        userAgent: 'custom agent',
      },
      assertCritical(target, expected, done)
    );
  });

  test('should use the provided request method to check for asset existance', async () => {
    const mockGet = jest.fn();
    const mockHead = jest.fn();
    nock(`http://localhost:${port}`, {allowUnmocked: true})
      .intercept('/styles/adaptive.css', 'GET')
      .reply(200, mockGet)
      .intercept('/styles/adaptive.css', 'HEAD')
      .reply(200, mockHead);

    await generate({
      base: FIXTURES_DIR,
      src: `http://localhost:${port}/generate-adaptive.html`,
      width: 1300,
      height: 900,
      request: {method: 'get'},
    });

    expect(mockGet).toHaveBeenCalled();
    expect(mockHead).not.toHaveBeenCalled();

    await generate({
      base: FIXTURES_DIR,
      src: `http://localhost:${port}/generate-adaptive.html`,
      width: 1300,
      height: 900,
    });

    expect(mockHead).toHaveBeenCalled();
  });

  test('issue #566 - consider base tag', (done) => {
    const expected = read('expected/issue-566.css');
    const target = path.join(__dirname, '.issue-566.css');

    generate(
      {
        base: FIXTURES_DIR,
        src: `http://localhost:${port}/issue-566.html`,
        target,
        inline: false,
        width: 1300,
        height: 900,
      },
      assertCritical(target, expected, done)
    );
  });
});


================================================
FILE: test/cli.test.js
================================================
import {exec, execFile} from 'node:child_process';
// import {createRequire} from 'node:module';
import path from 'node:path';
import process from 'node:process';
import {fileURLToPath} from 'node:url';
import {promisify} from 'node:util';
import {globby} from 'globby';
import {jest} from '@jest/globals';
import nn from 'normalize-newline';
import {read, getPkg} from './helper/index.js';

jest.setTimeout(100_000);

const __dirname = path.dirname(fileURLToPath(import.meta.url));
// const require = createRequire(import.meta.url);

const {version, bin} = getPkg();

jest.unstable_mockModule('../index.js', () => ({
  generate: jest.fn(),
  stream: jest.fn(),
}));

const criticalBin = path.join(__dirname, '..', bin);

process.chdir(path.resolve(__dirname));
process.setMaxListeners(0);

const pExec = promisify(exec);
const pExecFile = promisify(execFile);

const run = async (args = []) => pExecFile('node', [criticalBin, ...args]);

const getArgs = async (params = []) => {
  const origArgv = process.argv;
  process.argv = ['node', criticalBin, ...params];

  await import('../cli.js');
  process.argv = origArgv;
  const {generate} = await import('../index.js');
  const [args] = generate.mock.calls;
  const [opts] = args || [{}];
  expect(generate).toHaveBeenCalledTimes(1);
  return opts || {};
};

const pipe = async (filename, args = []) => {
  const cat = process.platform === 'win32' ? 'type' : 'cat';
  const cmd = `${cat} ${filename} | node ${criticalBin} ${args.join(' ')}`;
  return pExec(cmd, {shell: true});
};

describe('CLI', () => {
  describe('acceptance', () => {
    test('Show error alongside help', async () => {
      expect.assertions(3);
      try {
        await run(['not available']);
      } catch (error) {
        expect(error.stderr).toMatch('Error:');
        expect(error.stderr).toMatch('Usage: critical');
        expect(error.code).toBe(1);
      }
    });

    test('Return version', async () => {
      const {stdout, stderr} = await run(['--version']);

      expect(stderr).toBeFalsy();
      expect(stdout.trim()).toBe(version);
    });

    test('Take html file passed via parameter', async () => {
      const {stdout, stderr} = await run([
        'fixtures/generate-default.html',
        '--base',
        'fixtures',
        '--width',
        '1300',
        '--height',
        '900',
      ]);
      const expected = await read('expected/generate-default.css');

      expect(stderr).toBeFalsy();
      expect(nn(stdout)).toBe(expected);
    });

    test('Take html file piped to critical', async () => {
      const {stdout, stderr} = await pipe(path.normalize('fixtures/generate-default.html'), [
        '--base',
        'fixtures',
        '--width',
        '1300',
        '--height',
        '900',
      ]);
      const expected = await read('expected/generate-default.css');

      expect(stderr).toMatch('Not rebasing assets for');
      expect(stderr.code).toBeUndefined();
      expect(nn(stdout)).toBe(expected);
    });

    test('Pipe html file inside a folder to critical', async () => {
      const {stdout, stderr} = await pipe(path.normalize('fixtures/folder/generate-default.html'), [
        '--base',
        'fixtures',
        '--width',
        '1300',
        '--height',
        '900',
      ]);
      const expected = await read('expected/generate-default.css');

      expect(stderr).toMatch('Not rebasing assets for');
      expect(stderr.code).toBeUndefined();
      expect(nn(stdout)).toBe(expected);
    });

    test('Inline images to piped html file', async () => {
      const {stdout, stderr} = await pipe(path.normalize('fixtures/generate-image.html'), [
        '-c',
        'fixtures/styles/image-relative.css',
        '--inlineImages',
        '--base',
        'fixtures',
        '--width',
        '1300',
        '--height',
        '900',
      ]);
      const expected = await read('expected/generate-image.css');

      expect(stderr).toBeFalsy();
      expect(nn(stdout)).toBe(expected);
    });

    test("Add an absolute image path to critical css if we can't determine document location", async () => {
      const {stdout, stderr} = await pipe(path.normalize('fixtures/folder/generate-image.html'), [
        '-c',
        'fixtures/styles/image-relative.css',
        '--base',
        'fixtures',
        '--width',
        '1300',
        '--height',
        '900',
      ]);
      const expected = await read('expected/generate-image-absolute.css');

      expect(stderr).toBeFalsy();
      expect(nn(stdout)).toBe(expected);
    });

    test('Add absolute image paths on piped html without relative links', async () => {
      const {stdout, stderr} = await pipe(path.normalize('fixtures/folder/subfolder/generate-image-absolute.html'), [
        '--base',
        'fixtures',
        '--width',
        '1300',
        '--height',
        '900',
      ]);
      const expected = await read('expected/generate-image-absolute.css');

      expect(stderr).toBeFalsy();
      expect(nn(stdout)).toBe(expected);
    });

    test('Exit with code 1 and show help', async () => {
      expect.assertions(2);
      try {
        await run(['fixtures/not-exists.html']);
      } catch (error) {
        expect(error.code).toBe(1);
        expect(error.stderr).toMatch('Usage:');
      }
    });

    test('Generate multi-dimension critical-path CSS using cli', async () => {
      const {stdout} = await pipe(path.normalize('fixtures/generate-adaptive.html'), [
        '--base',
        'fixtures',
        '--dimensions',
        '100x70',
        '--dimensions',
        '1000x70',
      ]);
      const expected = await read('expected/generate-adaptive.css', 'utf8');
      expect(nn(stdout)).toBe(expected);
    });
  });

  let exit;
  describe('mocked', () => {
    beforeEach(() => {
      jest.resetModules();
      exit = process.exit;
    });

    afterEach(() => {
      process.exit = exit;
    });

    test('pass the correct opts when using short opts', async () => {
      const args = await getArgs(['fixtures/generate-default.html', '-c', 'css', '-w', '300', '-h', '400', '-e', '-i']);

      expect(args).toMatchObject({
        width: 300,
        height: 400,
        css: ['css'],
        inline: true,
        extract: true,
      });
    });

    test('pass the correct opts when using long opts', async () => {
      const args = await getArgs([
        'fixtures/generate-default.html',
        '--css',
        'css',
        '--width',
        '300',
        '--height',
        '400',
        '--ignore',
        'ignore',
        '--include',
        '/include/',
        '--inline',
        '--extract',
        '--inlineImages',
        '1024',
        '--assetPaths',
        'assetPath1',
        '--assetPaths',
        'assetPath2',
        '--dimensions',
        '1300x800',
        '--dimensions',
        '640x480',
        '--dimensions',
        '1x2,3x4,5x6',
      ]);

      expect(args).toMatchObject({
        width: 300,
        height: 400,
        css: ['css'],
        inline: true,
        extract: true,
        dimensions: [
          {width: 1300, height: 800},
          {width: 640, height: 480},
          {width: 1, height: 2},
          {width: 3, height: 4},
          {width: 5, height: 6},
        ],
      });
    });

    test('Set inline to false when prefixed with --no', async () => {
      const args = await getArgs(['fixtures/generate-default.html', '--no-inline']);

      expect(args).toMatchObject({
        inline: false,
      });
    });

    test('Set penthouse options prefixed with --penthouse-', async () => {
      const args = await getArgs([
        'fixtures/generate-default.html',
        '--penthouse-strict',
        '--penthouse-timeout',
        '50000',
        '--penthouse-renderWaitTime',
        '300',
      ]);

      expect(args).toMatchObject({
        penthouse: {
          strict: true,
          timeout: 50_000,
          renderWaitTime: 300,
        },
      });
    });

    test('Set request options prefixed with --request-', async () => {
      const args = await getArgs([
        'fixtures/generate-default.html',
        '--request-method',
        'get',
        '--no-request-followRedirect',
      ]);

      expect(args).toMatchObject({
        request: {
          method: 'get',
          followRedirect: false,
        },
      });
    });

    test('Handle shell expanded the glob', async () => {
      // simulate system glob
      const css = await globby('fixtures/**/*.css');
      const args = await getArgs(['fixtures/generate-default.html', '-c', ...css, '--target', 'test.css']);

      expect(args).toMatchObject({
        css,
        target: 'test.css',
        src: 'fixtures/generate-default.html',
      });
    });

    test('Handle glob', async () => {
      // simulate system glob
      const args = await getArgs(['fixtures/generate-default.html', '-c', 'fixtures/**/*.css', '--target', 'test.css']);

      expect(args).toMatchObject({
        css: ['fixtures/**/*.css'],
        target: 'test.css',
        src: 'fixtures/generate-default.html',
      });
    });
  });
});


================================================
FILE: test/config.test.js
================================================
import {ConfigError} from '../src/errors.js';
import {getOptions, DEFAULT} from '../src/config.js';

test('Throws ConfigError on invalid config', () => {
  expect(async () => {
    await getOptions({invalidParam: true});
  }).rejects.toThrow(ConfigError);
});

test('Throws ConfigError on missing param', async () => {
  expect(async () => {
    await getOptions({});
  }).rejects.toThrow(ConfigError);
});

test('Throws ConfigError when html & src are both set', async () => {
  expect(async () => {
    await getOptions({html: '...', src: '...'});
  }).rejects.toThrow(ConfigError);
});

test('Throws ConfigError on empty required value', async () => {
  expect(async () => {
    await getOptions({src: ''});
  }).rejects.toThrow(ConfigError);
});

test('Returns config object', async () => {
  const config = await getOptions({src: '...'});
  expect(config).toMatchObject({
    src: '...',
    width: DEFAULT.width,
    height: DEFAULT.height,
    maxImageFileSize: DEFAULT.maxImageFileSize,
    strict: DEFAULT.strict,
    extract: DEFAULT.extract,
    concurrency: DEFAULT.concurrency,
    inlineImages: DEFAULT.inlineImages,
    include: DEFAULT.include,
    inline: DEFAULT.inline,
    dimensions: [{width: DEFAULT.width, height: DEFAULT.height}],
    penthouse: {
      forceInclude: DEFAULT.include,
      timeout: DEFAULT.timeout,
      maxEmbeddedBase64Length: DEFAULT.maxImageFileSize,
    },
  });
});

test('Target config on passed string', async () => {
  expect(await getOptions({src: '...', target: 'test.css'})).toHaveProperty('target', {css: 'test.css'});
  expect(await getOptions({src: '...', target: 'test.html'})).toHaveProperty('target', {html: 'test.html'});
});

test('Inline config on passed boolean', async () => {
  expect(await getOptions({src: '...', inline: true, base: 'BASE'})).toHaveProperty('inline', {
    basePath: 'BASE',
    strategy: 'media',
  });
});

test('Inline config on passed object', async () => {
  expect(await getOptions({src: '...', inline: {check: true}, base: 'BASE'})).toHaveProperty('inline', {
    basePath: 'BASE',
    check: true,
  });
});

test('Penthouse config on passed object', async () => {
  expect(await getOptions({src: '...', penthouse: {check: true}})).toHaveProperty('penthouse', {
    forceInclude: DEFAULT.include,
    timeout: DEFAULT.timeout,
    maxEmbeddedBase64Length: DEFAULT.maxImageFileSize,
    check: true,
  });
});

test('Ignore config on passed array', async () => {
  expect(await getOptions({src: '...', ignore: ['@font-face']})).toHaveProperty('ignore', {
    atrule: ['@font-face'],
    rule: ['@font-face'],
    decl: ['@font-face'],
  });
});

test('Parses config values passed as JSON string', async () => {
  const headers = {cookie: 'key=value'};
  expect(await getOptions({src: '...', request: {headers: JSON.stringify(headers)}})).toHaveProperty('request', {
    headers,
  });
});


================================================
FILE: test/core.test.js
================================================
import process from 'node:process';
import {createServer} from 'node:http';
import path from 'node:path';
import {fileURLToPath} from 'node:url';
import {jest} from '@jest/globals';
import finalhandler from 'finalhandler';
import getPort from 'get-port';
import serveStatic from 'serve-static';
import CleanCSS from 'clean-css';
import {create} from '../src/core.js';
import {read} from './helper/index.js';

const __dirname = fileURLToPath(new URL('.', import.meta.url));
jest.setTimeout(100_000);

// Set up static fileserver to mimic remote requests
let server;
let port;
beforeAll(async () => {
  const root = path.join(__dirname, 'fixtures');
  const serve = serveStatic(root, {index: ['index.html', 'index.htm']});
  port = await getPort();

  server = createServer((req, res) => {
    serve(req, res, finalhandler(req, res));
  }).listen(port);
});

afterAll(() => server.close());

// Prepare stderr mock
let stderr;
beforeEach(() => {
  stderr = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
});

afterEach(() => {
  stderr.mockRestore();
});

test('Generate critical-path CSS', async () => {
  const css = read('expected/generate-default.css');
  const html = read('fixtures/generate-default.html');

  try {
    const result = await create({
      src: `http://localhost:${port}/generate-default.html`,
    });

    expect(result.css).toBe(css);
    expect(result.html).toBe(html);
  } catch (error) {
    expect(error).toBe(undefined);
  }
});

test('Generate critical-path CSS with custom cleancss config', async () => {
  const css = read('fixtures/styles/issue-562.css');
  const html = read('fixtures/issue-562.html');

  const optionsArray = [
    {
      level: 2,
      format: 'beautify',
    },
    {
      level: 1,
    },
  ];

  for (const options of optionsArray) {
    const expected = new CleanCSS(options).minify(css).styles;

    try {
      // eslint-disable-next-line no-await-in-loop
      const result = await create({
        src: `http://localhost:${port}/issue-562.html`,
        cleanCSS: options,
        inline: false,
        dimensions: [
          {
            width: 100,
            height: 70,
          },
          {
            width: 1000,
            height: 70,
          },
        ],
      });

      expect(result.css).toBe(expected);
      expect(result.html).toBe(html);
    } catch (error) {
      expect(error).toBe(undefined);
    }
  }
});


================================================
FILE: test/expected/generate-adaptive-useragent.css
================================================
#of{background:teal}#guybrush{color:pink}#threepwood{background:orange;content:'monkey island'}@media screen and (min-width:900px){div{height:400px;background:brown}}#revenge{background:#ffefd5}

================================================
FILE: test/expected/generate-adaptive.css
================================================
#of{background:teal}#guybrush{color:pink}#threepwood{background:orange;content:'monkey island'}@media screen and (min-width:900px){div{height:400px;background:brown}}#revenge{background:#ffefd5}

================================================
FILE: test/expected/generate-default-nostyle.css
================================================
html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a{background:0 0}h1{margin:.67em 0;font-size:2em}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}a{color:#428bca;text-decoration:none}h1,h3,h4{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1,h3{margin-top:20px;margin-bottom:10px}h4{margin-top:10px;margin-bottom:10px}h1{font-size:36px}h3{font-size:24px}h4{font-size:18px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}.text-muted{color:#999}ul{margin-top:0;margin-bottom:10px}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.lead{font-size:21px}.container{width:750px}}@media (min-width:992px){.container{width:970px}}.row{margin-right:-15px;margin-left:-15px}.col-lg-6{position:relative;min-height:1px;padding-right:15px;padding-left:15px}@media (min-width:1200px){.container{width:1170px}.col-lg-6{float:left}.col-lg-6{width:50%}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;background-image:none;border:1px solid transparent;border-radius:4px}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a{color:#fff;background-color:#428bca}.jumbotron{padding:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.container .jumbotron{border-radius:6px}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1{font-size:63px}}.container:after,.container:before,.nav:after,.nav:before,.row:after,.row:before{display:table;content:" "}.container:after,.nav:after,.row:after{clear:both}.pull-right{float:right!important}@-ms-viewport{width:device-width}

================================================
FILE: test/expected/generate-default-nostyle.html
================================================
<!doctype html>
<html class="no-js">
    <head>
        <meta charset="utf-8">
        <title>critical css test</title>
        <meta name="description" content="">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <!-- Place favicon.ico and apple-touch-icon.png in the root directory -->

        <style>html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a{background:0 0}h1{margin:.67em 0;font-size:2em}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}a{color:#428bca;text-decoration:none}h1,h3,h4{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1,h3{margin-top:20px;margin-bottom:10px}h4{margin-top:10px;margin-bottom:10px}h1{font-size:36px}h3{font-size:24px}h4{font-size:18px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}.text-muted{color:#999}ul{margin-top:0;margin-bottom:10px}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.lead{font-size:21px}.container{width:750px}}@media (min-width:992px){.container{width:970px}}.row{margin-right:-15px;margin-left:-15px}.col-lg-6{position:relative;min-height:1px;padding-right:15px;padding-left:15px}@media (min-width:1200px){.container{width:1170px}.col-lg-6{float:left}.col-lg-6{width:50%}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;background-image:none;border:1px solid transparent;border-radius:4px}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a{color:#fff;background-color:#428bca}.jumbotron{padding:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.container .jumbotron{border-radius:6px}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1{font-size:63px}}.container:after,.container:before,.nav:after,.nav:before,.row:after,.row:before{display:table;content:" "}.container:after,.nav:after,.row:after{clear:both}.pull-right{float:right!important}@-ms-viewport{width:device-width}</style>
    </head>
    <body>
        <!--[if lt IE 10]>
            <p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
        <![endif]-->

        <div class="container">
            <div class="header">
                <ul class="nav nav-pills pull-right">
                    <li class="active"><a href="#">Home</a></li>
                    <li><a href="#">About</a></li>
                    <li><a href="#">Contact</a></li>
                </ul>
                <h3 class="text-muted">critical css test</h3>
            </div>

            <div class="jumbotron">
                <h1>'Allo, 'Allo!</h1>
                <p class="lead">Always a pleasure scaffolding your apps.</p>
                <p><a class="btn btn-lg btn-success" href="#">Splendid!</a></p>
            </div>

            <div class="row marketing">
                <div class="col-lg-6">
                    <h4>HTML5 Boilerplate</h4>
                    <p>HTML5 Boilerplate is a professional front-end template for building fast, robust, and adaptable web apps or sites.</p>

                    <h4>Bootstrap</h4>
                    <p>Sleek, intuitive, and powerful mobile first front-end framework for faster and easier web development.</p>
                </div>
            </div>

            <div class="footer">
                <p>♥ from the Yeoman team</p>
            </div>
        </div>

        <!-- Google Analytics: change UA-XXXXX-X to be your site's ID. -->
        <script>
            (function(b,o,i,l,e,r){b.GoogleAnalyticsObject=l;b[l]||(b[l]=
            function(){(b[l].q=b[l].q||[]).push(arguments)});b[l].l=+new Date;
            e=o.createElement(i);r=o.getElementsByTagName(i)[0];
            e.src='//www.google-analytics.com/analytics.js';
            r.parentNode.insertBefore(e,r)}(window,document,'script','ga'));
            ga('create','UA-XXXXX-X');ga('send','pageview');
        </script>

        <!-- build:js scripts/vendor.js -->
        <!-- bower:js -->
        <script src="../old/fixture/bower_components/jquery/dist/jquery.js"></script>
        <script src="../old/fixture/bower_components/bootstrap/dist/js/bootstrap.js"></script>
        <!-- endbower -->
        <!-- endbuild -->

        <!-- build:js scripts/plugins.js -->
        <script src="../old/fixture/bower_components/bootstrap/js/affix.js"></script>
        <script src="../old/fixture/bower_components/bootstrap/js/alert.js"></script>
        <script src="../old/fixture/bower_components/bootstrap/js/dropdown.js"></script>
        <script src="../old/fixture/bower_components/bootstrap/js/tooltip.js"></script>
        <script src="../old/fixture/bower_components/bootstrap/js/modal.js"></script>
        <script src="../old/fixture/bower_components/bootstrap/js/transition.js"></script>
        <script src="../old/fixture/bower_components/bootstrap/js/button.js"></script>
        <script src="../old/fixture/bower_components/bootstrap/js/popover.js"></script>
        <script src="../old/fixture/bower_components/bootstrap/js/carousel.js"></script>
        <script src="../old/fixture/bower_components/bootstrap/js/scrollspy.js"></script>
        <script src="../old/fixture/bower_components/bootstrap/js/collapse.js"></script>
        <script src="../old/fixture/bower_components/bootstrap/js/tab.js"></script>
        <!-- endbuild -->

        <!-- build:js scripts/main.js -->
        <script src="../old/fixture/scripts/main.js"></script>
        <!-- endbuild -->
</body>
</html>


================================================
FILE: test/expected/generate-default.css
================================================
body{padding-top:20px;padding-bottom:20px}.header,.marketing{padding-left:15px;padding-right:15px}.header{border-bottom:1px solid #e5e5e5}.header h3{margin-top:0;margin-bottom:0;line-height:40px;padding-bottom:19px}.jumbotron{text-align:center;border-bottom:1px solid #e5e5e5}.jumbotron .btn{font-size:21px;padding:14px 24px}.marketing{margin:40px 0}@media screen and (min-width:768px){.container{max-width:730px}.header,.marketing{padding-left:0;padding-right:0}.header{margin-bottom:30px}.jumbotron{border-bottom:0}}html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a{background:0 0}h1{margin:.67em 0;font-size:2em}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}a{color:#428bca;text-decoration:none}h1,h3,h4{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1,h3{margin-top:20px;margin-bottom:10px}h4{margin-top:10px;margin-bottom:10px}h1{font-size:36px}h3{font-size:24px}h4{font-size:18px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}.text-muted{color:#999}ul{margin-top:0;margin-bottom:10px}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.lead{font-size:21px}.container{width:750px}}@media (min-width:992px){.container{width:970px}}.row{margin-right:-15px;margin-left:-15px}.col-lg-6{position:relative;min-height:1px;padding-right:15px;padding-left:15px}@media (min-width:1200px){.container{width:1170px}.col-lg-6{float:left}.col-lg-6{width:50%}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;background-image:none;border:1px solid transparent;border-radius:4px}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a{color:#fff;background-color:#428bca}.jumbotron{padding:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.container .jumbotron{border-radius:6px}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1{font-size:63px}}.container:after,.container:before,.nav:after,.nav:before,.row:after,.row:before{display:table;content:" "}.container:after,.nav:after,.row:after{clear:both}.pull-right{float:right!important}@-ms-viewport{width:device-width}

================================================
FILE: test/expected/generate-ignore.css
================================================
body{padding-top:20px;padding-bottom:20px}.marketing{padding-left:15px;padding-right:15px}.header h3{margin-top:0;margin-bottom:0;line-height:40px;padding-bottom:19px}.marketing{margin:40px 0}html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a{background:0 0}h1{margin:.67em 0;font-size:2em}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}a{color:#428bca;text-decoration:none}h1,h3,h4{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1,h3{margin-top:20px;margin-bottom:10px}h4{margin-top:10px;margin-bottom:10px}h1{font-size:36px}h3{font-size:24px}h4{font-size:18px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}.text-muted{color:#999}ul{margin-top:0;margin-bottom:10px}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-6{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;background-image:none;border:1px solid transparent;border-radius:4px}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a{color:#fff;background-color:#428bca}.container:after,.container:before,.nav:after,.nav:before,.row:after,.row:before{display:table;content:" "}.container:after,.nav:after,.row:after{clear:both}.pull-right{float:right!important}@-ms-viewport{width:device-width}

================================================
FILE: test/expected/generate-ignorefont.css
================================================
body{font-family:'PT Sans',sans-serif}

================================================
FILE: test/expected/generate-ignorefont.html
================================================
<!doctype html>
<html>
<head lang="en">
    <meta charset="utf-8">
    <title>Font-face</title>
    <style>
        body{font-family:'PT Sans',sans-serif}
    </style>
    <link rel="stylesheet" href="styles/font.css" media="print" onload="this.media='all'">
</head>
<body>
    <h1>should be styled by @font-face</h1>

<noscript><link rel="stylesheet" href="styles/font.css"></noscript>
</body>
</html>


================================================
FILE: test/expected/generate-image-absolute.css
================================================
.header{background:url('/images/critical.png')}

================================================
FILE: test/expected/generate-image-big.css
================================================
.header{background:url('images/critical-big.png')}

================================================
FILE: test/expected/generate-image-relative-subfolder.css
================================================
.header{background:url('../../images/critical.png')}

================================================
FILE: test/expected/generate-image-relative.css
================================================
.header{background:url('../images/critical.png')}

================================================
FILE: test/expected/generate-image-skip.css
================================================
.header{background:url('images/critical.png')}

================================================
FILE: test/expected/generate-image.css
================================================
.header{background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAL4AAABZCAYAAACANZ6nAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpGMkQ4NDQwMzFFQjkxMUU0OUEwRERGQUVGMzBBOUUwRCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpGMkQ4NDQwNDFFQjkxMUU0OUEwRERGQUVGMzBBOUUwRCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkYyRDg0NDAxMUVCOTExRTQ5QTBEREZBRUYzMEE5RTBEIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkYyRDg0NDAyMUVCOTExRTQ5QTBEREZBRUYzMEE5RTBEIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+JjHrHwAAGZpJREFUeNrsXQl4VEW2Pt2drUnIvrGJEFRWEXfBZ8SBAcFxGXGQ587oKIPoOCwqPPVTdPR7jogPWXRQBkdwgXFBRlBRHEEJWxSUZACTgCQhEEI2Qvb0q//ec9O3L7c7vZIAdb7vfEnf/Vb9deo/p07VtTgcDpIi5UwTqywCKRL4UqRI4EuRIoEvRYoEvhQpEvhSpJyCEqb/UVlZSdu2bSObzebrdexCY4TaZJGeIM1CjwutCakFs1rJbref2QXd3EyJiYl07rnn+gb8nTt30ogRI3y511lCLxF6jtAUoeFC5cCAUyxCm4RWCN0rNJv/tgT9RhYL9ezZU2kAZ+rYTE1NDY0ePZqWLl3qG/B9kHihNwq9iK/RILRRaL3Euin4E4X+F2uu0JVCDwa16w4Lo06dOp3RwIdERkb6TnW8lHShfxSaBnbEFh6WPoorWYqrONgoVHP59BM6TejfhP5HFk8H4PheCHj8JLZg5UIjGPCHhBayxZfgdwU9jEIXoV2Z9lSxT/QHoXO43IJi8eGbyRSU0AD/t2zxjwrtxED/h9DvhdbJ4nQrNrb0v2VfCI5uZ6G/E/pyMPyi+Ph4heY0NTXJ0vYmGODDsd3ZkYXFimTQvyp0kwR92wEHoT8JnSu0hC3+MQ4KDAm4VQlLn5CQQC0tLbKkQwD8C7mHaGF687HQX2QR+iQwGu+zhbdyg7gwGNYeVEcCPzRU52x20sBZDzO98STJQq8UmsTW7TuhBzw4zIh4xDGN2iC01M2xvYVeJjSaIyPfsON4qgjCmflCM7g8u7Eh8avXBODT0tIktw8R8O0M4CYGfjGpgzLuBI7cw0ITSA11hnMjWCR0l+HYvkIf4MrXrn+F0P8zaSigWnfrrCUawKV8bNkpVO77hZ4ntJYbcJy/wE9JSaHw8HDJ7UNEdazcSBwctWls4/ibhcay9YYjV87bbyE1EqRveOP4b4Xu2Bi+hl60bY1MGWoY7IiY/OYUK/cGXdTHRn6OpyBuD+BjxFJK6Di+XjyFLDG41YN7BCsfa2WLlsy9QavBYq01HHucKUCygWp1ZtBYdMeiAfRkh/t0L/cTrD1GbCXNaacKMIlguPOyHLxfkxZyH8prMRzb5OZYCx97Rnl2sPZxcXHS2ncg4MPR/A9THQ3YLUxVEAXSD9gcYr4bw8dpinN/1lEkYoewlDmx/lj0Arle0K/TSqS173jAh3zAwE1gECcwH19uYrWXcQOI52PjGeQrTHjx29yw4hjw8Rxd+pe09lJCEdXxVeB8YrBmMHN1gB6ZicdMjkVo9K9CL2C+f5DBbJbwlif0BVIHfeK4B/lRWnspHQX4xMDd4uWxaBAbvTwW0Z/1Z2qFSWvfsamOFM/it6lOTk6W1r4DWHw548p38RT18iiYYSWtffsAH6FDxON7Cf2z7DH8khZ2zGv94fYyA7P9LD7MjZ0rT4r/Vr+ZfJi7oHF7Cfr2pzrS2gdm9f3i9lLaD/jg9UgTyCc528pf0CeyetUAZCTHK8EgKMaBkO5SEWzgO5jmIMNyiSxrv2WU0BvIyyVHvLT26IExtoEkwCp/fAiDYHAQYWZfuBXypZBlW9kOZYqpnM8LfU/onaGgKw5p6QMWG3kZ0vTC2iOR71FSx0Fy2Shhttc7Qkf4+XzXk5p28nfyPnKH4xYL3cON+mRLNDf6xFBz/FYZMGAAXXzxxUpsuT3jy8hLz8vLo6ysLKqvD9pKJzAOSdyNosEjZeIIuSbQ+SpeG442rD0m72DOM7JTkdKBkWxE3VKF3ip0vNAXuWH4IsNIXUXjalLTTY7owD2BGxvSRPbqzknihoZ7DyV1ht7JFIcucBB64Pfr14+mTJmiRBs6wqAKGmBmZiatX7+eNm/eTA0NDf5eKoGBdRaDv17XneNFi9jKHg7Vu2CNmNjYWHdTCvuTOo0Rs9f+KfRJUvOjGrmRYmI7JujMEFpA6iQgb+UVpjibdaBXbIvQZ7mhlRiAj3K4j0H/6ukc1VFk5MiRsK6Wmpoau7BMHSHSUxsfH988YcIEGj58uNIA0AM0NvqUzvMrUmeBIQ/oQ1KT6Jp0Vg85RUO4opFXtIZCsIIcQI+J5G5CmM8w6D9ly663dJXsg8Uw+Kcz9fGWe2OG3SyT7XXknOZpNgNvNSud1sDv1q0bZWRkUF1dXazVakU2ZQY5ZxedbLFylORuYeU3g+pgEvYdd9xB/fv3pyVLlnhr/e8idYxinhugNLO1A9i/4a4fjtUbPjqCbUpERIS7XnQgOWedPe2he1/OAMY85Wu4EVv53HjuKbpyw0nid0YiIKaDZpKaMr6W6csopmgJfO3hzKtRFut533XcGL8kNepnlCF8Xm9uREhc/IzcTxvF7Lpf8/siqIKU9q+EbqMA518EBHxwT3a6bPwyvTtAY7Zrzwagl5WV0eDBg2nixInKmoq1tR6DHePZQfK2q65hh+5WjiS8GdQXsdvdAX8YP2cWg8edlHGDHM40RatzZMP2IXWm3P2kLh1D3JAB/GtJXexqAwN/kNC3DNf+E/9dowP+S6QumXKXAfiR7GtMNgmo4LjfC/3asP1mrod0k/daSOqc7sZ2Az5m+bMlrWWL117rZ9rY8jmMz4hVoC+99FLasmULZWe7xckFDISX/bj3u0KnML/9Llgv5MGp7cd/c73oZWaZOIHaxPbZbEUX8TYNrFodalZ1h9A7GC/g+N3YD9jCFr+VZvJf4zM9z+WDiUT/y2WEHudeoTdxzzOMnEsqwsqv4Mb0Ovsy9ew8w2eZxD7Wc+0C/JKSEjpw4AB16dKlUoD/Rp3j156y38xJzM3NpV27dnmKsoAKrNLRhih2EstMqISFeX61rrIx+ea/uRsONd2L479VAV5nK9OeQ21EoODgvs2/pzLwv+LyaitShYWFH+T/b+XzNIF/grg7VpSbxg0B8ghfb77uXOJgwjHuPW7hnqv+pAMfDuP27dtp3LhxzYJT53XUIXWEOdesWeMpxNmXAbxHF9G5nZzrgi43WLHfcWSjka39QbZAsGiDGVAnI3QX6GIB89oA/Qk2RIcZbyf338g06xMD6DVBb4OVNgaQc30hJEA+YYgaabKB/4ICJZKfq04HHNXBIvzC2gvMWyKpYwxs1Zs5PqmpqR5fg2mDJtcz1/2UHdcrdRwU6/ikcZd9JVfsQt63m/2cUAPfYmgA/spxPxucL/e+SGetzWQj+wXNup4yV4fP8zhoksiNoq8XvUxogd+1a1fq27cvLGksd1kdIapzD+lmfmmDahhv+Oqrr9ydm2qwLt+QcyW3VQbLqp8wv9VgMYs4cmEJMeVrDrTidX5RqCVFY8buiIObCNA17INc4eY9A0pcCgj4yA3nwRUbRwkyOoDFjzbb2Ma6klE66xfG4G52U2Elht7lZ51jfZy79TAKcB4waKMH6nhY12DbEm3CPxK3KtvJIPnaO8Hv+IgBv5yNz2Eu08HM/a3tBnxDVEejGO0d1Wlx10g9uSvkXOEtkx2ozV7eFxXRhcN6EeTMtQ+MxIteCj5JVFSU2e7vdTQCDd1Tshuc0pEcVXmtHeql1IdGqtXjUwxsOLlzDfsbgtHbBRzVKSwspPT09I4Q1dHoRYGZ5cRzehBELZLZubUyh/dW0nTlqH0lJiiLW3kYcNMW1UUvi5j7Sg9O+9XcC/0QpDK2+kg1cN9R7Bu586+eYJr4OKkpIhrnXxMKvzQoUZ0dO3bgo2OI6uztyBMl9u3b53E3qYM037GDepMPPL0X+wTEFbY/WM98/Phxd3SnhK33/zAPziLzL6sgOQ0f8FjnQw/WVs+ohW/jvDznI34OZGxeYuL4P8ARtE/JuUhYE1v+gVwferw+qGt4jnYBPsKEGBUVoLeJCurdgSz+cWMDHTVqFO3evRvpFWbn/sTOVCrze9C1YdT2kidD2D/IITUv5mxSh+CDIviKH57dzQfdMCh0OamDOv8mddR0E/c2GIj7PUecwO1nuonOOLyI3ri4ShxxGcwARK9TxT2Qu3PQKBcxwDEohRSLbUzR7uGoGajlkzojhAEXJAi+yj0Afnfne2of0gh3g19HyIEvKA51794dlimOW3bfDsDxr2UguFCGXr16Ue/evSknJ8fs3DquDFilv3EFPciVutPN/TIYWIv59/VsnYLmQCI5rby8XAnFmiSqoXFjPAEjqLexw2eUnfweWw3Opl0HHnfOvnas1UBrFvK7wr9Yzb1dJjknKZnhair7PxPpxLSOfG4U23XbpjOeEKufo9uOZeMnsQ+Aff3JuZS8dk97yIEPK4QK4XVeIg2F2l5i6sXC0mPeQE5OrjujgFgnBlFGk5qf8gZ3wQPYapXyiYnMV/uy44iCv4od3FeC7hmWllJ8fBzZbGFmVh9riyJHaB47sH34/QvZ+n9t4vg2cg+Q5IH+fEFqLky+ib8CoP+KyybewMNncfhyk0kjhWVHxuivuWes5eNWk2vqM/FzDWXK2Z/LfTs3BtC8Mga+PsKGyM9Rcg5Chhb4HC2BRcgzeN3tAfgWI81RG6caWZw8eQaNHXsrLVw4h7Zs+c4MSG+yhYzggoS1uZIbQ5iOW+bzvlquSHDXBRTk7Ez1K4YA/xFKSkoU1NJttuZW8n7QDGX0fhvH/MRKHuhLlgnFWOHhnGamjt6umFdgsPZ6MbtPNnlO2HMP/J49ewntTfv353t1clFRkTLjqU+fPlWC7txo6RjebR3ofktLMzU3NypASU8/iy64YKgATiRddll3uuKKK+nbb7+mp56aQQcOuPiisI7IX5/A3TMs/3ruDWyGaMY5TKsa+ByXzxENG5ZJnTvH0Nq1/q1nGx+fSM8/P1epEzTc1157mX74YTslJCTKVdSCHdXp0aM7LVnyPk2cOJ727cvz6gLr1q0D8FETtfoK0dqAWSVpzprZPsxlcThaTLarYUmzgShne3OI/Q7hECL+HS2eawClpfWg6OjOynUbGxsE369Xjs/MHEGLF79LmzZtoM8//5f4+432PLVs+QcxhRnBXaj2MetYDn3iQb41cFNKSUmjJ5/8i3CmrxMW20pTp06mjz5636dKiY2No/nzl9DQoZlUVVVBERGR9Mwzc2nmzCn0008/SPAHIxKiL8Cqqhphxavo8OESuueeW6igoBX80cwLozhyk613UiZP/iNlZPRxyXV3OJoJGA0LU7trlz6vuUmA3yLUuM+h5PejYaiNQ09ZWhQrDq5rHLtQ76XSLoCkZ89zFbXboxUfBOcZgYIGhHz3zp1jqb6+QfQA60VDmE+bN39rPDaJIwop5MxULNJxfkUuuugyGjPmBrrkkito4MDzlXkAoCoxMZ1pxgyA36V3Hit0DDm/GjOHr6mAftGif4ieaZigOIdbjURUlF15Fwl+z+HfESNG0Ouvv+4b8Kura0Wh7qPExEQ6dKiY7r67FfxugY8KeO+91UoFawMuAPTRo4cUy5uWdpYCPKe1t9HBg/sUUCYkOL/fpFnz4uJ9gs+mUadOMa3WHZVfV1crGmQRdet2NjcYR+u+qqpyOnasSlCaHhQZGaWcC+uu3tfilZ+CBgDZuHE9vfPOUvryy7WKxUaDctdrJSUlU79+A+mWW24TBX6tuLddPEe1qICaVsCiIdrtkXTzzaNp166dHoFvBnr9cwL8MBqPP/6guNYOhQ5J8AcJ+Dk5+5ljJgjwH6S77rpZ0J58Dfh2HfDfQEU98MCf6P77HxZAL1OsuB7gALMa9TE6bWqEQt8gNPDjPADeSHdAVbAPFW+Mymj3QiNSewbfB07VBmARDSBOuRboDxr1/v0FrfQIzuXPP+8RoCyhO+/8g6CGZwkjkaycW11d1dpbGa+Lsvzss9U0e/ZMca6S03adDvgonTmiLIsWLXpbgH7oCaDXXwu9lGr5HxRGCuBPMqWGEvh+AF9LPktOTqUff/xeWLTR0aKwp3HcF8D//uKLL1++YMGbgj/HUU1NNZ0uS+2gPAByNGoADJNY9D47wI2ywV/0Khhg0s7xdE27vZNo8ERPPDGdPvjgPcS9f8PAh9O84KWXFhSNG3e76A2LPOYVuYJ/iqgfSXv8Ab4xnJnCobsmFP7Ro0fCBg26oPj88y/6tr6+tkdGxnnpgnJYBSiip09/Ak5jbk3NMdF/W8aQ85tXxA0EkRCMGl6vM9EWDvl9yvcapouS2NiB/EQXJ2/UPSc413ccPkzVnRfB8eB9pA5AhZNz0SsHx6Rt7KS26EKftRwXRpz4Qu1eAsC4V7FwKr8Uv64S79dTF6YMF/jG++5g4GJSegtHs3DNf/M7jOHfDvWSliZRbqtFI0qfPXvOpUlJqecLCjlAALjhyJHSBkGNmkeOHDtE9AaDRLk36srwZzJMZcSt4EuB9rzwwnzh9D5K27dnKVRNgt/PqA6D7S1XR7Q5S4B+6L33Tr5w/Pg7h1RUlCtOmwDEJMFn54iKwKAFMuiME81v4ljwshPDjUoOBkC/1LAvj4GPCdATDfveYxDMphMTnh4idTDp76TmprhEFpmivW3YDicVsUbM/nnKsO8bATGsFDBNvJ/xG7ov8nvNJ+ckbU1wrRy+l83wzufU1dVdbbM1vjF16iwFpFglbceObUeXLVtaL3ye+wSgJxl6jnfJZA4vjoHP06lTtHCcnxZUcwLqQ8nklOD3TrzKaYaDp3JsqwJ6dPU8Yqt9b9ZXntPWORYP23w9jwK4lynb8PD8nsoDxWUBRVKjWmp5ogRVsLb4NAyCY+FEw3+YPHm6ck1/fBtp8VVBsj/WXmlgPhkZFha2Izo6xpaVtbE4PDw8trq6ujkuLq58+PARxdXVx7JFYTeJSviErZ/WTWtroID8ryBn1p2NKQa2F+jvxV27lneRxWFELT4axTFzyOd87QZdqHUP/36fLX6LjmqUsl+ykpyfH8V7l/NvhFpW6e6l+DD8/wY+Rstsw7V3MM3SckmaGOjaBJZK7p3CdM9RL8oSfD5PAPWfW7ZsSi4s/KVLdHS0Iz9/T1VFxVGbwP5WcUxXUZa1ujLc1Bb4KysrBK8dQ5s3b6R16xBdi6P2n+9/isXx9c6t5pChSx079qpowT+n653bhx6avmzGjCepoqJSCeG1MdHjjBY1FBmlDG4tX76EHn30oUxhoa/XnFur1TZ/5cq1RQMHDubomNWna8O5Xbx4Hr377hIlKiWd27adW6u7wkTKsbBI9Nxzs7QQXDhbXiWWP2/eX+nee2+lnTuzlcKW3NJzFKaw8AA9/PB9NGvWn0FLYvRlibDus8/OVKgLytxXyoKiV/OR5CLWfnN8DfRxcfE0bdok+vjjlW4rFHkoTz/9GEVE2HhEVYoZ7cdA07x5L9KKFW9Tfb3pfADKzt4qnNTblTCpP+CXoA8A+CroIwToE2jq1En04Ydt55js3p1Dc+c+T6mpybLwTYxDWlq6oCBLhZFY1ebxSJcA+JuaAP4Y6ayeLOBjkVJ8hGDq1Ad8SqyaP38uvfTSc6KSU1pHUdEDYCI6okGualF6FESH1MQz1/3Yp0Y7XBXb0ChPvJ4aacJ5ZvfCM6jPYX4v9TnMn9HsOTzfyyY0vDWhDqBfuXIZPf74w16XpQr+2xTqgjQQGBOzdzaWjfw8VgBRHXSzsPSrVq30+UKvvPKiYuEeeWSmsjpAdXW1MqqZkJDgYrlQSUjgwohoTIzRqlnEviNK4wO4NL8BIGpsbBKO9FFKSkpxqWRcD04N+HFiYtIJ96qoqFDOxzX1+wAY3AuWNTo6qnXYX019aMHgnXI9NSfe0XoO3g15QcjT0fs1uBfeuampXnlnBAZWrFhGjz32kM/frsJcAVj+hQvfUiaheF7iHOMBdsUoSPET+NnZW/wCvSYLFsylL75Yq4AWA12oMADVNUnNqmR/YuQRaQGuYLQo+UEJCUkuS2RrKx8fOVJK6eldXNID0MMgZQLzU1NT0wzAtwlwlyr3BIj1AMQ23AuOOQaStPO0nB88I66nn/mEczBqihAirLkR+NiOd05JSVWul5e3x+8PtgH8N9xwjRKxaesaeM+KijJhSKL1ax1J8Rb4vtBXs40Y1MrNdZ24g9wTXwUAdyfl5WVu9wGs7qSo6IDpdiSEuROEFt0/o/vzSkqK23pFr5BZWPiLot4KGjDmFUvw+xHVcSMwOVq8DKCX/WpgYteFYrSyDVhA+fLz85WeCBRNSuDARwwOJg6xfIyQYsmHWFl8fgnAjrV4tLVjMIpdEayLA/yYDgrKJcEfOPAhBQx8WCesNHC5LD6/BFMazyZ1GRb0nL9QkCfoww8pKChQ/CJEtNAA5Mi6/8DH3NJa9gswKRupt/1kEfok+KDCeHJdjSEkS4pjOZW9e/dScXExVVVVKb8tMubpF/BBdTYwxWliBw0LAWEaXQIFuHrtaU5tEJDHxHUE9KPZ2iNtAV9VzAnVjRENwro8WD4Rn0OS1McpvkZ1sPgPFi3qzbw0jIGfyQ2jgeTwrV60TFAMa8cx4OsY9Fi5YcXJehBYfJlP5T/wAWysg4hVsc5lx6yKr9Ndgt6tNHFZ2bh31BZ9LTtpLVCCPiDgEwMdM66wmBIW9oznitXoj5QTJYIpDnwkTE9E4s4xWSynFvA1C/YJc/5BbP3T2aJJ0+LK71vYsuNTQ5jEUtoeD4LwJka3T+eBLbyfhw/8uVaMvguEA7Rt2zZ/nCBUcBSdnG8qnYo8H7y+uT0fAmHN031OLpx5rAmFDxL6BHwpUs4UkSFIKRL4UqRI4EuRIoEvRYoEvhQpEvhSpEjgS5EigS9FigS+FCkS+FKktLf8vwADAPYLv1CPrtvnAAAAAElFTkSuQmCC')}

================================================
FILE: test/expected/generateInline-external-extract.html
================================================
<!doctype html>
<html class="no-js">
    <head>
        <meta charset="utf-8">
        <title>critical css test</title>
        <meta name="description" content="">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <!-- Place favicon.ico and apple-touch-icon.png in the root directory -->

        <!-- build:css styles/main.css -->
        <style>body{padding-top:20px;padding-bottom:20px}.header,.marketing{padding-left:15px;padding-right:15px}.header{border-bottom:1px solid #e5e5e5}.header h3{margin-top:0;margin-bottom:0;line-height:40px;padding-bottom:19px}.jumbotron{text-align:center;border-bottom:1px solid #e5e5e5}.jumbotron .btn{font-size:21px;padding:14px 24px}.marketing{margin:40px 0}@media screen and (min-width:768px){.container{max-width:730px}.header,.marketing{padding-left:0;padding-right:0}.h
Download .txt
gitextract_6mprb1p7/

├── .editorconfig
├── .gitattributes
├── .github/
│   └── workflows/
│       ├── docker.yml
│       └── test.yml
├── .gitignore
├── .npmrc
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Dockerfile
├── README.md
├── cli.js
├── index.js
├── license
├── package.json
├── src/
│   ├── array.js
│   ├── config.js
│   ├── core.js
│   ├── errors.js
│   └── file.js
└── test/
    ├── array.test.js
    ├── blackbox.test.js
    ├── cli.test.js
    ├── config.test.js
    ├── core.test.js
    ├── expected/
    │   ├── generate-adaptive-useragent.css
    │   ├── generate-adaptive.css
    │   ├── generate-default-nostyle.css
    │   ├── generate-default-nostyle.html
    │   ├── generate-default.css
    │   ├── generate-ignore.css
    │   ├── generate-ignorefont.css
    │   ├── generate-ignorefont.html
    │   ├── generate-image-absolute.css
    │   ├── generate-image-big.css
    │   ├── generate-image-relative-subfolder.css
    │   ├── generate-image-relative.css
    │   ├── generate-image-skip.css
    │   ├── generate-image.css
    │   ├── generateInline-external-extract.html
    │   ├── generateInline-external-extract2.html
    │   ├── generateInline-external-minified.html
    │   ├── generateInline-external-minified2.html
    │   ├── generateInline-extract.html
    │   ├── generateInline-svg.html
    │   ├── generateInline.html
    │   ├── ignore.css
    │   ├── inline-image.html
    │   ├── inline-minified.html
    │   ├── inline.html
    │   ├── issue-192.css
    │   ├── issue-395.css
    │   ├── issue-566.css
    │   ├── main.css
    │   ├── path-prefix.css
    │   └── streams-default.html
    ├── file.test.js
    ├── fixtures/
    │   ├── 403-css.html
    │   ├── 404-css.html
    │   ├── error.html
    │   ├── folder/
    │   │   ├── generate-default.html
    │   │   ├── generate-image.html
    │   │   ├── index.html
    │   │   ├── relative-different.html
    │   │   ├── relative.html
    │   │   ├── styles/
    │   │   │   └── issue-566.css
    │   │   └── subfolder/
    │   │       ├── generate-image-absolute.html
    │   │       ├── head.html
    │   │       ├── issue-216.css
    │   │       └── relative.html
    │   ├── generate-adaptive-base64.html
    │   ├── generate-adaptive-inline.html
    │   ├── generate-adaptive.html
    │   ├── generate-default-nostyle.html
    │   ├── generate-default-querystring.html
    │   ├── generate-default.html
    │   ├── generate-ignorefont.html
    │   ├── generate-image.html
    │   ├── generateInline-external.html
    │   ├── generateInline-external2.html
    │   ├── generateInline-svg.html
    │   ├── generateInline.html
    │   ├── head.html
    │   ├── ignore.html
    │   ├── ignoreInlinedStyles.html
    │   ├── include.html
    │   ├── inline-image.html
    │   ├── inline.html
    │   ├── issue-192.html
    │   ├── issue-304-nostyle.html
    │   ├── issue-304.html
    │   ├── issue-314.html
    │   ├── issue-395.html
    │   ├── issue-415.html
    │   ├── issue-562.html
    │   ├── issue-566.html
    │   ├── media-attr.html
    │   ├── path-prefix.html
    │   ├── preload.html
    │   ├── print.html
    │   ├── relative-different.html
    │   ├── remote-different.html
    │   ├── streams-default.html
    │   ├── styles/
    │   │   ├── adaptive.css
    │   │   ├── bootstrap.css
    │   │   ├── critical-image-pregenerated.css
    │   │   ├── critical-pregenerated.css
    │   │   ├── font.css
    │   │   ├── ignore.css
    │   │   ├── image-absolute.css
    │   │   ├── image-big.css
    │   │   ├── image-relative.css
    │   │   ├── include.css
    │   │   ├── issue-192.css
    │   │   ├── issue-304.css
    │   │   ├── issue-415.css
    │   │   ├── issue-562.css
    │   │   ├── main.css
    │   │   ├── media-attr.css
    │   │   ├── path-prefix.css
    │   │   ├── print.css
    │   │   └── some/
    │   │       └── path/
    │   │           └── image.css
    │   └── useragent/
    │       ├── generate-default-useragent.html
    │       └── styles/
    │           ├── bootstrap.css
    │           └── main.css
    ├── helper/
    │   └── index.js
    └── index.test.js
Download .txt
SYMBOL INDEX (62 symbols across 9 files)

FILE: cli.js
  function showError (line 190) | function showError(err) {
  function run (line 197) | function run(data) {

FILE: index.js
  function generate (line 17) | async function generate(params, cb) {
  function stream (line 59) | function stream(params) {

FILE: src/array.js
  function mapAsync (line 1) | async function mapAsync(array = [], callback = (a) => a) {
  function forEachAsync (line 11) | async function forEachAsync(array = [], callback = () => {}) {
  function filterAsync (line 17) | async function filterAsync(array = [], filter = (a) => a) {
  function reduceAsync (line 29) | async function reduceAsync(initial, array = [], reducer = (r) => r) {

FILE: src/config.js
  constant DEFAULT (line 9) | const DEFAULT = {
  function getOptions (line 79) | async function getOptions(options = {}) {

FILE: src/core.js
  function combineCss (line 29) | function combineCss(cssArray) {
  function callPenthouse (line 43) | function callPenthouse(document, options) {
  function create (line 80) | async function create(options = {}) {

FILE: src/errors.js
  class FileNotFoundError (line 5) | class FileNotFoundError extends Error {
    method constructor (line 6) | constructor(file = '', paths = [], ...params) {
  class NoCssError (line 23) | class NoCssError extends Error {
    method constructor (line 24) | constructor(...params) {
  class ConfigError (line 37) | class ConfigError extends Error {
    method constructor (line 38) | constructor(msg, ...params) {

FILE: src/file.js
  constant BASE_WARNING (line 30) | const BASE_WARNING = `${pico.yellow(
  function outputFileAsync (line 42) | async function outputFileAsync(file, data) {
  function normalizePath (line 57) | function normalizePath(str) {
  function isRemote (line 66) | function isRemote(href) {
  function urlParse (line 75) | function urlParse(str = '') {
  function getFileUri (line 92) | function getFileUri(file) {
  function urlResolve (line 108) | function urlResolve(from = '', to = '') {
  function isFilePath (line 122) | function isFilePath(href) {
  function isAbsolute (line 126) | function isAbsolute(href) {
  function isRelative (line 135) | function isRelative(href) {
  function isVinyl (line 144) | function isVinyl(file) {
  function fileExists (line 158) | async function fileExists(href, options = {}) {
  function joinPath (line 207) | function joinPath(base, part) {
  function resolve (line 226) | async function resolve(href, search = [], options = {}) {
  function glob (line 249) | function glob(pattern, {base} = {}) {
  function rebaseAssets (line 278) | async function rebaseAssets(css, from, to, options = {}) {
  function fetch (line 350) | async function fetch(uri, options = {}, secure = true) {
  function getStylesheetObjects (line 407) | function getStylesheetObjects(file, options) {
  function getStylesheetHrefs (line 473) | function getStylesheetHrefs(file, options) {
  function getStylesheetsMedia (line 483) | function getStylesheetsMedia(file, options) {
  function getAssets (line 492) | function getAssets(file) {
  function getDocumentPath (line 506) | async function getDocumentPath(file, options = {}) {
  function getRemoteStylesheetPath (line 577) | function getRemoteStylesheetPath(fileObj, documentObj, filename) {
  function getStylesheetPath (line 600) | function getStylesheetPath(document, file, options = {}) {
  function getAssetPaths (line 682) | async function getAssetPaths(document, file, options = {}, strict = true) {
  function vinylize (line 772) | async function vinylize(src, options = {}) {
  function getStylesheet (line 825) | async function getStylesheet(document, filepath, options = {}) {
  function getCss (line 948) | async function getCss(document, options = {}) {
  function preparePenthouseData (line 991) | async function preparePenthouseData(document) {
  function getDocument (line 1042) | async function getDocument(filepath, options = {}) {
  function getDocumentFromSource (line 1084) | async function getDocumentFromSource(html, options = {}) {

FILE: test/blackbox.test.js
  constant FIXTURES_DIR (line 21) | const FIXTURES_DIR = path.join(__dirname, '/fixtures/');
  function assertCritical (line 23) | function assertCritical(target, expected, done, skipTarget) {
  method first (line 609) | first(cb) {
  method second (line 619) | second(cb) {
  method first (line 1281) | first(cb) {
  method second (line 1291) | second(cb) {

FILE: test/helper/index.js
  function getFile (line 11) | function getFile(file) {
  function getPkg (line 20) | function getPkg() {
  function readAndRemove (line 25) | function readAndRemove(file) {
  function read (line 34) | function read(file) {
  function getVinyl (line 40) | function getVinyl(...args) {
  function strip (line 58) | function strip(string) {
Condensed preview — 126 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (640K chars).
[
  {
    "path": ".editorconfig",
    "chars": 279,
    "preview": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ni"
  },
  {
    "path": ".gitattributes",
    "chars": 43,
    "preview": "# Enforce Unix newlines\n* text=auto eol=lf\n"
  },
  {
    "path": ".github/workflows/docker.yml",
    "chars": 1544,
    "preview": "# This workflow uses actions that are not certified by GitHub.\n# They are provided by a third-party and are governed by\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 1263,
    "preview": "name: Tests\n\non: [push, pull_request]\n\nenv:\n  CI: true\n\njobs:\n  run:\n    name: Node ${{ matrix.node }} on ${{ matrix.os "
  },
  {
    "path": ".gitignore",
    "chars": 204,
    "preview": "/coverage/\n/node_modules/\n/test/fixture/bower_components/bootstrap/dist/css/*\n/test/fixture/test-*\n/test/fixture/tmp-*\n/"
  },
  {
    "path": ".npmrc",
    "chars": 19,
    "preview": "lockfile-version=2\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 10878,
    "preview": "# v2.0.0 / 2020-06-16\n\n- Drop support for Node.js < 10\n- Bump dependencies\n- Use Jest for testing\n- Drop `include` and `"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 1138,
    "preview": "Critical is an open source project. It is licensed using the\n[Apache Software License 2.0](http://www.apache.org/license"
  },
  {
    "path": "Dockerfile",
    "chars": 972,
    "preview": "FROM node:20-slim\n\nARG CRITICAL_VERSION=5.0.4\n\nARG PACKAGES=\"\\\n  libx11-6\\\n  libx11-xcb1\\\n  libxcomposite1\\\n  libxcursor"
  },
  {
    "path": "README.md",
    "chars": 24734,
    "preview": "[![NPM version][npm-image]][npm-url] [![Build Status][ci-image]][ci-url] [![Coverage][coveralls-image]][coveralls-url]\n\n"
  },
  {
    "path": "cli.js",
    "chars": 6801,
    "preview": "#!/usr/bin/env node\nimport os from 'node:os';\nimport process from 'node:process';\nimport stdin from 'get-stdin';\nimport "
  },
  {
    "path": "index.js",
    "chars": 2271,
    "preview": "import path from 'node:path';\nimport {Buffer} from 'node:buffer';\nimport process from 'node:process';\nimport through2 fr"
  },
  {
    "path": "license",
    "chars": 10829,
    "preview": "                              Apache License\n                        Version 2.0, January 2004\n                     http"
  },
  {
    "path": "package.json",
    "chars": 2732,
    "preview": "{\n  \"name\": \"critical\",\n  \"version\": \"7.2.1\",\n  \"description\": \"Extract & Inline Critical-path CSS from HTML\",\n  \"author"
  },
  {
    "path": "src/array.js",
    "chars": 1041,
    "preview": "export async function mapAsync(array = [], callback = (a) => a) {\n  const result = [];\n  for (const index of array.keys("
  },
  {
    "path": "src/config.js",
    "chars": 4443,
    "preview": "import process from 'node:process';\nimport Joi from 'joi';\nimport debugBase from 'debug';\nimport {traverse, STOP} from '"
  },
  {
    "path": "src/core.js",
    "chars": 7030,
    "preview": "import {EOL} from 'node:os';\nimport {Buffer} from 'node:buffer';\nimport process from 'node:process';\nimport path from 'n"
  },
  {
    "path": "src/errors.js",
    "chars": 1228,
    "preview": "import process from 'node:process';\nimport pico from 'picocolors';\nimport {stripIndents, stripIndent} from 'common-tags'"
  },
  {
    "path": "src/file.js",
    "chars": 33977,
    "preview": "/* eslint-disable complexity */\nimport {Buffer} from 'node:buffer';\nimport fs from 'node:fs';\nimport os from 'node:os';\n"
  },
  {
    "path": "test/array.test.js",
    "chars": 2997,
    "preview": "import {mapAsync, reduceAsync, filterAsync, forEachAsync} from '../src/array.js';\n\nconst waitFor = (ms) => new Promise(("
  },
  {
    "path": "test/blackbox.test.js",
    "chars": 39739,
    "preview": "import path from 'node:path';\nimport {createServer} from 'node:http';\nimport {fileURLToPath} from 'node:url';\nimport {Bu"
  },
  {
    "path": "test/cli.test.js",
    "chars": 9129,
    "preview": "import {exec, execFile} from 'node:child_process';\n// import {createRequire} from 'node:module';\nimport path from 'node:"
  },
  {
    "path": "test/config.test.js",
    "chars": 2882,
    "preview": "import {ConfigError} from '../src/errors.js';\nimport {getOptions, DEFAULT} from '../src/config.js';\n\ntest('Throws Config"
  },
  {
    "path": "test/core.test.js",
    "chars": 2423,
    "preview": "import process from 'node:process';\nimport {createServer} from 'node:http';\nimport path from 'node:path';\nimport {fileUR"
  },
  {
    "path": "test/expected/generate-adaptive-useragent.css",
    "chars": 194,
    "preview": "#of{background:teal}#guybrush{color:pink}#threepwood{background:orange;content:'monkey island'}@media screen and (min-wi"
  },
  {
    "path": "test/expected/generate-adaptive.css",
    "chars": 194,
    "preview": "#of{background:teal}#guybrush{color:pink}#threepwood{background:orange;content:'monkey island'}@media screen and (min-wi"
  },
  {
    "path": "test/expected/generate-default-nostyle.css",
    "chars": 2581,
    "preview": "html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a{background:0 0}h1{ma"
  },
  {
    "path": "test/expected/generate-default-nostyle.html",
    "chars": 6530,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/expected/generate-default.css",
    "chars": 3099,
    "preview": "body{padding-top:20px;padding-bottom:20px}.header,.marketing{padding-left:15px;padding-right:15px}.header{border-bottom:"
  },
  {
    "path": "test/expected/generate-ignore.css",
    "chars": 2179,
    "preview": "body{padding-top:20px;padding-bottom:20px}.marketing{padding-left:15px;padding-right:15px}.header h3{margin-top:0;margin"
  },
  {
    "path": "test/expected/generate-ignorefont.css",
    "chars": 38,
    "preview": "body{font-family:'PT Sans',sans-serif}"
  },
  {
    "path": "test/expected/generate-ignorefont.html",
    "chars": 403,
    "preview": "<!doctype html>\n<html>\n<head lang=\"en\">\n    <meta charset=\"utf-8\">\n    <title>Font-face</title>\n    <style>\n        body"
  },
  {
    "path": "test/expected/generate-image-absolute.css",
    "chars": 47,
    "preview": ".header{background:url('/images/critical.png')}"
  },
  {
    "path": "test/expected/generate-image-big.css",
    "chars": 50,
    "preview": ".header{background:url('images/critical-big.png')}"
  },
  {
    "path": "test/expected/generate-image-relative-subfolder.css",
    "chars": 52,
    "preview": ".header{background:url('../../images/critical.png')}"
  },
  {
    "path": "test/expected/generate-image-relative.css",
    "chars": 49,
    "preview": ".header{background:url('../images/critical.png')}"
  },
  {
    "path": "test/expected/generate-image-skip.css",
    "chars": 46,
    "preview": ".header{background:url('images/critical.png')}"
  },
  {
    "path": "test/expected/generate-image.css",
    "chars": 10001,
    "preview": ".header{background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAL4AAABZCAYAAACANZ6nAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZS"
  },
  {
    "path": "test/expected/generateInline-external-extract.html",
    "chars": 5782,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/expected/generateInline-external-extract2.html",
    "chars": 5880,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/expected/generateInline-external-minified.html",
    "chars": 5746,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/expected/generateInline-external-minified2.html",
    "chars": 5862,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/expected/generateInline-extract.html",
    "chars": 5421,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/expected/generateInline-svg.html",
    "chars": 7210,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n<head>\n    <meta charset=\"utf-8\">\n    <title>critical css test</title>\n    <meta na"
  },
  {
    "path": "test/expected/generateInline.html",
    "chars": 5385,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/expected/ignore.css",
    "chars": 38,
    "preview": ".ignored{color:orange}.in{color:green}"
  },
  {
    "path": "test/expected/inline-image.html",
    "chars": 5513,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/expected/inline-minified.html",
    "chars": 6474,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n<head>\n    <meta charset=\"utf-8\">\n    <title>critical css test</title>\n    <meta na"
  },
  {
    "path": "test/expected/inline.html",
    "chars": 7495,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n<head>\n    <meta charset=\"utf-8\">\n    <title>critical css test</title>\n    <meta na"
  },
  {
    "path": "test/expected/issue-192.css",
    "chars": 313,
    "preview": ".deck,.hero-deck,.main-navigation,.search-box{position:static;float:none;width:auto;margin-top:0;background-color:transp"
  },
  {
    "path": "test/expected/issue-395.css",
    "chars": 184,
    "preview": ".relative {\n  background-image: url(\"http://cdn.rcvc.io/image.jpeg\");\n  font-size: 100px;\n}\n.absolute {\n  background-ima"
  },
  {
    "path": "test/expected/issue-566.css",
    "chars": 20,
    "preview": "#element{color:#0ff}"
  },
  {
    "path": "test/expected/main.css",
    "chars": 518,
    "preview": "body{padding-top:20px;padding-bottom:20px}.header,.marketing{padding-left:15px;padding-right:15px}.header{border-bottom:"
  },
  {
    "path": "test/expected/path-prefix.css",
    "chars": 46,
    "preview": ".header{background:url('images/critical.png')}"
  },
  {
    "path": "test/expected/streams-default.html",
    "chars": 2806,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/file.test.js",
    "chars": 24163,
    "preview": "/* eslint-disable no-await-in-loop */\nimport {fileURLToPath} from 'node:url';\nimport {createServer} from 'node:http';\nim"
  },
  {
    "path": "test/fixtures/403-css.html",
    "chars": 202,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Title</title>\n    <link href=\"https://"
  },
  {
    "path": "test/fixtures/404-css.html",
    "chars": 202,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Title</title>\n    <link href=\"https://"
  },
  {
    "path": "test/fixtures/error.html",
    "chars": 226,
    "preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\">\n    <title>Title</title>\n    <link rel=\"stylesheet\" h"
  },
  {
    "path": "test/fixtures/folder/generate-default.html",
    "chars": 4115,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/fixtures/folder/generate-image.html",
    "chars": 439,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/fixtures/folder/index.html",
    "chars": 176,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>TEST</title>\n    <link rel=\"stylesheet\" hr"
  },
  {
    "path": "test/fixtures/folder/relative-different.html",
    "chars": 262,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>TEST</title>\n    <link rel=\"stylesheet\" hr"
  },
  {
    "path": "test/fixtures/folder/relative.html",
    "chars": 234,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>TEST</title>\n  <link rel=\"stylesheet\" href"
  },
  {
    "path": "test/fixtures/folder/styles/issue-566.css",
    "chars": 28,
    "preview": "#element {\n  color: aqua;\n}\n"
  },
  {
    "path": "test/fixtures/folder/subfolder/generate-image-absolute.html",
    "chars": 437,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/fixtures/folder/subfolder/head.html",
    "chars": 176,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>TEST</title>\n    <link rel=\"stylesheet\" hr"
  },
  {
    "path": "test/fixtures/folder/subfolder/issue-216.css",
    "chars": 513,
    "preview": "@font-face {\n  font-family: FontAwesome;\n  src: url(../fonts/fontawesome-webfont.eot?v=4.7.0);\n  src: url(../fonts/fonta"
  },
  {
    "path": "test/fixtures/folder/subfolder/relative.html",
    "chars": 237,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>TEST</title>\n  <link rel=\"stylesheet\" href"
  },
  {
    "path": "test/fixtures/generate-adaptive-base64.html",
    "chars": 899,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/fixtures/generate-adaptive-inline.html",
    "chars": 733,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/fixtures/generate-adaptive.html",
    "chars": 479,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/fixtures/generate-default-nostyle.html",
    "chars": 3925,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/fixtures/generate-default-querystring.html",
    "chars": 4147,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/fixtures/generate-default.html",
    "chars": 4109,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/fixtures/generate-ignorefont.html",
    "chars": 223,
    "preview": "<!doctype html>\n<html>\n<head lang=\"en\">\n    <meta charset=\"utf-8\">\n    <title>Font-face</title>\n    <link rel=\"styleshee"
  },
  {
    "path": "test/fixtures/generate-image.html",
    "chars": 436,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/fixtures/generateInline-external.html",
    "chars": 2175,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/fixtures/generateInline-external2.html",
    "chars": 2233,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/fixtures/generateInline-svg.html",
    "chars": 3987,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n<head>\n    <meta charset=\"utf-8\">\n    <title>critical css test</title>\n    <meta na"
  },
  {
    "path": "test/fixtures/generateInline.html",
    "chars": 2025,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/fixtures/head.html",
    "chars": 233,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>TEST</title>\n    <link rel=\"stylesheet\" hr"
  },
  {
    "path": "test/fixtures/ignore.html",
    "chars": 211,
    "preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\">\n    <title>Ignore</title>\n    <link rel=\"stylesheet\" "
  },
  {
    "path": "test/fixtures/ignoreInlinedStyles.html",
    "chars": 274,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Ignore inlined stylesheet</title>\n    "
  },
  {
    "path": "test/fixtures/include.html",
    "chars": 178,
    "preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\">\n    <title>Title</title>\n    <link rel=\"stylesheet\" h"
  },
  {
    "path": "test/fixtures/inline-image.html",
    "chars": 3779,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/fixtures/inline.html",
    "chars": 3261,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n<head>\n    <meta charset=\"utf-8\">\n    <title>critical css test</title>\n    <meta na"
  },
  {
    "path": "test/fixtures/issue-192.html",
    "chars": 178,
    "preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\">\n    <title>#192</title>\n    <link rel=\"stylesheet\" hr"
  },
  {
    "path": "test/fixtures/issue-304-nostyle.html",
    "chars": 397,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "test/fixtures/issue-304.html",
    "chars": 455,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "test/fixtures/issue-314.html",
    "chars": 545,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>https://status.sommerlaune.com/418</title>\n    <link rel=\"canonical\" href=\"ht"
  },
  {
    "path": "test/fixtures/issue-395.html",
    "chars": 360,
    "preview": "<!DOCTYPE html>\n<html lang=\"en-US\" class=\"no-js\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Absolute path testing"
  },
  {
    "path": "test/fixtures/issue-415.html",
    "chars": 402,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "test/fixtures/issue-562.html",
    "chars": 203,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <link rel=\"stylesheet\" href=\"styles/issue-562.css\" />\n  </head>\n  <body>\n    <div cl"
  },
  {
    "path": "test/fixtures/issue-566.html",
    "chars": 430,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "test/fixtures/media-attr.html",
    "chars": 459,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/fixtures/path-prefix.html",
    "chars": 371,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/fixtures/preload.html",
    "chars": 260,
    "preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\">\n    <title>Title</title>\n    <link rel=\"preload\" href"
  },
  {
    "path": "test/fixtures/print.html",
    "chars": 244,
    "preview": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"utf-8\">\n    <title>Title</title>\n    <link rel=\"stylesheet\" h"
  },
  {
    "path": "test/fixtures/relative-different.html",
    "chars": 174,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <title>TEST</title>\n  <link rel=\"stylesheet\" href=\"st"
  },
  {
    "path": "test/fixtures/remote-different.html",
    "chars": 199,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <title>TEST</title>\n  <link rel=\"stylesheet\" href=\"ht"
  },
  {
    "path": "test/fixtures/streams-default.html",
    "chars": 2021,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/fixtures/styles/adaptive.css",
    "chars": 284,
    "preview": "@media screen and (min-width: 900px) {\n    div  {\n        height: 400px;\n        background: brown;\n    }\n}\n\n#revenge {\n"
  },
  {
    "path": "test/fixtures/styles/bootstrap.css",
    "chars": 121455,
    "preview": "/*!\n * Bootstrap v3.1.1 (http://getbootstrap.com)\n * Copyright 2011-2014 Twitter, Inc.\n * Licensed under MIT (https://gi"
  },
  {
    "path": "test/fixtures/styles/critical-image-pregenerated.css",
    "chars": 2497,
    "preview": "body {\n    padding-top: 20px;\n    padding-bottom: 20px;\n}\n\n.header{\n    padding-left: 15px;\n    padding-right: 15px;\n}\n\n"
  },
  {
    "path": "test/fixtures/styles/critical-pregenerated.css",
    "chars": 4301,
    "preview": "body {\n    padding-top: 20px;\n    padding-bottom: 20px;\n}\n\n\n.header,\n.marketing{\n    padding-left: 15px;\n    padding-rig"
  },
  {
    "path": "test/fixtures/styles/font.css",
    "chars": 5889,
    "preview": "/* cyrillic-ext */\n@font-face {\n    font-family: 'PT Sans';\n    font-style: normal;\n    font-weight: 400;\n    src: local"
  },
  {
    "path": "test/fixtures/styles/ignore.css",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "test/fixtures/styles/image-absolute.css",
    "chars": 68,
    "preview": ".header {\n    background: transparent url('/images/critical.png');\n}"
  },
  {
    "path": "test/fixtures/styles/image-big.css",
    "chars": 74,
    "preview": ".header {\n    background: transparent url('../images/critical-big.png');\n}"
  },
  {
    "path": "test/fixtures/styles/image-relative.css",
    "chars": 70,
    "preview": ".header {\n    background: transparent url('../images/critical.png');\n}"
  },
  {
    "path": "test/fixtures/styles/include.css",
    "chars": 28,
    "preview": ".someRule{position:absolute}"
  },
  {
    "path": "test/fixtures/styles/issue-192.css",
    "chars": 382,
    "preview": ".main-navigation,\n.hero-deck,\n.deck,\n.search-box {\n  position: static;\n  float: none;\n  width: auto;\n  margin-top: 0;\n  "
  },
  {
    "path": "test/fixtures/styles/issue-304.css",
    "chars": 131,
    "preview": ".default .sub {\n  background-color: #ffffff;\n}\n\n.a .sub {\n  background-color: #000000;\n}\n\n.b .sub {\n  background-color: "
  },
  {
    "path": "test/fixtures/styles/issue-415.css",
    "chars": 899,
    "preview": "@media only screen and (max-width:55px){#element p{font-size:1px}}@media only screen and (max-width:150px){#element p{fo"
  },
  {
    "path": "test/fixtures/styles/issue-562.css",
    "chars": 215,
    "preview": "@media all and (min-width: 768px) {\n  .title {\n    width: 200px;\n    height: 100px;\n  }\n}\n@media all and (min-width: 768"
  },
  {
    "path": "test/fixtures/styles/main.css",
    "chars": 771,
    "preview": "body {\n    padding-top: 20px;\n    padding-bottom: 20px;\n}\n\n\n.header,\n.marketing{\n    padding-left: 15px;\n    padding-rig"
  },
  {
    "path": "test/fixtures/styles/media-attr.css",
    "chars": 30,
    "preview": ".header {\n    display: flex;\n}"
  },
  {
    "path": "test/fixtures/styles/path-prefix.css",
    "chars": 70,
    "preview": ".header {\n    background: transparent url('../images/critical.png');\n}"
  },
  {
    "path": "test/fixtures/styles/print.css",
    "chars": 31,
    "preview": ".someRule {\n    color: #000;\n}\n"
  },
  {
    "path": "test/fixtures/styles/some/path/image.css",
    "chars": 76,
    "preview": ".header {\n    background: transparent url('../../../images/critical.png');\n}"
  },
  {
    "path": "test/fixtures/useragent/generate-default-useragent.html",
    "chars": 4110,
    "preview": "<!doctype html>\n<html class=\"no-js\">\n    <head>\n        <meta charset=\"utf-8\">\n        <title>critical css test</title>\n"
  },
  {
    "path": "test/fixtures/useragent/styles/bootstrap.css",
    "chars": 121455,
    "preview": "/*!\n * Bootstrap v3.1.1 (http://getbootstrap.com)\n * Copyright 2011-2014 Twitter, Inc.\n * Licensed under MIT (https://gi"
  },
  {
    "path": "test/fixtures/useragent/styles/main.css",
    "chars": 771,
    "preview": "body {\n    padding-top: 20px;\n    padding-bottom: 20px;\n}\n\n\n.header,\n.marketing{\n    padding-left: 15px;\n    padding-rig"
  },
  {
    "path": "test/helper/index.js",
    "chars": 1390,
    "preview": "import {Buffer} from 'node:buffer';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport {fileURLToPath} from "
  },
  {
    "path": "test/index.test.js",
    "chars": 11769,
    "preview": "import process from 'node:process';\nimport path from 'node:path';\nimport {promisify} from 'node:util';\nimport {fileURLTo"
  }
]

About this extraction

This page contains the full source code of the addyosmani/critical GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 126 files (592.7 KB), approximately 178.1k tokens, and a symbol index with 62 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!