├── src
├── __tests__
│ ├── inline-false
│ │ ├── style.css
│ │ ├── __snapshots__
│ │ │ └── inline-false.test.ts.snap
│ │ ├── index.html
│ │ └── inline-false.test.ts
│ └── inline-true
│ │ ├── style.css
│ │ ├── index.html
│ │ ├── __snapshots__
│ │ └── inline-true.test.ts.snap
│ │ └── inline-true.test.ts
├── @types
│ ├── rollup-plugin-critical.d.ts
│ ├── penthouse.d.ts
│ └── critical.d.ts
└── index.ts
├── vitest.config.ts
├── Dockerfile
├── dist
├── index.d.ts
├── index.d.cts
├── index.js
├── index.cjs
├── index.js.map
└── index.cjs.map
├── tsconfig.json
├── LICENSE.md
├── eslint.config.js
├── package.json
├── Makefile
├── CHANGELOG.md
└── README.md
/src/__tests__/inline-false/style.css:
--------------------------------------------------------------------------------
1 | .heading {
2 | color: red;
3 | }
4 |
5 | .unused-class {
6 | border: 3px solid blue;
7 | }
8 |
--------------------------------------------------------------------------------
/src/__tests__/inline-true/style.css:
--------------------------------------------------------------------------------
1 | .heading {
2 | color: red;
3 | }
4 |
5 | .unused-class {
6 | border: 3px solid blue;
7 | }
8 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config'
2 |
3 | export default defineConfig({
4 | test: {
5 | testTimeout: 20000,
6 | },
7 | });
8 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG TAG=22-alpine
2 | FROM nystudio107/node-dev-base:$TAG
3 |
4 | WORKDIR /app/
5 |
6 | RUN npm install -g npm@^11.0.0
7 |
8 | CMD ["run build"]
9 |
10 | ENTRYPOINT ["npm"]
11 |
--------------------------------------------------------------------------------
/src/__tests__/inline-false/__snapshots__/inline-false.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`\`inline: false\` Critical CSS generation 1`] = `".heading{color:red}"`;
4 |
--------------------------------------------------------------------------------
/src/__tests__/inline-false/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Critical CSS test
4 |
5 |
6 |
7 |
8 | hello, world.
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/__tests__/inline-true/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Critical CSS test
4 |
5 |
6 |
7 |
8 | hello, world.
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/dist/index.d.ts:
--------------------------------------------------------------------------------
1 | import { Plugin } from 'rollup';
2 |
3 | /**
4 | * [Vite.js](https://vitejs.dev/) & [Rollup](https://rollupjs.org/) plugin for generating critical CSS
5 | * that uses the [critical](https://github.com/addyosmani/critical) generator under the hood.
6 | *
7 | * @param {CriticalPluginConfig} pluginConfig - the plugin configuration object
8 | * @param {Function} callback - callback upon completion of the critical CSS generation
9 | * @constructor
10 | */
11 | declare function PluginCritical(pluginConfig: CriticalPluginConfig, callback?: CriticalPluginCallback): Plugin;
12 |
13 | export { PluginCritical as default };
14 |
--------------------------------------------------------------------------------
/dist/index.d.cts:
--------------------------------------------------------------------------------
1 | import { Plugin } from 'rollup';
2 |
3 | /**
4 | * [Vite.js](https://vitejs.dev/) & [Rollup](https://rollupjs.org/) plugin for generating critical CSS
5 | * that uses the [critical](https://github.com/addyosmani/critical) generator under the hood.
6 | *
7 | * @param {CriticalPluginConfig} pluginConfig - the plugin configuration object
8 | * @param {Function} callback - callback upon completion of the critical CSS generation
9 | * @constructor
10 | */
11 | declare function PluginCritical(pluginConfig: CriticalPluginConfig, callback?: CriticalPluginCallback): Plugin;
12 |
13 | export { PluginCritical as default };
14 |
--------------------------------------------------------------------------------
/src/__tests__/inline-true/__snapshots__/inline-true.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2 |
3 | exports[`\`inline: true\` Critical CSS generation 1`] = `
4 | "
5 |
6 | Critical CSS test
7 |
8 |
9 |
10 |
11 |
12 | hello, world.
13 |
14 |
15 |
16 |
17 | "
18 | `;
19 |
--------------------------------------------------------------------------------
/dist/index.js:
--------------------------------------------------------------------------------
1 | import*as n from"path";var m="_critical.min.css",h={inline:!1,extract:!1,width:1200,height:1200,penthouse:{blockJSRequests:!1}};function p(i,r){return{name:"critical",async writeBundle(o,f){let c=[];for(let t of Object.values(f))if(t.type==="asset"&&t.fileName.endsWith(".css")){let a=n.join(o.dir||"",t.fileName);c.push(a)}if(c.length)for(let t of i.criticalPages){let a=i.criticalBase,s=i.criticalUrl+t.uri,l=i.criticalConfig&&i.criticalConfig.inline==!0?t.template+".html":t.template+m,g=Object.assign({css:c},h,{base:a,src:s,target:l},i.criticalConfig),u=(await import("critical")).generate;console.log(`Generating critical CSS from ${s} to ${l}`),await u(g,e=>{e&&console.error(e),r&&r(e)})}}}}var C=p;export{C as default};
2 | //# sourceMappingURL=index.js.map
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "allowSyntheticDefaultImports": true,
5 | "baseUrl": "node_modules",
6 | "declaration": true,
7 | "esModuleInterop": true,
8 | "experimentalDecorators": true,
9 | "module": "esnext",
10 | "moduleResolution": "node",
11 | "noEmit": true,
12 | "noImplicitAny": true,
13 | "outDir": "./dist/",
14 | "paths": {
15 | "@/*": [
16 | "./src/*"
17 | ]
18 | },
19 | "resolveJsonModule": true,
20 | "skipLibCheck": true,
21 | "sourceMap": true,
22 | "strict": true,
23 | "strictBindCallApply": true,
24 | "strictFunctionTypes": true,
25 | "strictNullChecks": true,
26 | "target": "esnext",
27 | "types": [
28 | "node",
29 | "vite/client"
30 | ]
31 | },
32 | "include": [
33 | "./src/**/*.ts",
34 | "./src/**/*.vue"
35 | ],
36 | "exclude": [
37 | "./dist/**"
38 | ]
39 | }
40 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 nystudio107
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import typescriptEslint from "@typescript-eslint/eslint-plugin";
2 | import globals from "globals";
3 | import path from "node:path";
4 | import { fileURLToPath } from "node:url";
5 | import js from "@eslint/js";
6 | import { FlatCompat } from "@eslint/eslintrc";
7 |
8 | const __filename = fileURLToPath(import.meta.url);
9 | const __dirname = path.dirname(__filename);
10 | const compat = new FlatCompat({
11 | baseDirectory: __dirname,
12 | recommendedConfig: js.configs.recommended,
13 | allConfig: js.configs.all
14 | });
15 |
16 | export default [
17 | ...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"),
18 | {
19 | plugins: {
20 | "@typescript-eslint": typescriptEslint,
21 | },
22 |
23 | languageOptions: {
24 | globals: {
25 | ...globals.browser,
26 | ...globals.amd,
27 | ...globals.node,
28 | },
29 |
30 | ecmaVersion: 2020,
31 | sourceType: "module",
32 |
33 | parserOptions: {
34 | parser: "@typescript-eslint/parser",
35 | },
36 | },
37 |
38 | rules: {
39 | "no-undef": "off",
40 | "@typescript-eslint/ban-ts-comment": "off",
41 | },
42 | },
43 | ];
--------------------------------------------------------------------------------
/dist/index.cjs:
--------------------------------------------------------------------------------
1 | "use strict";var C=Object.create;var s=Object.defineProperty;var P=Object.getOwnPropertyDescriptor;var d=Object.getOwnPropertyNames;var S=Object.getPrototypeOf,b=Object.prototype.hasOwnProperty;var w=(t,i)=>{for(var c in i)s(t,c,{get:i[c],enumerable:!0})},g=(t,i,c,r)=>{if(i&&typeof i=="object"||typeof i=="function")for(let a of d(i))!b.call(t,a)&&a!==c&&s(t,a,{get:()=>i[a],enumerable:!(r=P(i,a))||r.enumerable});return t};var u=(t,i,c)=>(c=t!=null?C(S(t)):{},g(i||!t||!t.__esModule?s(c,"default",{value:t,enumerable:!0}):c,t)),j=t=>g(s({},"__esModule",{value:!0}),t);var N={};w(N,{default:()=>k});module.exports=j(N);var m=u(require("path"),1),x="_critical.min.css",y={inline:!1,extract:!1,width:1200,height:1200,penthouse:{blockJSRequests:!1}};function B(t,i){return{name:"critical",async writeBundle(c,r){let a=[];for(let e of Object.values(r))if(e.type==="asset"&&e.fileName.endsWith(".css")){let l=m.join(c.dir||"",e.fileName);a.push(l)}if(a.length)for(let e of t.criticalPages){let l=t.criticalBase,o=t.criticalUrl+e.uri,f=t.criticalConfig&&t.criticalConfig.inline==!0?e.template+".html":e.template+x,h=Object.assign({css:a},y,{base:l,src:o,target:f},t.criticalConfig),p=(await import("critical")).generate;console.log(`Generating critical CSS from ${o} to ${f}`),await p(h,n=>{n&&console.error(n),i&&i(n)})}}}}var k=B;
2 | //# sourceMappingURL=index.cjs.map
--------------------------------------------------------------------------------
/src/__tests__/inline-false/inline-false.test.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 | import PluginCritical from '../../index';
4 | import {Plugin} from 'rollup';
5 | import {expect, test} from 'vitest'
6 |
7 | const testRoot = path.join(__dirname, '/');
8 | const testOutputPath = path.join(testRoot, '__output__/test_critical.min.css');
9 |
10 | const pluginConfig: CriticalPluginConfig = {
11 | criticalBase: testRoot,
12 | criticalUrl: testRoot,
13 | criticalPages: [
14 | {
15 | uri: 'index.html',
16 | template: '__output__/test',
17 | }
18 | ],
19 | criticalConfig: {
20 | inline: false,
21 | },
22 | };
23 |
24 | test('`inline: false` Critical CSS generation', async () => {
25 | // Instantiate the Rollup plugin
26 | const plugin: Plugin = PluginCritical(pluginConfig);
27 | // Call the plugin to generate critical css
28 | if (plugin && typeof plugin.writeBundle === 'function') {
29 | // @ts-ignore
30 | await plugin.writeBundle({
31 | dir: testRoot,
32 | }, {
33 | chunk: {
34 | type: 'asset',
35 | fileName: 'style.css',
36 | }
37 | });
38 | // Compare the output with the snapshot
39 | expect(fs.readFileSync(testOutputPath).toString())
40 | .toMatchSnapshot();
41 | }
42 | });
43 |
--------------------------------------------------------------------------------
/src/__tests__/inline-true/inline-true.test.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 | import PluginCritical from '../../index';
4 | import { Plugin } from 'rollup';
5 | import {expect, test} from 'vitest'
6 |
7 | const testRoot = path.join(__dirname, '/');
8 | const testOutputPath = path.join(testRoot, '__output__/test_index.html');
9 |
10 | const pluginConfig: CriticalPluginConfig = {
11 | criticalBase: testRoot,
12 | criticalUrl: testRoot,
13 | criticalPages: [
14 | {
15 | uri: 'index.html',
16 | template: '__output__/test_index',
17 | }
18 | ],
19 | criticalConfig: {
20 | inline: true,
21 | },
22 | };
23 |
24 | test('`inline: true` Critical CSS generation', async () => {
25 | // Instantiate the Rollup plugin
26 | const plugin: Plugin = PluginCritical(pluginConfig);
27 | // Call the plugin to generate critical css
28 | if (plugin && typeof plugin.writeBundle === 'function') {
29 | // @ts-ignore
30 | await plugin.writeBundle({
31 | dir: testRoot,
32 | }, {
33 | chunk: {
34 | type: 'asset',
35 | fileName: 'style.css',
36 | }
37 | });
38 | // Compare the output with the snapshot
39 | expect(fs.readFileSync(testOutputPath).toString())
40 | .toMatchSnapshot();
41 | }
42 | });
43 |
--------------------------------------------------------------------------------
/src/@types/rollup-plugin-critical.d.ts:
--------------------------------------------------------------------------------
1 | type CriticalPluginCallback = (err: string) => void;
2 |
3 | interface CriticalPages {
4 | /** Combined with `criticalUrl` to determine the URLs to scrape for Critical CSS */
5 | uri: string;
6 | /** Critical CSS files are named with the `template` path, and saved to the `criticalBase` directory */
7 | template: string;
8 | }
9 |
10 | interface CriticalPluginConfig {
11 | /**
12 | * The base URL to use in combination with the `criticalPages` `uri`s to determine the URLs to scrape for Critical CSS.
13 | * This can also be a file system path. This is combined with `criticalPages.uri`
14 | * to determine pages to scrap for critical CSS.
15 | * Determines the `criticalConfig.src` property
16 | */
17 | criticalUrl: string;
18 | /**
19 | * The base file system path to where the generated Critical CSS file should be saved.
20 | * This is combined with `criticalPages.template` with `_critical.min.css` appended
21 | * to it to determine the saved critical CSS file name.
22 | * Determines the `criticalConfig.target` property
23 | */
24 | criticalBase?: string;
25 | /**
26 | * An array objects that contain the page `uri`s that are combined with the `criticalUrl` to
27 | * determine the URLs to scrape for Critical CSS. The resulting files are named with the
28 | * `template` path, and saved to the `criticalBase` directory
29 | */
30 | criticalPages: Partial[];
31 | /**
32 | * This is the full [config for critical](https://github.com/addyosmani/critical#options) that is passed
33 | * through to the `critical` package.
34 | * You may optionally override any properties you like here
35 | */
36 | criticalConfig?: Partial;
37 | }
38 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rollup-plugin-critical",
3 | "type": "module",
4 | "version": "1.0.15",
5 | "description": "Rollup plugin to generate critical CSS.",
6 | "author": "nystudio107",
7 | "license": "MIT",
8 | "keywords": [
9 | "rollup",
10 | "plugin",
11 | "critical",
12 | "css"
13 | ],
14 | "homepage": "https://github.com/nystudio107/rollup-plugin-critical",
15 | "repository": {
16 | "type": "git",
17 | "url": "git+ssh://github.com/nystudio107/rollup-plugin-critical"
18 | },
19 | "bugs": {
20 | "url": "https://github.com/nystudio107/rollup-plugin-critical/issues"
21 | },
22 | "exports": {
23 | "types": "./dist/index.d.ts",
24 | "import": "./dist/index.js",
25 | "require": "./dist/index.cjs"
26 | },
27 | "main": "dist/index.cjs",
28 | "module": "dist/index.js",
29 | "types": "dist/index.d.ts",
30 | "files": [
31 | "dist",
32 | "LICENSE",
33 | "README.md"
34 | ],
35 | "scripts": {
36 | "build": "npm run lint && tsup src/index.ts --clean --minify --sourcemap --dts --format cjs,esm",
37 | "check": "tsc --noEmit",
38 | "dev": "nr build --watch",
39 | "lint": "tsc --noEmit && eslint './src/**/*.{js,ts,vue}' --fix",
40 | "prepublishOnly": "nr build",
41 | "release": "npm run lint && npm login && npm publish",
42 | "test": "vitest run --coverage",
43 | "test-coverage": "vitest run --coverage",
44 | "test-ci": "vitest run --coverage.enabled --coverage.reporter='text-summary'",
45 | "test-dev": "vitest"
46 | },
47 | "dependencies": {
48 | "critical": "^7.0.0"
49 | },
50 | "devDependencies": {
51 | "@antfu/ni": "^0.23.0",
52 | "@types/node": "^20.0.0",
53 | "@typescript-eslint/eslint-plugin": "^8.0.0",
54 | "@vitest/coverage-v8": "^2.0.0",
55 | "eslint": "^9.0.0",
56 | "rollup": "^4.0.0",
57 | "tsup": "^8.0.0",
58 | "typescript": "latest",
59 | "vite": "^5.0.0",
60 | "vitest": "^2.0.0"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | TAG?=22-alpine
2 | CONTAINER?=$(shell basename $(CURDIR))
3 | DOCKER_RUN=docker container run --rm -it -v `pwd`:/app
4 | IMAGE_INFO=$(shell docker image inspect $(CONTAINER):$(TAG))
5 | IMAGE_NAME=${CONTAINER}:${TAG}
6 |
7 | .PHONY: build clean dev image-build image-check lint release ssh test test-coverage test-dev npm
8 |
9 | # Perform a dist build
10 | build: image-check
11 | ${DOCKER_RUN} --name ${CONTAINER}-$@ ${IMAGE_NAME} run build
12 | # Remove node_modules/ & package-lock.json
13 | clean:
14 | rm -rf node_modules/
15 | rm -f package-lock.json
16 | # Run in watch mode for development
17 | dev: image-check
18 | ${DOCKER_RUN} --name ${CONTAINER}-$@ ${IMAGE_NAME} run dev
19 | # Build the Docker image & run npm install
20 | image-build:
21 | docker build . -t ${IMAGE_NAME} --build-arg TAG=${TAG} --no-cache
22 | ${DOCKER_RUN} --name ${CONTAINER}-$@ ${IMAGE_NAME} install
23 | # Ensure the image has been created
24 | image-check:
25 | ifeq ($(IMAGE_INFO), [])
26 | image-check: image-build
27 | endif
28 | # Run eslint & tsc to check the code
29 | lint: image-check
30 | ${DOCKER_RUN} --name ${CONTAINER}-$@ ${IMAGE_NAME} run lint
31 | # Release a new version
32 | release: image-check
33 | ${DOCKER_RUN} --name ${CONTAINER}-$@ ${IMAGE_NAME} run release
34 | # ssh into the already running container
35 | ssh: image-check
36 | ${DOCKER_RUN} --name ${CONTAINER}-$@ --entrypoint=/bin/sh ${IMAGE_NAME}
37 | # Run tests via npm run test
38 | test: image-check
39 | ${DOCKER_RUN} --name ${CONTAINER}-$@ ${IMAGE_NAME} run test
40 | # Run tests with coverage via npm run test-coverage
41 | test-coverage: image-check
42 | ${DOCKER_RUN} --name ${CONTAINER}-$@ ${IMAGE_NAME} run test-coverage
43 | # Run tests in dev mode via npm run test-dev
44 | test-dev: image-check
45 | ${DOCKER_RUN} --name ${CONTAINER}-$@ ${IMAGE_NAME} run test-dev
46 | npm: docker
47 | ${DOCKER_RUN} --name ${CONTAINER}-$@ ${IMAGE_NAME} $(filter-out $@,$(MAKECMDGOALS)) $(MAKEFLAGS)
48 | %:
49 | @:
50 | # ref: https://stackoverflow.com/questions/6273608/how-to-pass-argument-to-makefile-from-command-line
51 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Rollup Plugin Critical
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | ## 1.0.15 - 2025.02.02
6 | ### Changed
7 | * Updated to Node 22 & npm 11
8 | * Updated to latest deps
9 |
10 | ## 1.0.14 - 2024.09.04
11 | ### Changed
12 | * Updated to the latest Critical & other deps
13 |
14 | ## 1.0.13 - 2023.11.22
15 | ### Changed
16 | * Switch over to using Node 20 and NPM 10 in the Dockerfile
17 | * Update to Vite `^5.0.0` and Vitest `^1.0.0-beta.5`
18 |
19 | ### Fix
20 | * Fix an issue where building with a project of `"type": "module"` would fail because conditional exports were not defined in `"exports"` ([#12](https://github.com/nystudio107/rollup-plugin-critical/issues/12))
21 |
22 | ## 1.0.12 - 2022.12.12
23 | ### Fix
24 | * Fix import of the now ESM-only `critical` package into the CJS build of `rollup-plugin-critical` ([#9](https://github.com/nystudio107/rollup-plugin-critical/issues/9))
25 |
26 | ## 1.0.11 - 2022.12.12
27 | ### Changed
28 | * Refactored the tests to use snapshots
29 |
30 | ### Fixed
31 | * Fixed an issue where the plugin was being bundled as ESM, when it should be CommonJS for broader (and backwards) compatibility ([#9](https://github.com/nystudio107/rollup-plugin-critical/issues/9))
32 |
33 | ## 1.0.10 - 2022.12.11
34 | ### Added
35 | * Add `eslint` to the build phase
36 |
37 | ### Changed
38 | * Install the latest `npm` in the Docker image
39 | * Add `dev`, `release` commands, remove `update` dependency
40 | * Minify the `dist/` output
41 | * Clean up type definitions
42 | * Refactor to use `critical` `^5.0.0`
43 | * Switch tests from Jest to Vitest
44 |
45 | ## 1.0.9 - 2022.09.13
46 | ### Added
47 | * Added support for `inline: true` via ([#5](https://github.com/nystudio107/rollup-plugin-critical/pull/5))
48 |
49 | ## 1.0.8 - 2021.10.15
50 | ### Changed
51 | * Switched to `critical` `^4.0.0` ([#2](https://github.com/nystudio107/rollup-plugin-critical/issues/2))
52 | * Remove the `minify` option, since it was removed from `critical` `^4.0.0`
53 |
54 | ## 1.0.7 - 2021-06-02
55 | ### Changed
56 | * Switched to `tsup` for bundling
57 |
58 | ## 1.0.6 - 2021-06-01
59 | ### Fixed
60 | * Fixed build of `dist/index.d.ts` to have the correct default export by sourcing `index.ts`
61 |
62 | ## 1.0.5 - 2021-06-01
63 | ### Fixed
64 | * Fixed `respectExternal` setting
65 |
66 | ## 1.0.4 - 2021-06-01
67 | ### Changed
68 | * Split types out into separate files
69 | * Use Rollup and dts to build the types
70 |
71 | ## 1.0.3 - 2021-05-31
72 | ### Fixed
73 | * Fixed `dist/index.d.ts` to have complete type definitions
74 |
75 | ## 1.0.2 - 2021-05-31
76 | ### Changed
77 | * Administrivia
78 |
79 | ## 1.0.1 - 2021-05-31
80 | ### Changed
81 | * Remove unneeded conditionals
82 | * Improve comment typehints in jsdoc
83 | * Remove vestigial `concurrency` property
84 |
85 | ## 1.0.0 - 2021-05-30
86 | ### Added
87 | * Initial release
88 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import {Plugin} from 'rollup';
2 | import * as path from 'path';
3 |
4 | const criticalSuffix = '_critical.min.css';
5 |
6 | /**
7 | * Default `criticalConfig` passed in to `critical`
8 | */
9 | const defaultCriticalConfig: Partial = {
10 | inline: false,
11 | extract: false,
12 | width: 1200,
13 | height: 1200,
14 | penthouse: {
15 | blockJSRequests: false
16 | }
17 | };
18 |
19 | /**
20 | * [Vite.js](https://vitejs.dev/) & [Rollup](https://rollupjs.org/) plugin for generating critical CSS
21 | * that uses the [critical](https://github.com/addyosmani/critical) generator under the hood.
22 | *
23 | * @param {CriticalPluginConfig} pluginConfig - the plugin configuration object
24 | * @param {Function} callback - callback upon completion of the critical CSS generation
25 | * @constructor
26 | */
27 | function PluginCritical(pluginConfig: CriticalPluginConfig, callback?: CriticalPluginCallback): Plugin {
28 | return {
29 | name: 'critical',
30 | async writeBundle(outputOptions, bundle) {
31 | const css: Array = [];
32 | // Find all of the generated CSS assets
33 | for (const chunk of Object.values(bundle)) {
34 | if (chunk.type === 'asset' && chunk.fileName.endsWith('.css')) {
35 | const cssFile = path.join(outputOptions.dir || '', chunk.fileName);
36 | css.push(cssFile);
37 | }
38 | }
39 | // If we have no CSS, skip bundle
40 | if (!css.length) {
41 | return;
42 | }
43 | // Iterate through the pages
44 | for (const page of pluginConfig.criticalPages) {
45 | const criticalBase = pluginConfig.criticalBase;
46 | const criticalSrc = pluginConfig.criticalUrl + page.uri;
47 | // If inline is set to true, use HTML as target, otherwise CSS with suffix
48 | const criticalTarget = (pluginConfig.criticalConfig && pluginConfig.criticalConfig.inline == true) ? page.template + ".html" : page.template + criticalSuffix;
49 | // Merge in our options
50 | const options = Object.assign(
51 | { css },
52 | defaultCriticalConfig,
53 | {
54 | base: criticalBase,
55 | src: criticalSrc,
56 | target: criticalTarget,
57 | },
58 | pluginConfig.criticalConfig
59 | );
60 | // Horrible nonsense to import an ESM module into CJS
61 | // ref: https://adamcoster.com/blog/commonjs-and-esm-importexport-compatibility-examples
62 | const generate = (await import('critical')).generate;
63 | // Generate the Critical CSS
64 | console.log(`Generating critical CSS from ${criticalSrc} to ${criticalTarget}`);
65 | await generate(options, (err: string) => {
66 | if (err) {
67 | console.error(err);
68 | }
69 | if (callback) {
70 | callback(err);
71 | }
72 | });
73 | }
74 | }
75 | }
76 | }
77 |
78 | export default PluginCritical;
79 |
--------------------------------------------------------------------------------
/src/@types/penthouse.d.ts:
--------------------------------------------------------------------------------
1 | type PenthouseAllowedResponseCallback = (response: object) => boolean;
2 |
3 | interface PenthouseConfig {
4 | /** Accessible url. Use file:/// protocol for local html files. */
5 | url: string;
6 | /** Original css to extract critical css from */
7 | cssString: string;
8 | /** Path to original css file on disk (if using instead of `cssString`) */
9 | css: string;
10 | /** Width for critical viewport */
11 | width: number;
12 | /** Height for critical viewport */
13 | height: number;
14 | /** Configuration for screenshots (not used by default). See [Screenshot example](https://github.com/pocketjoso/penthouse/blob/master/examples/screenshots.js) */
15 | screenshots: object;
16 | /** Keep media queries even for width/height values larger than critical viewport. */
17 | keepLargerMediaQueries: boolean;
18 | /**
19 | * Array of css selectors to keep in critical css, even if not appearing in critical viewport.
20 | * Strings or regex (f.e. `['.keepMeEvenIfNotSeenInDom', /^\.button/]`)
21 | */
22 | forceInclude: Array;
23 | /**
24 | * Array of css selectors to remove in critical css, even if appearing in critical viewport.
25 | * Strings or regex (f.e. `['.doNotKeepMeEvenIfNotSeenInDom', /^\.button/]`)
26 | */
27 | forceExclude: Array;
28 | /** Css properties to filter out from critical css */
29 | propertiesToRemove: Array;
30 | /** Ms; abort critical CSS generation after this time */
31 | timeout: number;
32 | /** Settings for puppeteer. See [Custom puppeteer browser example](https://github.com/pocketjoso/penthouse/blob/master/examples/custom-browser.js) */
33 | puppeteer: object;
34 | /** Ms; stop waiting for page load after this time (for sites when page load event is unreliable) */
35 | pageLoadSkipTimeout: number;
36 | /**
37 | * ms; wait time after page load before critical css extraction starts
38 | * (also before "before" screenshot is taken, if used)
39 | */
40 | renderWaitTime: number;
41 | /** set to false to load JS (not recommended) */
42 | blockJSRequests: boolean;
43 | /** characters; strip out inline base64 encoded resources larger than this */
44 | maxEmbeddedBase64Length: number;
45 | /** Can be specified to limit nr of elements to inspect per css selector, reducing execution time. */
46 | maxElementsToCheckPerSelector: number;
47 | /** specify which user agent string when loading the page */
48 | userAgent: string;
49 | /** Set extra http headers to be sent with the request for the url. */
50 | customPageHeaders: object;
51 | /** For formatting of each cookie, see [Puppeteer setCookie docs](https://github.com/puppeteer/puppeteer/blob/v1.9.0/docs/api.md#pagesetcookiecookies) */
52 | cookies: Array;
53 | /** Make Penthouse throw on errors parsing the original CSS. Legacy option, not recommended */
54 | strict: boolean;
55 | /**
56 | * Let Penthouse stop if the server response code is not matching this value. number and
57 | * regex types are tested against the [response.status()](https://github.com/puppeteer/puppeteer/blob/v1.14.0/docs/api.md#responsestatus). A function is also allowed and
58 | * gets [Response](https://github.com/puppeteer/puppeteer/blob/v1.14.0/docs/api.md#class-response) as argument. The function should return a boolean.
59 | */
60 | allowedResponseCode: number | RegExp | PenthouseAllowedResponseCallback;
61 | }
62 |
--------------------------------------------------------------------------------
/src/@types/critical.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'critical';
2 |
3 | type DeclCallback = (node: object, value: string) => boolean;
4 |
5 | interface PostcssUrlAsset {
6 | /** original url */
7 | url: string;
8 | /** url pathname (url without search or hash) */
9 | pathname: string;
10 | /** absolute path to asset */
11 | absolutePath: string;
12 | /** current relative path to asset */
13 | relativePath: string;
14 | /** search from url, ex. ?query=1 from ./image.png?query=1 **/
15 | search: string;
16 | /** hash from url, ex. #spriteLink from ../asset.svg#spriteLink */
17 | hash: string;
18 | }
19 |
20 | type RebaseFn = (asset: PostcssUrlAsset) => string;
21 |
22 | interface RebaseConfig {
23 | from: string;
24 | to: string;
25 | }
26 |
27 | interface CriticalConfig {
28 | /** Inline critical-path CSS using Filament Group's loadCSS. Pass an object to configure `inline-critical` */
29 | inline: boolean;
30 | /** Base directory in which the source and destination are to be written */
31 | base: string;
32 | /** HTML source to be operated against. This option takes precedence over the `src` option */
33 | html: string;
34 | /** An array of paths to css files, file globs or Vinyl file objects. */
35 | css: Array;
36 | /** Location of the HTML source to be operated against */
37 | src: string;
38 | /**
39 | * Location of where to save the output of an operation.
40 | * Use an object with 'html' and 'css' props if you want to store both
41 | */
42 | target: string | Partial<{
43 | css: string;
44 | html: string;
45 | uncritical: string;
46 | }>;
47 | /** Width of the target viewport */
48 | width: number;
49 | /** Height of the target viewport */
50 | height: number;
51 | /**
52 | * Remove the inlined styles from any stylesheets referenced in the HTML.
53 | * It generates new references based on extracted content so it's safe to use for
54 | * multiple HTML files referencing the same stylesheet. Use with caution.
55 | * Removing the critical CSS per page results in a unique async loaded CSS file for every page.
56 | * Meaning you can't rely on cache across multiple pages
57 | */
58 | extract: boolean;
59 | /** Inline images */
60 | inlineImages: boolean;
61 | /** List of directories/urls where the inliner should start looking for assets */
62 | assetPaths: Array;
63 | /** Sets a max file size (in bytes) for base64 inlined images */
64 | maxImageFileSize: number;
65 | /**
66 | * Critical tries it's best to rebase the asset paths relative to the document.
67 | * If this doesn't work as expected you can always use this option to control the rebase paths.
68 | * See postcss-url for details. (https://github.com/pocketjoso/penthouse#usage-1).
69 | */
70 | rebase: RebaseConfig | RebaseFn;
71 | /** ignore CSS rules */
72 | ignore: Partial<{
73 | atrule: Array;
74 | rule: Array;
75 | decl: DeclCallback;
76 | }>;
77 | /** User agent to use when fetching a remote src */
78 | userAgent: string;
79 | /** Configuration options for `penthouse`. */
80 | penthouse: Partial;
81 | /** Configuration options for `got`. */
82 | request: object;
83 | /** RFC2617 basic authorization: `user` */
84 | user: string;
85 | /** RFC2617 basic authorization: `pass` */
86 | pass: string;
87 | /** Throw an error if no css is found */
88 | strict: boolean;
89 | }
90 |
--------------------------------------------------------------------------------
/dist/index.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {Plugin} from 'rollup';\nimport * as path from 'path';\n\nconst criticalSuffix = '_critical.min.css';\n\n/**\n * Default `criticalConfig` passed in to `critical`\n */\nconst defaultCriticalConfig: Partial = {\n inline: false,\n extract: false,\n width: 1200,\n height: 1200,\n penthouse: {\n blockJSRequests: false\n }\n};\n\n/**\n * [Vite.js](https://vitejs.dev/) & [Rollup](https://rollupjs.org/) plugin for generating critical CSS\n * that uses the [critical](https://github.com/addyosmani/critical) generator under the hood.\n *\n * @param {CriticalPluginConfig} pluginConfig - the plugin configuration object\n * @param {Function} callback - callback upon completion of the critical CSS generation\n * @constructor\n */\nfunction PluginCritical(pluginConfig: CriticalPluginConfig, callback?: CriticalPluginCallback): Plugin {\n return {\n name: 'critical',\n async writeBundle(outputOptions, bundle) {\n const css: Array = [];\n // Find all of the generated CSS assets\n for (const chunk of Object.values(bundle)) {\n if (chunk.type === 'asset' && chunk.fileName.endsWith('.css')) {\n const cssFile = path.join(outputOptions.dir || '', chunk.fileName);\n css.push(cssFile);\n }\n }\n // If we have no CSS, skip bundle\n if (!css.length) {\n return;\n }\n // Iterate through the pages\n for (const page of pluginConfig.criticalPages) {\n const criticalBase = pluginConfig.criticalBase;\n const criticalSrc = pluginConfig.criticalUrl + page.uri;\n // If inline is set to true, use HTML as target, otherwise CSS with suffix\n const criticalTarget = (pluginConfig.criticalConfig && pluginConfig.criticalConfig.inline == true) ? page.template + \".html\" : page.template + criticalSuffix;\n // Merge in our options\n const options = Object.assign(\n { css },\n defaultCriticalConfig,\n {\n base: criticalBase,\n src: criticalSrc,\n target: criticalTarget,\n },\n pluginConfig.criticalConfig\n );\n // Horrible nonsense to import an ESM module into CJS\n // ref: https://adamcoster.com/blog/commonjs-and-esm-importexport-compatibility-examples\n const generate = (await import('critical')).generate;\n // Generate the Critical CSS\n console.log(`Generating critical CSS from ${criticalSrc} to ${criticalTarget}`);\n await generate(options, (err: string) => {\n if (err) {\n console.error(err);\n }\n if (callback) {\n callback(err);\n }\n });\n }\n }\n }\n}\n\nexport default PluginCritical;\n"],"mappings":"AACA,UAAYA,MAAU,OAEtB,IAAMC,EAAiB,oBAKjBC,EAAiD,CACrD,OAAQ,GACR,QAAS,GACT,MAAO,KACP,OAAQ,KACR,UAAW,CACT,gBAAiB,EACnB,CACF,EAUA,SAASC,EAAeC,EAAoCC,EAA2C,CACrG,MAAO,CACL,KAAM,WACN,MAAM,YAAYC,EAAeC,EAAQ,CACvC,IAAMC,EAAqB,CAAC,EAE5B,QAAWC,KAAS,OAAO,OAAOF,CAAM,EACtC,GAAIE,EAAM,OAAS,SAAWA,EAAM,SAAS,SAAS,MAAM,EAAG,CAC7D,IAAMC,EAAe,OAAKJ,EAAc,KAAO,GAAIG,EAAM,QAAQ,EACjED,EAAI,KAAKE,CAAO,CAClB,CAGF,GAAKF,EAAI,OAIT,QAAWG,KAAQP,EAAa,cAAe,CAC7C,IAAMQ,EAAeR,EAAa,aAC5BS,EAAcT,EAAa,YAAcO,EAAK,IAE9CG,EAAkBV,EAAa,gBAAkBA,EAAa,eAAe,QAAU,GAAQO,EAAK,SAAW,QAAUA,EAAK,SAAWV,EAEzIc,EAAU,OAAO,OACnB,CAAE,IAAAP,CAAI,EACNN,EACA,CACE,KAAMU,EACN,IAAKC,EACL,OAAQC,CACV,EACAV,EAAa,cACjB,EAGMY,GAAY,KAAM,QAAO,UAAU,GAAG,SAE5C,QAAQ,IAAI,gCAAgCH,CAAW,OAAOC,CAAc,EAAE,EAC9E,MAAME,EAASD,EAAUE,GAAgB,CACnCA,GACF,QAAQ,MAAMA,CAAG,EAEfZ,GACFA,EAASY,CAAG,CAEhB,CAAC,CACH,CACF,CACF,CACF,CAEA,IAAOC,EAAQf","names":["path","criticalSuffix","defaultCriticalConfig","PluginCritical","pluginConfig","callback","outputOptions","bundle","css","chunk","cssFile","page","criticalBase","criticalSrc","criticalTarget","options","generate","err","index_default"]}
--------------------------------------------------------------------------------
/dist/index.cjs.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {Plugin} from 'rollup';\nimport * as path from 'path';\n\nconst criticalSuffix = '_critical.min.css';\n\n/**\n * Default `criticalConfig` passed in to `critical`\n */\nconst defaultCriticalConfig: Partial = {\n inline: false,\n extract: false,\n width: 1200,\n height: 1200,\n penthouse: {\n blockJSRequests: false\n }\n};\n\n/**\n * [Vite.js](https://vitejs.dev/) & [Rollup](https://rollupjs.org/) plugin for generating critical CSS\n * that uses the [critical](https://github.com/addyosmani/critical) generator under the hood.\n *\n * @param {CriticalPluginConfig} pluginConfig - the plugin configuration object\n * @param {Function} callback - callback upon completion of the critical CSS generation\n * @constructor\n */\nfunction PluginCritical(pluginConfig: CriticalPluginConfig, callback?: CriticalPluginCallback): Plugin {\n return {\n name: 'critical',\n async writeBundle(outputOptions, bundle) {\n const css: Array = [];\n // Find all of the generated CSS assets\n for (const chunk of Object.values(bundle)) {\n if (chunk.type === 'asset' && chunk.fileName.endsWith('.css')) {\n const cssFile = path.join(outputOptions.dir || '', chunk.fileName);\n css.push(cssFile);\n }\n }\n // If we have no CSS, skip bundle\n if (!css.length) {\n return;\n }\n // Iterate through the pages\n for (const page of pluginConfig.criticalPages) {\n const criticalBase = pluginConfig.criticalBase;\n const criticalSrc = pluginConfig.criticalUrl + page.uri;\n // If inline is set to true, use HTML as target, otherwise CSS with suffix\n const criticalTarget = (pluginConfig.criticalConfig && pluginConfig.criticalConfig.inline == true) ? page.template + \".html\" : page.template + criticalSuffix;\n // Merge in our options\n const options = Object.assign(\n { css },\n defaultCriticalConfig,\n {\n base: criticalBase,\n src: criticalSrc,\n target: criticalTarget,\n },\n pluginConfig.criticalConfig\n );\n // Horrible nonsense to import an ESM module into CJS\n // ref: https://adamcoster.com/blog/commonjs-and-esm-importexport-compatibility-examples\n const generate = (await import('critical')).generate;\n // Generate the Critical CSS\n console.log(`Generating critical CSS from ${criticalSrc} to ${criticalTarget}`);\n await generate(options, (err: string) => {\n if (err) {\n console.error(err);\n }\n if (callback) {\n callback(err);\n }\n });\n }\n }\n }\n}\n\nexport default PluginCritical;\n"],"mappings":"0jBAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,aAAAE,IAAA,eAAAC,EAAAH,GACA,IAAAI,EAAsB,qBAEhBC,EAAiB,oBAKjBC,EAAiD,CACrD,OAAQ,GACR,QAAS,GACT,MAAO,KACP,OAAQ,KACR,UAAW,CACT,gBAAiB,EACnB,CACF,EAUA,SAASC,EAAeC,EAAoCC,EAA2C,CACrG,MAAO,CACL,KAAM,WACN,MAAM,YAAYC,EAAeC,EAAQ,CACvC,IAAMC,EAAqB,CAAC,EAE5B,QAAWC,KAAS,OAAO,OAAOF,CAAM,EACtC,GAAIE,EAAM,OAAS,SAAWA,EAAM,SAAS,SAAS,MAAM,EAAG,CAC7D,IAAMC,EAAe,OAAKJ,EAAc,KAAO,GAAIG,EAAM,QAAQ,EACjED,EAAI,KAAKE,CAAO,CAClB,CAGF,GAAKF,EAAI,OAIT,QAAWG,KAAQP,EAAa,cAAe,CAC7C,IAAMQ,EAAeR,EAAa,aAC5BS,EAAcT,EAAa,YAAcO,EAAK,IAE9CG,EAAkBV,EAAa,gBAAkBA,EAAa,eAAe,QAAU,GAAQO,EAAK,SAAW,QAAUA,EAAK,SAAWV,EAEzIc,EAAU,OAAO,OACnB,CAAE,IAAAP,CAAI,EACNN,EACA,CACE,KAAMU,EACN,IAAKC,EACL,OAAQC,CACV,EACAV,EAAa,cACjB,EAGMY,GAAY,KAAM,QAAO,UAAU,GAAG,SAE5C,QAAQ,IAAI,gCAAgCH,CAAW,OAAOC,CAAc,EAAE,EAC9E,MAAME,EAASD,EAAUE,GAAgB,CACnCA,GACF,QAAQ,MAAMA,CAAG,EAEfZ,GACFA,EAASY,CAAG,CAEhB,CAAC,CACH,CACF,CACF,CACF,CAEA,IAAOnB,EAAQK","names":["index_exports","__export","index_default","__toCommonJS","path","criticalSuffix","defaultCriticalConfig","PluginCritical","pluginConfig","callback","outputOptions","bundle","css","chunk","cssFile","page","criticalBase","criticalSrc","criticalTarget","options","generate","err"]}
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | 
3 | [](https://scrutinizer-ci.com/g/nystudio107/rollup-plugin-critical/?branch=master)
4 | [](https://scrutinizer-ci.com/g/nystudio107/rollup-plugin-critical/?branch=master)
5 | [](https://scrutinizer-ci.com/g/nystudio107/rollup-plugin-critical/build-status/master)
6 |
7 | # rollup-plugin-critical
8 |
9 | [Vite.js](https://vitejs.dev/) & [Rollup](https://rollupjs.org/) plugin for generating critical CSS that uses the [critical](https://github.com/addyosmani/critical) generator under the hood.
10 |
11 | ## Install
12 |
13 | ```bash
14 | npm i -D rollup-plugin-critical
15 | ```
16 |
17 | ## Usage
18 |
19 | ```js
20 | // rollup.config.js
21 |
22 | import PluginCritical from 'rollup-plugin-critical';
23 |
24 | export default {
25 | input: 'index.js',
26 | output: {
27 | dir: 'dist',
28 | format: 'es',
29 | },
30 | plugins: [
31 | PluginCritical({
32 | criticalUrl: 'https://nystudio107.com/',
33 | criticalBase: './',
34 | criticalPages: [
35 | { uri: '', template: 'index' },
36 | { uri: 'about', template: 'about/index' },
37 | ],
38 | criticalConfig: {
39 | },
40 | }),
41 | ],
42 | }
43 | ```
44 |
45 | ## Options
46 |
47 | ### `criticalUrl: string`
48 |
49 | The base URL to use in combination with the `criticalPages` `uri`s to determine the URLs to scrape for Critical CSS.
50 |
51 | This can also be a file system path. This is combined with `criticalPages.uri` (see below) to determine pages to scrap for critical CSS.
52 |
53 | Determines the `criticalConfig.src` property (see below)
54 |
55 | ### `criticalBase: string`
56 |
57 | The base file system path to where the generated Critical CSS file should be saved.
58 |
59 | This is combined with `criticalPages.template` (see below) with `_critical.min.css` appended to it to determine the saved critical CSS file name.
60 |
61 | Determines the `criticalConfig.target` property (see below)
62 |
63 | ### `criticalPages: array of objects`
64 |
65 | An array objects that contain the page `uri`s that are combined with the `criticalUrl` to determine the URLs to scrape for Critical CSS.
66 |
67 | The resulting files are named with the `template` path, and saved to the `criticalBase` directory
68 |
69 | ### `criticalConfig: object`
70 |
71 | This is the full [config for critical](https://github.com/addyosmani/critical#options) that is passed through to the `critical` package.
72 |
73 | You may optionally override any properties you like here. The default values passed in are:
74 |
75 | ```ts
76 | const defaultCriticalConfig: Partial = {
77 | inline: false,
78 | extract: false,
79 | width: 1200,
80 | height: 1200,
81 | penthouse: {
82 | blockJSRequests: false
83 | }
84 | };
85 | ```
86 |
87 | The following [critical config properties](https://github.com/addyosmani/critical#options) are set dynamically by `rollup-plugin-critical`, but can be overridden via `criticalConfig`:
88 |
89 | - **`css`** - set to the css files that are generated in the Rollup build
90 | - **`base`** - property is set to `criticalBase`
91 | - **`src`** - derived from `criticalUrl` and `criticalPages.uri`
92 | - **`target`** - derived from `criticalPages.template` with `_critical.min.css` appended to it. If the `inline` option is set to `true`, the suffix `.html` is appended instead.
93 | ## License
94 |
95 | [MIT](LICENSE) © [nystudio107](https://nystudio107.com)
96 |
--------------------------------------------------------------------------------