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 @@ + + +
+ +