Skip to content

Commit

Permalink
Add support for multiple critical resources (CSS & JavaScript)
Browse files Browse the repository at this point in the history
  • Loading branch information
jkphl committed Dec 16, 2017
1 parent 8352012 commit 3cddb3a
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 20 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Updated dependencies
* Fixed documentation errors
* Dropped support for Node.js v4
* Added support for multiple critical resources (CSS & JavaScript)

## 0.4.0 Feature release (2017-06-18)
* Updated dependencies
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ The sixth argument to `shortbread()` may be an object with following properties:
| Property | Type | Description |
|----------|----------|------------------------------------------------|
| prefix | String | Prefix for all resource URLs (JavaScript and CSS). Set it to `"/"` for instance in order to use root-relative paths like `<script src="/path/to/script.js">`.|
| css | Array | List of regular expressions to match critical CSS resource file names (defaults to `['\\.css$']`) |
| js | Array | List of regular expressions to match critical JavaScript resource file names (defaults to `['\\.js$']`) |


Server side load type detection
Expand Down
43 changes: 31 additions & 12 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ function makeUrlList(val) {
*
* @param {File|Array.<File>|Object.<String, File>} js [OPTIONAL] JavaScript resource(s)
* @param {File|Array.<File>|Object.<String, File>} css [OPTIONAL] CSS resource(s)
* @param {File} critical [OPTIONAL] Critical CSS resource
* @param {File|Array.<File>|Object.<File>} critical [OPTIONAL] Critical CSS / JS resource(s)
* @param {String} slot [OPTIONAL] Cookie slot
* @param {String} callback [OPTIONAL] Callback(s)
* @param {Object} config [OPTIONAL] Extended configuration
Expand All @@ -105,9 +105,13 @@ function shortbread(js, css, critical, slot, callback, config) {
const jsUrls = makeUrlList(js);
const cssFiles = makeVinylFileList(css);
const cssUrls = makeUrlList(css);
const criticalFile = isVinylFile(critical) ? critical : null;
const criticalFiles = makeVinylFileList(critical);
const cookieSlot = (typeof slot === 'string') ? (slot.trim() || null) : null;
const options = Object.assign({ prefix: '' }, config || {});
const options = Object.assign({
prefix: '',
css: ['\\.css$'],
js: ['\\.js$'],
}, config || {});
const callbackString = callback ? JSON.stringify(callback) : 'null';

if (typeof options.prefix !== 'string') {
Expand All @@ -123,7 +127,8 @@ function shortbread(js, css, critical, slot, callback, config) {
};

// Return if no resources are given
if (!jsFiles.length && !jsUrls.length && !cssFiles.length && !cssUrls.length && !criticalFile) {
if (!jsFiles.length && !jsUrls.length && !cssFiles.length && !cssUrls.length
&& !criticalFiles.length) {
return result;
}

Expand Down Expand Up @@ -152,10 +157,24 @@ function shortbread(js, css, critical, slot, callback, config) {
result.subsequent += `<script src="${jsUrl}"></script>`;
});

// 3.a Critical CSS
if (criticalFile) {
result.initial += `<style>${criticalFile.contents}</style>`;
}
// 3.a Critical CSS & JavaScript
criticalFiles.forEach((criticalFile) => {
// Detect whether it's a JavaScript resource
for (const r of options.js) {
if (criticalFile.relative.match(r)) {
result.initial += `<script>${criticalFile.contents}</script>`;
return;
}
}

// Detect whether it's a CSS resource
for (const r of options.css) {
if (criticalFile.relative.match(r)) {
result.initial += `<style>${criticalFile.contents}</style>`;
return;
}
}
});

let synchronousCSS = '';
cssFiles.forEach((cssFile) => {
Expand Down Expand Up @@ -190,7 +209,7 @@ function shortbread(js, css, critical, slot, callback, config) {
/**
* Streaming interface for shortbread
*
* @param {File} critical [OPTIONAL] Critical CSS resource
* @param {File} critical [OPTIONAL] Critical CSS or JavaScript resource(s)
* @param {String} slot [OPTIONAL] Cookie slot (optional)
* @param {String} callback [OPTIONAL] Callback(s)
* @param {Object} config [OPTIONAL] Extended configuration
Expand All @@ -210,9 +229,9 @@ shortbread.stream = function stream(critical, slot, callback, config) {
options.js = makeRegexList(options.js);
options.data = !!options.data;

// Validate the critical CSS
if (critical && !isVinylFile(critical)) {
throw new Error('shortbread.stream: Critical CSS must be a Vinyl object');
// Validate the critical CSS or JavaScript resource(s)
if (critical && !makeVinylFileList(critical).length) {
throw new Error('shortbread.stream: Critical resources must be single a Vinyl object, a Vinyl object array or object');
}

// Prepare the fragment paths
Expand Down
2 changes: 2 additions & 0 deletions test/fixtures/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ function allLoaded() {
}

document.documentElement.className += ' js';

/* criticaljs (don't remove, used for testing) */
91 changes: 83 additions & 8 deletions test/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,13 +204,71 @@ describe('shortbread()', () => {
});

describe('should support options', () => {
it('critical CSS', () => {
const criticalcss = vinyl.readSync(path.join(__dirname, 'fixtures/critical.css'));
const result = shortbread(null, null, criticalcss);
should(result.initial).be.not.empty();
should(result.initial).startWith('<style>');
should(result.initial).endWith('</style>');
should(result.initial).match(/criticalcss/);
const criticalcss = vinyl.readSync(path.join(__dirname, 'fixtures/critical.css'));
const criticaljs = vinyl.readSync(path.join(__dirname, 'fixtures/script.js'));
describe('Critical CSS', () => {
it('as Vinyl file', () => {
const result = shortbread(null, null, criticalcss);
should(result.initial).be.not.empty();
should(result.initial).startWith('<style>');
should(result.initial).endWith('</style>');
should(result.initial).match(/criticalcss/);
});
it('as Vinyl file array', () => {
const result = shortbread(null, null, [criticalcss]);
should(result.initial).be.not.empty();
should(result.initial).startWith('<style>');
should(result.initial).endWith('</style>');
should(result.initial).match(/criticalcss/);
});
it('as Vinyl file object', () => {
const result = shortbread(null, null, { critical: criticalcss });
should(result.initial).be.not.empty();
should(result.initial).startWith('<style>');
should(result.initial).endWith('</style>');
should(result.initial).match(/criticalcss/);
});
});
describe('Critical JavaScript', () => {
it('as Vinyl file', () => {
const result = shortbread(null, null, criticaljs);
should(result.initial).be.not.empty();
should(result.initial).startWith('<script>');
should(result.initial).endWith('</script>');
should(result.initial).match(/criticaljs/);
});
it('as Vinyl file array', () => {
const result = shortbread(null, null, [criticaljs]);
should(result.initial).be.not.empty();
should(result.initial).startWith('<script>');
should(result.initial).endWith('</script>');
should(result.initial).match(/criticaljs/);
});
it('as Vinyl file object', () => {
const result = shortbread(null, null, { critical: criticaljs });
should(result.initial).be.not.empty();
should(result.initial).startWith('<script>');
should(result.initial).endWith('</script>');
should(result.initial).match(/criticaljs/);
});
});
describe('Critical CSS & JavaScript', () => {
it('as Vinyl files array', () => {
const result = shortbread(null, null, [criticalcss, criticaljs]);
should(result.initial).be.not.empty();
should(result.initial).startWith('<style>');
should(result.initial).endWith('</script>');
should(result.initial).match(/criticalcss/);
should(result.initial).match(/criticaljs/);
});
it('as Vinyl files object', () => {
const result = shortbread(null, null, { css: criticalcss, js: criticaljs });
should(result.initial).be.not.empty();
should(result.initial).startWith('<style>');
should(result.initial).endWith('</script>');
should(result.initial).match(/criticalcss/);
should(result.initial).match(/criticaljs/);
});
});
it('cookie slot', () => {
const result = shortbread(null, null, null, 'test');
Expand Down Expand Up @@ -244,7 +302,7 @@ describe('shortbread().stream', () => {
});

it('should error on invalid critical CSS', () => {
shortbread.stream.bind(null, { invalid: true }).should.throw('shortbread.stream: Critical CSS must be a Vinyl object');
shortbread.stream.bind(null, { invalid: true }).should.throw('shortbread.stream: Critical resources must be single a Vinyl object, a Vinyl object array or object');
});

it('should error on streamed file', (done) => {
Expand Down Expand Up @@ -297,6 +355,23 @@ describe('shortbread().stream', () => {
});

describe('should support options', () => {
const criticalcss = vinyl.readSync(path.join(__dirname, 'fixtures/critical.css'));
const criticaljs = vinyl.readSync(path.join(__dirname, 'fixtures/script.js'));
it('critical CSS & JavaScript', (done) => {
gulp.src(['fixtures/*.js', 'fixtures/style.css'], { cwd: __dirname })
.pipe(shortbread.stream([criticalcss, criticaljs]))
.pipe(assert.length(2))
.pipe(assert.nth(0, (d) => {
should(path.basename(d.path)).eql('initial.html');
should(d.contents.toString()).match(/criticalcss/);
should(d.contents.toString()).match(/criticaljs/);
}))
.pipe(assert.nth(1, (d) => {
should(path.basename(d.path)).eql('subsequent.html');
}))
.pipe(assert.end(done));
});

it('file extension filters', (done) => {
const jsxHash = shortbread.createHash(fs.readFileSync(path.join(__dirname, 'fixtures/helloworld.jsx')));
const scssHash = shortbread.createHash(fs.readFileSync(path.join(__dirname, 'fixtures/dummy.scss')));
Expand Down

0 comments on commit 3cddb3a

Please sign in to comment.