Skip to content
This repository was archived by the owner on Oct 6, 2020. It is now read-only.

Commit 8d40ab4

Browse files
authored
Merge pull request #6 from theetrain/feature/options
Feature/options
2 parents ba8fc15 + 3362d43 commit 8d40ab4

File tree

11 files changed

+3843
-61
lines changed

11 files changed

+3843
-61
lines changed

.vscode/launch.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"version": "0.2.0",
3+
"configurations": [
4+
{
5+
"type": "node",
6+
"request": "launch",
7+
"name": "Gulp task",
8+
"program": "${workspaceRoot}/node_modules/gulp/bin/gulp.js",
9+
"args": [
10+
"--gulpfile", "test/gulpfile.js"
11+
]
12+
}
13+
]
14+
}

README.md

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,108 @@
11
# gulp-resource-hints
2-
Add resource hints to your html files
2+
> Add resource hints to your html files
3+
4+
## Introduction
5+
6+
[Resource hints](https://www.w3.org/TR/resource-hints/) are a great way to reduce loading times on your progressive website. At the time of this writing, only Chrome has support for the major resource hints, but `prefetch` and `dns-prefetch` have [fairly wide availability](http://caniuse.com/#search=resource%20hints) among browsers. Further reading [here](https://medium.com/@luisvieira_gmr/html5-prefetch-1e54f6dda15d).
7+
8+
## Install
9+
10+
```bash
11+
$ npm install --save-dev gulp-resource-hints
12+
```
13+
14+
## Usage
15+
16+
1. (optional) place the string `##gulp-resource-hints##` in each of your HTML files' `<head>`, ideally right after your last `<meta>` tag. If not provided, resource hints will be inserted after your last `<meta>` in your document's `<head>`, or prepended to `<head>`, whichever comes first.
17+
1. Add `gulp-resource-hints` to one of your Gulp tasks to parse your HTML files for static and external assets, and prepend them with resource hints to their respective `<head>`.
18+
19+
```js
20+
const gulp = require('gulp')
21+
const resourceHints = require('gulp-resource-hints')
22+
23+
gulp.task('resourceHints', function (cb) {
24+
return gulp.src('./app/**/*.html')
25+
.pipe(resourceHints())
26+
.pipe(gulp.dest('./dist/'))
27+
})
28+
```
29+
30+
### Input example
31+
32+
**app/index.html**
33+
```html
34+
<html>
35+
<head>
36+
##gulp-resource-hints##
37+
</head>
38+
<body>
39+
40+
<img src="asset/image1.jpg" alt="">
41+
<img src="asset/image2.jpg" alt="">
42+
<img src="asset/image3.png" alt="">
43+
<img src="asset/image4.svg" alt="">
44+
</body>
45+
</html>
46+
```
47+
48+
### Output example
49+
50+
**dist/index.html**
51+
```html
52+
<html>
53+
<head>
54+
<link rel="prefetch" href="asset/image1.jpg" /><link rel="prefetch" href="asset/image2.jpg" /><link rel="prefetch" href="asset/image3.png" /><link rel="prefetch" href="asset/image4.svg" />
55+
</head>
56+
<body>
57+
58+
<img src="asset/image1.jpg" alt="">
59+
<img src="asset/image2.jpg" alt="">
60+
<img src="asset/image3.png" alt="">
61+
<img src="asset/image4.svg" alt="">
62+
</body>
63+
</html>
64+
```
65+
66+
## Options
67+
68+
### resourceHints([options])
69+
70+
`options <Object>` - see [default options](./lib/defaults.js)
71+
72+
- `pageToken <String>` : add your own custom string replace token (default is **##gulp-resource-hints##**)
73+
- `paths <Object>` : custom string patterns for their respective resource hint.
74+
- `dns-prefetch <String>` : custom [URL pattern](#url-patterns). Default is `//*` (all non-relative URLs)
75+
- `preconnect <String>` : custom URL pattern.
76+
- `prerender <String>` : custom [glob](https://www.npmjs.com/package/glob) pattern.
77+
- `prefetch <String>` : custom glob pattern. Default is all locally-served fonts and images.
78+
- `preload <String>` : custom glob pattern.
79+
80+
### URL Patterns
81+
82+
Similar to glob, url patterns work like so:
83+
84+
```js
85+
// Example 1: single wildcard
86+
var options = {
87+
paths: {
88+
'dns-prefetch': '*unpkg.com'
89+
}
90+
}
91+
92+
'https://unpkg.com/react@15.3.1/dist/react.min.js' // match
93+
'https://unpkg.com/history@4.2.0/umd/history.min.js' // match
94+
'https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js' // no match
95+
96+
/* ----- */
97+
98+
// Example 2: comma-separated wildcards
99+
var options {
100+
paths: {
101+
preconnect: '*unpkg.com,//cdnjs.cloudflare.com*'
102+
}
103+
}
104+
105+
'https://unpkg.com/react@15.3.1/dist/react.min.js' // match
106+
'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/core.js' // match
107+
'https://cdn.jsdelivr.net/jquery/3.2.1/jquery.min.js' // no match
108+
```

lib/defaults.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module.exports = {
2+
pageToken: '##gulp-resource-hints##',
3+
getCSSAssets: true,
4+
paths: {
5+
'dns-prefetch': '//*',
6+
preconnect: '',
7+
prerender: '',
8+
prefetch: '**/*.+(png|svg|jpg|jpeg|gif|woff|woff2)',
9+
preload: ''
10+
}
11+
}

lib/helpers.js

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
const isCSS = require('is-css')
2+
const minimatch = require('minimatch')
3+
const defaults = require('./defaults')
4+
const Soup = require('soup')
5+
const parse = require('url').parse
6+
7+
var parsedAssets = []
8+
9+
var fonts = [
10+
'**/*.woff',
11+
'**/*.woff2',
12+
'**/*.ttf',
13+
'**/*.eot',
14+
'**/*.eot',
15+
'**/*.otf'
16+
]
17+
18+
// Determines if url matches glob pattern
19+
function urlMatch (asset, pattern) {
20+
var multi = pattern.split(',')
21+
var re
22+
23+
if (multi.length > 1) {
24+
for (var i = 0, len = multi.length; i < len; i++) {
25+
re = new RegExp(multi[i].replace(/([.?+^$[\]\\(){}|/-])/g, '\\$1').replace(/\*/g, '.*'))
26+
if (re.test(asset)) {
27+
return true
28+
}
29+
}
30+
return false
31+
}
32+
33+
re = new RegExp(pattern.replace(/([.?+^$[\]\\(){}|/-])/g, '\\$1').replace(/\*/g, '.*'))
34+
return re.test(asset)
35+
}
36+
37+
function urlParse (asset) {
38+
var url = parse(asset, false, true)
39+
if (url.protocol === null) {
40+
url.protocol = ''
41+
}
42+
return url.protocol + '//' + url.hostname
43+
}
44+
45+
function globMatch (file, globs) {
46+
for (let i = 0, len = globs.length; i < len; i++) {
47+
if (minimatch(file, globs[i])) {
48+
return true
49+
}
50+
}
51+
}
52+
53+
/**
54+
* Checking duplicates is necessary for dns-prefetch and prefetch
55+
* resource hints since the same web page could have multiple assets
56+
* from the same external host, but we only want to dns-prefetch an
57+
* external host once.
58+
*/
59+
function isDuplicate (assetToCheck, isHost) {
60+
return parsedAssets.find(function (asset) {
61+
if (isHost) {
62+
// We don't want to preconnect twice, eh?
63+
return asset.split('//')[1] === assetToCheck.split('//')[1]
64+
}
65+
return asset === assetToCheck
66+
})
67+
}
68+
69+
// Checks if asset matches user glob pattern
70+
// Writes resource hint if it does
71+
function buildResourceHint (hint, asset, glob) {
72+
var as = ''
73+
74+
if (hint === 'dns-prefetch' || hint === 'preconnect') {
75+
if (!urlMatch(asset, glob)) {
76+
return ''
77+
}
78+
79+
asset = urlParse(asset)
80+
if (isDuplicate(asset, true)) {
81+
return ''
82+
}
83+
} else {
84+
if (!minimatch(asset, glob) || isDuplicate(asset)) {
85+
return ''
86+
}
87+
if (globMatch(asset, fonts)) {
88+
as += ' as="font"'
89+
} else if (isCSS(asset)) {
90+
as += ' as="style"'
91+
}
92+
}
93+
94+
parsedAssets.push(asset)
95+
return `<link rel="${hint}" href="${asset}"${as} />`
96+
}
97+
module.exports.buildResourceHint = buildResourceHint
98+
99+
function mergeOptions (userOpts) {
100+
// iterate over existing keys
101+
102+
var options
103+
104+
if (!userOpts) {
105+
return defaults
106+
} else {
107+
var iterate = function (obj) {
108+
for (var key in defaults) {
109+
if (typeof obj[key] === 'object' && typeof defaults[key] !== 'object') {
110+
// Deep compare if object found
111+
iterate(obj[key])
112+
} else if (!obj[key] && defaults[key] !== '') {
113+
// Use non-blank default since there is no user option
114+
options[key] = defaults[key]
115+
} else if (typeof defaults[key] === typeof obj[key]) {
116+
// Use user-defined value only if the type matches
117+
options[key] = obj[key]
118+
}
119+
}
120+
}
121+
122+
iterate(userOpts)
123+
return options
124+
}
125+
}
126+
module.exports.mergeOptions = mergeOptions
127+
128+
function writeData (file, data, token) {
129+
if (token !== '' && String(file.contents).indexOf(token) > -1) {
130+
let html = String(file.contents).replace(token, data)
131+
return new Buffer(html)
132+
}
133+
134+
if (token !== '' && token !== defaults.pageToken) {
135+
console.warn('Provided token was not found')
136+
}
137+
var soup = new Soup(String(file.contents))
138+
var hasMeta = false
139+
var hasLink = false
140+
var hasHead = false
141+
142+
// Append after metas
143+
soup.setInnerHTML('head > meta:last-of-type', function (oldHTML) {
144+
hasMeta = true
145+
return oldHTML + data
146+
})
147+
148+
// Else, prepend before links
149+
if (!hasMeta) {
150+
soup.setInnerHTML('head > link:first-of-type', function (oldHTML) {
151+
hasLink = true
152+
return data + oldHTML
153+
})
154+
}
155+
156+
// Else, append to head
157+
if (!hasMeta && !hasLink) {
158+
soup.setInnerHTML('head', function (oldHTML) {
159+
hasHead = true
160+
return oldHTML + data
161+
})
162+
}
163+
164+
// No head? Oh noes!
165+
if (!hasHead) {
166+
console.warn('No document <head> found, cannot write resource hints. Skipping file:', file.relative)
167+
return false
168+
}
169+
170+
return soup.toString()
171+
}
172+
module.exports.writeData = writeData

0 commit comments

Comments
 (0)