From 0f63fc1be4444407bd9681e30f8703e5fd46853c Mon Sep 17 00:00:00 2001 From: Mike Evans Date: Fri, 17 Feb 2017 16:38:01 +0000 Subject: [PATCH] explicit position - sorts tag arrays but these always appended to head & body --- lib/config.js | 4 ++ lib/plugin.js | 6 +++ lib/sort.js | 87 ++++++++++++++++++++++++++++++++ spec/config-spec.js | 16 ++++++ spec/fixtures/sort-template.html | 13 +++++ spec/sort-spec.js | 82 ++++++++++++++++++++++++++++++ 6 files changed, 208 insertions(+) create mode 100644 lib/sort.js create mode 100644 spec/fixtures/sort-template.html create mode 100644 spec/sort-spec.js diff --git a/lib/config.js b/lib/config.js index 7c9411a..2e681b3 100644 --- a/lib/config.js +++ b/lib/config.js @@ -18,6 +18,7 @@ const DEFAULT_OPTIONS = Object.freeze({ prefetch: DEFAULT_RESOURCE_HINT_HASH, preload: DEFAULT_RESOURCE_HINT_HASH, defaultAttribute: 'sync', + defaultPosition: 'plugin', removeInlinedAssets: true }); @@ -64,6 +65,9 @@ const denormaliseValue = (value, defaultProps) => { if (value.chunks) { denormalised.chunks = value.chunks; } + if (value.position) { + denormalised.position = value.position; + } if (value.test) { return convertToArray(value.test, error); } else { diff --git a/lib/plugin.js b/lib/plugin.js index d7cc972..bf88d37 100644 --- a/lib/plugin.js +++ b/lib/plugin.js @@ -9,6 +9,8 @@ const shouldAddResourceHints = require('./resource-hints.js').shouldAddResourceH const addInitialChunkResourceHints = require('./initial-chunk-resource-hints.js'); const addAsyncChunkResourceHints = require('./async-chunk-resource-hints.js'); const elements = require('./elements.js'); +const shouldSort = require('./sort.js').shouldSort; +const sort = require('./sort.js').sort; const debugEvent = msg => { debug(`${EVENT}: ${msg}`); @@ -46,6 +48,10 @@ class ScriptExtHtmlWebpackPlugin { addAsyncChunkResourceHints(options, compilation) ]); } + if (shouldSort(options)) { + debugEvent('sorting elements'); + sort(options, pluginArgs); + } debugEvent('completed'); callback(null, pluginArgs); } catch (err) { diff --git a/lib/sort.js b/lib/sort.js new file mode 100644 index 0000000..0f58154 --- /dev/null +++ b/lib/sort.js @@ -0,0 +1,87 @@ +'use strict'; + +const hasNonDefaultPositionProperty = property => { + return typeof property === 'object' && + property.hasOwnProperty('position') && + property['position'] !== 'plugin'; +}; + +const shouldSort = (options) => { + return (options.defaultPosition === 'plugin') + ? Object.keys(options).some(key => hasNonDefaultPositionProperty(options[key])) + : true; +}; + +const sort = (options, pluginArgs) => { + const sortedTags = { + 'head-top': [], + 'head': [], + 'head-bottom': [], + 'body-top': [], + 'body': [], + 'body-bottom': [] + }; + // sort tags + const headTagPositionsByType = tagPositionsByType(options, 'head'); + pluginArgs.head.forEach(tag => { + sortedTags[headTagPositionsByType[typeOfTag(tag)]].push(tag); + }); + const bodyTagPositionsByType = tagPositionsByType(options, 'body'); + pluginArgs.body.forEach(tag => { + sortedTags[bodyTagPositionsByType[typeOfTag(tag)]].push(tag); + }); + // return to plugin args, now sorted + pluginArgs.head = sortedTags['head-top'].concat(sortedTags['head']).concat(sortedTags['head-bottom']); + pluginArgs.body = sortedTags['body-top'].concat(sortedTags['body']).concat(sortedTags['body-bottom']); +}; + +const tagPositionsByType = (options, defaultTagPosition) => { + const defaultPosition = (options.defaultPosition === 'plugin') ? defaultTagPosition : options.defaultPosition; + return { + inline: options.inline.position || defaultPosition, + sync: options.sync.position || defaultPosition, + async: options.async.position || defaultPosition, + defer: options.defer.position || defaultPosition, + module: options.module.position || defaultPosition, + prefetch: options.prefetch.position || defaultPosition, + preload: options.preload.position || defaultPosition, + other: defaultTagPosition + }; +}; + +const typeOfTag = tag => { + switch (tag.tagName) { + case 'script': + if (tag.innnerHTML) { + return 'inline'; + } else if (getAttribute(tag, 'type') === 'module') { + return 'module'; + } else if (hasAttribute(tag, 'async')) { + return 'async'; + } else if (hasAttribute(tag, 'defer')) { + return 'defer'; + } else { + return 'sync'; + } + case 'link': + switch (getAttribute(tag, 'rel')) { + case 'prefetch': + return 'prefetch'; + case 'preload': + return 'preload'; + default: + return 'other'; + } + default: + return 'other'; + } +}; + +const getAttribute = (tag, attribute) => (tag.attributes) ? tag.attributes.attribute : null; + +const hasAttribute = (tag, attribute) => tag.attributes && tag.attributes.hasOwnProperty(attribute); + +module.exports = { + shouldSort, + sort +}; diff --git a/spec/config-spec.js b/spec/config-spec.js index d55eb72..85c029a 100644 --- a/spec/config-spec.js +++ b/spec/config-spec.js @@ -63,6 +63,22 @@ describe('Correctly understands all configuration permutations', () => { expect(denormaliseOptions(options)).toEqual(expected); }); + it('handles hash configuration with position attribute', () => { + const options = { + module: { + test: '*.js', + position: 'body-top' + } + }; + const expected = Object.assign({}, DEFAULT_OPTIONS, { + module: { + test: ['*.js'], + position: 'body-top' + } + }); + expect(denormaliseOptions(options)).toEqual(expected); + }); + it('handles full hash configuration for attribute', () => { const options = { module: { diff --git a/spec/fixtures/sort-template.html b/spec/fixtures/sort-template.html new file mode 100644 index 0000000..cf38930 --- /dev/null +++ b/spec/fixtures/sort-template.html @@ -0,0 +1,13 @@ + + + + + Sort functionality tests + + +
+
+
+
+ + diff --git a/spec/sort-spec.js b/spec/sort-spec.js new file mode 100644 index 0000000..e8911c9 --- /dev/null +++ b/spec/sort-spec.js @@ -0,0 +1,82 @@ +/* eslint-env jasmine */ +'use strict'; + +const path = require('path'); +const deleteDir = require('rimraf'); +const version = require('./helpers/versions.js'); +// const webpack = require('webpack'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const ScriptExtHtmlWebpackPlugin = require('../index.js'); +const testPlugin = require('./helpers/core-test.js'); + +const OUTPUT_DIR = path.join(__dirname, '../dist'); + +const baseConfig = (scriptExtOptions, outputFilename) => { + outputFilename = outputFilename || '[name].js'; + return { + entry: { + a: path.join(__dirname, 'fixtures/script1.js'), + b: path.join(__dirname, 'fixtures/script2.js'), + c: path.join(__dirname, 'fixtures/script3.js') + }, + output: { + path: OUTPUT_DIR, + filename: outputFilename + }, + plugins: [ + new HtmlWebpackPlugin({ + template: path.join(__dirname, 'fixtures/sort-template.html') + }), + new ScriptExtHtmlWebpackPlugin(scriptExtOptions) + ] + }; +}; + +const baseExpectations = () => ({ + html: [], + js: [], + files: [], + not: { + html: [], + js: [], + files: [] + } +}); + +describe(`Sort functionality (webpack ${version.webpack})`, function () { + beforeEach((done) => { + deleteDir(OUTPUT_DIR, done); + }); + + it('default position of head-bottom works', (done) => { + const config = baseConfig( + { + defaultAttribute: 'async', + defaultPosition: 'head-bottom' + }, + 'index_bundle.js' + ); + config.entry = path.join(__dirname, 'fixtures/script1.js'); + const expected = baseExpectations(); + expected.html = [ + /(