├── examples
└── with-next
│ ├── .gitignore
│ ├── .dockerignore
│ ├── static
│ ├── logo.png
│ └── favicon.png
│ ├── styles
│ ├── background.jpg
│ └── index.scss
│ ├── Dockerfile
│ ├── components
│ ├── page.js
│ ├── link.js
│ └── navbar.js
│ ├── routes.js
│ ├── pages
│ ├── _document.js
│ ├── index.js
│ └── editor.js
│ ├── package.json
│ ├── bin
│ ├── bundle
│ └── start
│ ├── README.md
│ └── next.config.js
├── CHANGELOG.md
├── package.json
├── index.js
└── README.md
/examples/with-next/.gitignore:
--------------------------------------------------------------------------------
1 | !bin
2 | .next
3 | .static
4 | out
5 |
--------------------------------------------------------------------------------
/examples/with-next/.dockerignore:
--------------------------------------------------------------------------------
1 | Dockerfile
2 | node_modules
3 | .next
4 | .static
5 | out
6 |
--------------------------------------------------------------------------------
/examples/with-next/static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sheerun/extracted-loader/HEAD/examples/with-next/static/logo.png
--------------------------------------------------------------------------------
/examples/with-next/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sheerun/extracted-loader/HEAD/examples/with-next/static/favicon.png
--------------------------------------------------------------------------------
/examples/with-next/styles/background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sheerun/extracted-loader/HEAD/examples/with-next/styles/background.jpg
--------------------------------------------------------------------------------
/examples/with-next/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM sheerun/critical:1.1.0
2 |
3 | WORKDIR /app
4 |
5 | ADD *.json /app/
6 |
7 | RUN yarn
8 |
9 | RUN yarn add critical@1.1.0 --offline
10 |
11 | ADD . /app/
12 |
13 | ENV NODE_ENV=production
14 |
15 | RUN yarn build
16 |
17 | EXPOSE 3000
18 |
19 | CMD ["node", "bin/start"]
20 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 1.0.4
2 |
3 | Reloaded element can exist anywhere in the DOM and stays in place when reloaded.
4 |
5 | # 1.0.3
6 |
7 | Fix hot reloading on pages other than index
8 |
9 | # 1.0.2
10 |
11 | Final documentation update (to show it on npmjs.org)
12 |
13 | # 1.0.1
14 |
15 | Fix example how to use this plugin
16 |
17 | # 1.0.0
18 |
19 | Initial release
20 |
--------------------------------------------------------------------------------
/examples/with-next/components/page.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Head from 'next/head'
3 |
4 | export default class Page extends React.Component {
5 | render () {
6 | return (
7 |
8 |
9 |
10 |
11 | {this.props.children}
12 |
13 | )
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/examples/with-next/styles/index.scss:
--------------------------------------------------------------------------------
1 | @import "~bulma/sass/utilities/initial-variables";
2 | $primary: #3E3C3D;
3 | $info: $blue;
4 |
5 | @import "~bulma";
6 |
7 | .editor {
8 | .navbar-brand img {
9 | filter: invert(100%);
10 | }
11 | }
12 |
13 | .hero {
14 | background-image: url("./background.jpg");
15 | min-height: 100vh;
16 | }
17 |
18 | .editor-container {
19 | width: 300px;
20 | height: 450px;
21 | margin: 0 auto;
22 | border: 1px solid gray;
23 | }
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "extracted-loader",
3 | "version": "1.0.7",
4 | "description": "Reloads stylesheets extracted with with ExtractTextPlugin",
5 | "main": "index.js",
6 | "files": ["index.js"],
7 | "homepage": "https://github.com/sheerun/extracted-loader",
8 | "repository": {
9 | "type": "git",
10 | "url": "git://github.com/sheerun/extracted-loader.git"
11 | },
12 | "author": "sheerun",
13 | "license": "MIT",
14 | "keywords": [
15 | "webpack",
16 | "loader",
17 | "css",
18 | "scss",
19 | "styles",
20 | "stylesheets",
21 | "hotreload",
22 | "next.js"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/examples/with-next/routes.js:
--------------------------------------------------------------------------------
1 | // Order from most specific to least specific
2 |
3 | const routes = {
4 | '/': { page: '/' },
5 | '/docs/': { page: '/', query: { id: 'docs' } },
6 | '/examples/': { page: '/', query: { id: 'examples' } },
7 | '/editor/': { page: '/editor' }
8 | }
9 |
10 | function orderedRoutes () {
11 | const pathnames = Object.keys(routes).sort(
12 | (a, b) => a.split('/').length < b.split('/').length
13 | )
14 |
15 | return pathnames.map(pathname => ({
16 | pathname,
17 | ...routes[pathname]
18 | }))
19 | }
20 |
21 | function exportPathMap () {
22 | return routes
23 | }
24 |
25 | module.exports = {
26 | exportPathMap,
27 | orderedRoutes
28 | }
29 |
--------------------------------------------------------------------------------
/examples/with-next/components/link.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { orderedRoutes } from '../routes'
3 | import { default as NextLink } from 'next/link'
4 | import { format } from 'url'
5 |
6 | const routes = orderedRoutes()
7 |
8 | function propsForHref (href) {
9 | if (href[href.length - 1]) href += '/'
10 |
11 | for (const route of routes) {
12 | if (href.indexOf(route.pathname) === 0) {
13 | return {
14 | as: route.pathname,
15 | href: format({ ...route, pathname: route.page })
16 | }
17 | }
18 | }
19 |
20 | throw new Error('Unknown route: ' + href)
21 | }
22 |
23 | export default props =>
24 |
--------------------------------------------------------------------------------
/examples/with-next/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Head, Main, NextScript } from 'next/document'
2 | import flush from 'styled-jsx/server'
3 |
4 | export default class MyDocument extends Document {
5 | static getInitialProps ({ renderPage }) {
6 | const { html, head, errorHtml, chunks } = renderPage()
7 | const styles = flush()
8 | return { html, head, errorHtml, chunks, styles }
9 | }
10 |
11 | render () {
12 | const { __NEXT_DATA__ } = this.props
13 |
14 | return (
15 |
16 |
17 | {process.env.NODE_ENV === 'development' ? (
18 |
23 | ) : (
24 |
29 | )}
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | )
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/examples/with-next/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Page from '../components/page'
3 | import Navbar from '../components/navbar'
4 |
5 | import '../styles/index.scss'
6 |
7 | export default class Index extends React.Component {
8 | constructor (props) {
9 | super(props)
10 |
11 | this.state = {
12 | name: 'server'
13 | }
14 | }
15 |
16 | componentDidMount () {
17 | this.setState({ name: 'browser' })
18 | }
19 |
20 | page () {
21 | switch (this.props.url.query.id) {
22 | case 'docs':
23 | return 'Documentation'
24 | case 'examples':
25 | return 'Examples'
26 | default:
27 | return 'Home'
28 | }
29 | }
30 |
31 | render () {
32 | return (
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
{this.page()}
41 |
{this.state.name}
42 |
43 |
44 |
45 |
46 | )
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = function (source) {
2 | if (source.match('module.hot')) {
3 | return source
4 | }
5 |
6 | return (
7 | source +
8 | `;
9 | if (module.hot) {
10 | var injectCss = function injectCss(prev, href) {
11 | var link = prev.cloneNode();
12 | link.href = href;
13 | link.onload = link.onerror = function() {
14 | prev.parentNode.removeChild(prev);
15 | };
16 | prev.stale = true;
17 | prev.parentNode.insertBefore(link, prev.nextSibling);
18 | };
19 | module.hot.dispose(function() {
20 | window.__webpack_reload_css__ = true;
21 | });
22 | module.hot.accept();
23 | if (window.__webpack_reload_css__) {
24 | module.hot.__webpack_reload_css__ = false;
25 | console.log("[HMR] Reloading stylesheets...");
26 | var prefix = document.location.protocol + '//' + document.location.hostname;
27 | document
28 | .querySelectorAll("link[href][rel=stylesheet]")
29 | .forEach(function(link) {
30 | if (!link.href.match(prefix) || link.stale) return;
31 | injectCss(link, link.href.split("?")[0] + "?unix=${+new Date()}");
32 | });
33 | }
34 | }
35 | `
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/examples/with-next/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "with-next",
3 | "version": "1.0.0",
4 | "scripts": {
5 | "dev": "node bin/start",
6 | "build": "NODE_ENV=production next build && next export && node bin/bundle",
7 | "start": "cross-env NODE_ENV=production node bin/start"
8 | },
9 | "author": "Adam Stankiewicz (https://sheerun.net)",
10 | "license": "Apache-2.0",
11 | "dependencies": {
12 | "autoprefixer": "^7.1.6",
13 | "brace": "^0.11.0",
14 | "bulma": "^0.6.1",
15 | "compression": "^1.7.1",
16 | "cross-env": "^5.1.1",
17 | "css-loader": "^0.28.7",
18 | "express": "^4.16.2",
19 | "express-device": "^0.4.2",
20 | "extract-text-webpack-plugin": "^3.0.2",
21 | "extracted-loader": "^1.0.4",
22 | "file-loader": "1.1.5",
23 | "font-awesome": "^4.7.0",
24 | "fs-extra": "4.0.3",
25 | "glob-promise": "^3.3.0",
26 | "img-loader": "2.0.0",
27 | "next": "latest",
28 | "node-sass": "^4.4.0",
29 | "postcss-loader": "^2.0.7",
30 | "react": "^16.0.0",
31 | "react-ace": "^5.8.0",
32 | "react-dom": "^16.0.0",
33 | "react-icons": "^2.2.7",
34 | "sass-loader": "^6.0.6",
35 | "url-loader": "0.6.2"
36 | },
37 | "devDependencies": {
38 | "now": "^9.2.7"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/examples/with-next/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const path = require('path')
4 | const fs = require('fs-extra')
5 | const glob = require('glob-promise')
6 |
7 | let critical
8 |
9 | try {
10 | critical = require("critical")
11 | } catch(e) { }
12 |
13 | const outDir = path.resolve(__dirname, '../out')
14 | const staticDir = path.resolve(__dirname, '../.next/.static')
15 |
16 | function generate (src, filename, resolution) {
17 | return critical.generate({
18 | base: outDir,
19 | src,
20 | dest: path.join(path.dirname(src), filename),
21 | inline: true,
22 | extract: false,
23 | minify: true,
24 | inlineImages: true,
25 | width: resolution[0],
26 | height: resolution[1],
27 | penthouse: {
28 | blockJSRequests: false
29 | }
30 | })
31 | }
32 |
33 | async function main () {
34 | console.log('Copying static files to output directory...')
35 |
36 | await fs.copy(staticDir, path.join(outDir, 'static'))
37 |
38 | if (critical) {
39 | for (const index of await glob('**/index.html', { cwd: outDir })) {
40 | console.log('Genetating critical path files for ' + index + '...')
41 | await generate(index, 'index-phone.html', [520, 960])
42 | await generate(index, 'index-tablet.html', [769, 1024])
43 | await generate(index, 'index-desktop.html', [1280, 1024])
44 | }
45 | } else {
46 | console.warn('WARNING: Run "npm install critical" to enable critical CSS injecting')
47 | }
48 | }
49 |
50 | main()
51 |
--------------------------------------------------------------------------------
/examples/with-next/components/navbar.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import FaPencil from 'react-icons/lib/fa/pencil'
3 |
4 | import Link from './link'
5 |
6 | export default () => (
7 |
46 | )
47 |
--------------------------------------------------------------------------------
/examples/with-next/pages/editor.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Page from '../components/page'
3 | import Navbar from '../components/navbar'
4 | import '../styles/index.scss'
5 |
6 | const NoSSR = ({ children, placeholder }) =>
7 | typeof window === 'undefined' ? placeholder : children()
8 |
9 | export default class MapPage extends React.Component {
10 | render () {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
Heavy editor
18 |
19 | But it doesn't pollute bundle of home page
20 |
21 |
}>
22 | {() => {
23 | const AceEditor = require('react-ace').default
24 | require('brace')
25 | require('brace/mode/javascript')
26 | require('brace/theme/github')
27 |
28 | return (
29 |
38 | )
39 | }}
40 |
41 |
42 |
43 |
44 |
45 | )
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/examples/with-next/README.md:
--------------------------------------------------------------------------------
1 | # Next.js
2 |
3 | This app shows how you can use extracted stylesheets for maximum performance in production.
4 |
5 | Features:
6 |
7 | - Hot reloading in development without CSS-in-JS
8 | - 100% Page Speed score and 100% YSlow score (if put behind CDN)
9 | - Critical stylesheets injected per-device (phone, tablet, desktop)
10 | - SCSS and PostCSS with autoprefixing for styling
11 | - Content hashes for all resources for best integration with CDNs
12 | - Properly setup Cache headers for resources
13 | - Auto-bundling images referenced as `url()` in stylesheets
14 | - Auto-bundling of imported and required images
15 | - Inlining small images as Data URI (in both stylesheets and SSR page)
16 | - Properly handling multiple pages and routing between them
17 | - Ability to fallback to dynamic pages
18 | - Importing single icons from Font Awesome
19 | - GZip compression of served pages
20 |
21 | ## How to use
22 |
23 | Download the example [or clone the repo](https://github.com/zeit/next.js):
24 |
25 | ```bash
26 | curl https://codeload.github.com/zeit/next.js/tar.gz/master | tar -xz --strip=2 next.js-master/examples/with-extracted-stylesheet
27 | cd with-extracted-stylesheet
28 | ```
29 |
30 | To get this example running you just need to
31 |
32 | npm install
33 | npm run dev
34 |
35 | Visit [http://localhost:3000](http://localhost:3000) and try to modify `styles/index.scss` changing color. Your changes should be picked up instantly.
36 |
37 | ## Deploying as static website
38 |
39 | You should get complete webpage in `out` directory after running `npm run build`.
40 |
41 | It is recommended to use `node bin/start` as a server though, to get dynamic content as fallback.
42 |
43 | ## Deploying to now
44 |
45 | Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download)) with:
46 |
47 | ```bash
48 | now --npm
49 | ```
50 |
51 | To deploy with critical CSS optimization (takes longer):
52 |
53 | ```bash
54 | now --docker
55 | ```
56 |
57 | ## Demo
58 |
59 | You see this app and test with performance analytics tools under following address:
60 |
61 | [https://with-extracted-stylesheet.now.sh/](https://with-extracted-stylesheet.now.sh/)
62 |
--------------------------------------------------------------------------------
/examples/with-next/next.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
3 | const exportPathMap = require('./routes').exportPathMap
4 |
5 | module.exports = {
6 | webpack: (config, { buildId, dev }) => {
7 | config.module.rules.push({
8 | test: /\.((sa|sc|c)ss|jpg|png)$/,
9 | use: [
10 | {
11 | loader: 'emit-file-loader',
12 | options: {
13 | name: 'dist/[path][name].[ext]'
14 | }
15 | }
16 | ]
17 | })
18 |
19 | config.module.rules.push({
20 | test: /\.(jpg|png|svg)$/,
21 | use: [
22 | {
23 | loader: 'url-loader',
24 | options: {
25 | limit: 10000,
26 | name: '.static/assets/[hash].[ext]',
27 | outputPath: dev ? path.join(__dirname, '/') : undefined,
28 | publicPath: function (url) {
29 | return url.replace(/^.*.static/, '/static')
30 | }
31 | }
32 | }
33 | ]
34 | })
35 |
36 | config.module.rules.push({
37 | test: /\.(sa|sc|c)ss$/,
38 | use: ['extracted-loader'].concat(
39 | ExtractTextPlugin.extract({
40 | use: [
41 | 'babel-loader',
42 | {
43 | loader: 'css-loader',
44 | options: {
45 | url: true,
46 | minimize: !dev,
47 | sourceMap: dev,
48 | importLoaders: 2
49 | }
50 | },
51 | {
52 | loader: 'postcss-loader',
53 | options: {
54 | plugins: [
55 | require('autoprefixer')({
56 | /* options */
57 | })
58 | ]
59 | }
60 | },
61 | {
62 | loader: 'sass-loader'
63 | }
64 | ]
65 | })
66 | )
67 | })
68 |
69 | config.plugins.push(
70 | new ExtractTextPlugin({
71 | filename: dev
72 | ? path.join(__dirname, '.static/assets/index.css')
73 | : '.static/assets/' + buildId + '.css',
74 | allChunks: true
75 | })
76 | )
77 |
78 | return config
79 | },
80 | useFileSystemPublicRoutes: false,
81 | exportPathMap
82 | }
83 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # extracted-loader [![][npm-image]][npm-url]
2 |
3 | [npm-image]: http://img.shields.io/npm/v/extracted-loader.svg?style=flat-square
4 | [npm-url]: http://npmjs.org/package/extracted-loader
5 |
6 | It hotreloads extracted stylesheets extracted with `ExtractTextPlugin`.
7 |
8 | No configuration needed. A better [css-hot-loader](https://github.com/shepherdwind/css-hot-loader).
9 |
10 | ## Use case
11 |
12 | You want to hot reload only stylesheets, not the whole page. Great for editing dynamic views.
13 |
14 | ## Installation
15 |
16 | ```
17 | npm install extracted-loader --save-dev
18 | ```
19 |
20 | or
21 |
22 | ```
23 | yarn add extracted-loader --dev
24 | ```
25 |
26 | And then you can use it for example as so:
27 |
28 | ```js
29 | const isDev = process.env.NODE_ENV === 'development'
30 |
31 | config.module.rules.push({
32 | test: /\.css$/,
33 | use: ['extracted-loader'].concat(ExtractTextPlugin.extract({
34 | filename: isDev ? "[name].css" : "[name].[contenthash].css",
35 | /* Your configuration here */
36 | }))
37 | })
38 |
39 | config.plugins.push(new ExtractTextPlugin('index.css'))
40 | ```
41 |
42 | For hot reloading to work it is important **to not** use `[contenthash]` in development configuration.
43 |
44 | ## Example use with sass
45 |
46 | ```js
47 | config.module.rules.push({
48 | test: /\.(sa|sc|c)ss$/,
49 | use: ['extracted-loader'].concat(ExtractTextPlugin.extract({
50 | use: [
51 | "babel-loader",
52 | {
53 | loader: 'css-loader',
54 | options: {
55 | url: true,
56 | minimize: !isDev,
57 | sourceMap: isDev,
58 | importLoaders: 2
59 | }
60 | },
61 | {
62 | loader: 'postcss-loader',
63 | options: {
64 | sourceMap: isDev,
65 | plugins: [
66 | require('autoprefixer')({
67 | /* options */
68 | })
69 | ]
70 | }
71 | },
72 | {
73 | loader: 'sass-loader',
74 | options: {
75 | sourceMap: isDev
76 | }
77 | }
78 | ]
79 | }))
80 | })
81 |
82 | config.plugins.push(new ExtractTextPlugin('index.css'))
83 | ```
84 |
85 | ## How it works
86 |
87 | By reloading all relevant `` when extracted text changes.
88 |
89 | ## How to use with...
90 |
91 | - [next.js](https://github.com/sheerun/extracted-loader/tree/master/examples/with-next)
92 |
93 | ## Contributing
94 |
95 | Yes, please
96 |
97 | ## License
98 |
99 | MIT
100 |
--------------------------------------------------------------------------------
/examples/with-next/bin/start:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const express = require('express')
4 | const next = require('next')
5 | const fs = require('fs')
6 | const path = require('path')
7 | const compression = require('compression')
8 | const device = require('express-device')
9 |
10 | const port = parseInt(process.env.PORT, 10) || 3000
11 | const dev = process.env.NODE_ENV === 'development'
12 | const app = next({ dev })
13 | const handle = app.getRequestHandler()
14 | const rootDir = path.resolve(__dirname, '..')
15 | const outDir = path.join(rootDir, 'out')
16 | const routes = require('../routes')
17 |
18 | app.prepare().then(() => {
19 | const server = express()
20 |
21 | server.use(compression())
22 |
23 | const orderedRoutes = routes.orderedRoutes()
24 |
25 | const hasCritical = fs.existsSync(path.join(outDir, 'index-phone.html'))
26 |
27 | if (dev) {
28 | server.use('/static', express.static(path.join(rootDir, '.static')))
29 | } else {
30 | server.use('/_next/', express.static(path.join(outDir, '_next'), {
31 | maxAge: "365d",
32 | immutable: true
33 | }))
34 |
35 | // We know generated assets are immutable so we set high max-age
36 | server.use('/static/assets/', express.static(path.join(outDir, 'static/assets'), {
37 | maxAge: "365d",
38 | immutable: true
39 | }))
40 |
41 | // Other assets are mutable, but we want CDNs to cache it for at least 1m
42 | server.use('/static/', express.static(path.join(outDir, 'static'), {
43 | maxAge: "1m"
44 | }))
45 |
46 | if (hasCritical) {
47 | const criticalMiddlewares = {
48 | phone: express.static(outDir, { index: 'index-phone.html' }),
49 | tablet: express.static(outDir, { index: 'index-tablet.html' }),
50 | desktop: express.static(outDir, { index: 'index-desktop.html' })
51 | }
52 |
53 | server.use(device.capture())
54 | server.use((req, res, next) => {
55 | switch (req.device.type) {
56 | case 'phone':
57 | criticalMiddlewares.phone(req, res, next)
58 | return
59 | case 'tablet':
60 | criticalMiddlewares.tablet(req, res, next)
61 | return
62 | default:
63 | criticalMiddlewares.desktop(req, res, next)
64 | }
65 | });
66 | } else {
67 | server.use('/', express.static(path.join(rootDir, 'out')))
68 | }
69 | }
70 |
71 | // Fallback to dynamic routes
72 | orderedRoutes.forEach(({ pathname, page, query }) => {
73 | server.get(pathname + '*', (req, res) =>
74 | app.render(req, res, page, query || {}))
75 | })
76 |
77 | server.get('*', (req, res) => handle(req, res))
78 |
79 | server.listen(port, err => {
80 | if (err) throw err
81 | console.log(`> Ready on http://localhost:${port}`)
82 | })
83 | })
84 |
--------------------------------------------------------------------------------