diff --git a/lib/parseCommandLineOptions.js b/lib/parseCommandLineOptions.js index d705c153..a15473a0 100644 --- a/lib/parseCommandLineOptions.js +++ b/lib/parseCommandLineOptions.js @@ -26,6 +26,18 @@ module.exports = function parseCommandLineOptions(argv) { type: 'string', demand: false, }) + .options('browsers', { + describe: + "Override your projects browserslist configuration to specify which browsers to support. Controls font formats and polyfill. Defaults to browserslist's default query if your project has no browserslist configuration", + type: 'string', + demand: false, + }) + .options('formats', { + describe: + 'Font formats to use when subsetting. The default is to select the formats based on the browser capabilities as specified via --browsers or the browserslist configuration.', + type: 'array', + choices: ['woff2', 'woff', 'truetype'], + }) .options('fallbacks', { describe: 'Include fallbacks so the original font will be loaded when dynamic content gets injected at runtime. Disable with --no-fallbacks', @@ -61,12 +73,6 @@ module.exports = function parseCommandLineOptions(argv) { default: 'swap', choices: ['auto', 'block', 'swap', 'fallback', 'optional'], }) - .options('formats', { - describe: 'Font formats to use when subsetting.', - type: 'array', - default: ['woff2', 'woff'], - choices: ['woff2', 'woff', 'truetype'], - }) .options('subset-per-page', { describe: 'Create a unique subset for each page.', type: 'boolean', diff --git a/lib/subfont.js b/lib/subfont.js index 8c867500..81da31df 100644 --- a/lib/subfont.js +++ b/lib/subfont.js @@ -1,5 +1,6 @@ const AssetGraph = require('assetgraph'); const prettyBytes = require('pretty-bytes'); +const browsersList = require('browserslist'); const _ = require('lodash'); const urlTools = require('urltools'); const util = require('util'); @@ -16,7 +17,7 @@ module.exports = async function subfont( inlineFonts = false, inlineCss = false, fontDisplay = 'swap', - formats = ['woff2', 'woff'], + formats, subsetPerPage = false, inPlace = false, inputFiles = [], @@ -24,9 +25,44 @@ module.exports = async function subfont( fallbacks = true, dynamic = false, harfbuzz = false, + browsers, }, console ) { + let selectedBrowsers; + if (browsers) { + selectedBrowsers = browsersList(browsers); + } else { + // Will either pick up the browserslist config or use the defaults query + selectedBrowsers = browsersList(); + } + + if (!formats) { + formats = ['woff2']; + if ( + _.intersection( + browsersList('supports woff, not supports woff2'), + selectedBrowsers + ).length > 0 + ) { + formats.push('woff'); + } + if ( + _.intersection( + browsersList('supports ttf, not supports woff'), + selectedBrowsers + ).length > 0 + ) { + formats.push('truetype'); + } + } + + const jsPreload = + _.intersection( + browsersList('supports font-loading, not supports link-rel-preload'), + selectedBrowsers + ).length > 0; + let rootUrl = root && urlTools.urlOrFsPathToUrl(root, true); const outRoot = output && urlTools.urlOrFsPathToUrl(output, true); let inputUrls; @@ -185,6 +221,7 @@ module.exports = async function subfont( fontDisplay, subsetPerPage, formats, + jsPreload, omitFallbacks: !fallbacks, harfbuzz, dynamic, diff --git a/lib/subsetFonts.js b/lib/subsetFonts.js index bc3f368d..b1c1b725 100644 --- a/lib/subsetFonts.js +++ b/lib/subsetFonts.js @@ -647,6 +647,7 @@ async function subsetFonts( { formats = ['woff2', 'woff'], subsetPath = 'subfont/', + jsPreload = true, omitFallbacks = false, subsetPerPage, inlineFonts, @@ -1041,65 +1042,67 @@ These glyphs are used on your site, but they don't exist in the font you applied } ); - // Generate JS fallback for browser that don't support - const preloadData = unsubsettedFontUsagesToPreload.map( - (fontUsage, idx) => { - const preloadRelation = preloadRelations[idx]; - - const formatMap = { - '.woff': 'woff', - '.woff2': 'woff2', - '.ttf': 'truetype', - '.svg': 'svg', - '.eot': 'embedded-opentype', - }; - const name = fontUsage.props['font-family']; - const props = Object.keys(initialValueByProp).reduce( - (result, prop) => { - if ( - fontUsage.props[prop] !== - normalizeFontPropertyValue(prop, initialValueByProp[prop]) - ) { - result[prop] = fontUsage.props[prop]; - } - return result; - }, - {} - ); + if (jsPreload) { + // Generate JS fallback for browser that don't support + const preloadData = unsubsettedFontUsagesToPreload.map( + (fontUsage, idx) => { + const preloadRelation = preloadRelations[idx]; + + const formatMap = { + '.woff': 'woff', + '.woff2': 'woff2', + '.ttf': 'truetype', + '.svg': 'svg', + '.eot': 'embedded-opentype', + }; + const name = fontUsage.props['font-family']; + const props = Object.keys(initialValueByProp).reduce( + (result, prop) => { + if ( + fontUsage.props[prop] !== + normalizeFontPropertyValue(prop, initialValueByProp[prop]) + ) { + result[prop] = fontUsage.props[prop]; + } + return result; + }, + {} + ); - return `new FontFace( + return `new FontFace( "${name}", "url('" + "${preloadRelation.href}".toString('url') + "') format('${ - formatMap[preloadRelation.to.extension] - }')", + formatMap[preloadRelation.to.extension] + }')", ${JSON.stringify(props)} ).load().then(void 0, function () {});`; - } - ); + } + ); - const originalFontJsPreloadAsset = htmlAsset.addRelation( - { - type: 'HtmlScript', - hrefType: 'inline', - to: { - type: 'JavaScript', - text: `try{${preloadData.join('')}}catch(e){}`, + const originalFontJsPreloadAsset = htmlAsset.addRelation( + { + type: 'HtmlScript', + hrefType: 'inline', + to: { + type: 'JavaScript', + text: `try{${preloadData.join('')}}catch(e){}`, + }, }, - }, - 'before', - insertionPoint - ).to; + 'before', + insertionPoint + ).to; + + for (const [ + idx, + relation, + ] of originalFontJsPreloadAsset.outgoingRelations.entries()) { + relation.hrefType = 'rootRelative'; + relation.to = preloadRelations[idx].to; + relation.refreshHref(); + } - for (const [ - idx, - relation, - ] of originalFontJsPreloadAsset.outgoingRelations.entries()) { - relation.hrefType = 'rootRelative'; - relation.to = preloadRelations[idx].to; - relation.refreshHref(); + originalFontJsPreloadAsset.minify(); } - - originalFontJsPreloadAsset.minify(); } if (subsetFontUsages.length === 0) { @@ -1238,7 +1241,7 @@ These glyphs are used on your site, but they don't exist in the font you applied ); let cssAssetInsertion = cssRelation; - if (!inlineFonts) { + if (jsPreload && !inlineFonts) { // JS-based font preloading for browsers without support const fontFaceContructorCalls = []; diff --git a/package.json b/package.json index 075eefed..536b2a9f 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "dependencies": { "@gustavnikolaj/async-main-wrap": "^3.0.1", "assetgraph": "^6.1.1", + "browserslist": "^4.13.0", "css-font-parser": "^0.3.0", "css-font-weight-names": "^0.2.1", "css-list-helpers": "^2.0.0", diff --git a/test/subfont.js b/test/subfont.js index 0efe05ea..915a4adc 100644 --- a/test/subfont.js +++ b/test/subfont.js @@ -6,6 +6,7 @@ const httpception = require('httpception'); const AssetGraph = require('assetgraph'); const proxyquire = require('proxyquire'); const pathModule = require('path'); + const openSansBold = require('fs').readFileSync( pathModule.resolve( __dirname, @@ -793,4 +794,116 @@ describe('subfont', function () { }); }); }); + + describe('configuring via browserslist', function () { + // /~https://github.com/browserslist/browserslist#best-practices + it('should default to woff+woff2 and jsPreload:true when no config is given, due to the browserslist defaults', async function () { + const dir = pathModule.resolve( + __dirname, + '..', + 'testdata', + 'pageWithStrictCsp' + ); + const root = encodeURI(`file://${dir}`); + const mockSubsetFonts = sinon.stub().resolves({ fontInfo: [] }); + + const originalDir = process.cwd(); + process.chdir(dir); + + try { + await proxyquire('../lib/subfont', { + '../lib/subsetFonts': mockSubsetFonts, + })( + { + root, + inputFiles: [`${root}/index.html`], + silent: true, + dryRun: true, + }, + mockConsole + ); + expect(mockSubsetFonts, 'to have calls satisfying', () => { + mockSubsetFonts(expect.it('to be an object'), { + formats: ['woff2', 'woff'], + jsPreload: true, + }); + }); + } finally { + process.chdir(originalDir); + } + }); + + it('should prefer the browsers config option over browserslist configured in package.json', async function () { + const dir = pathModule.resolve( + __dirname, + '..', + 'testdata', + 'browserslistInPackageJson' + ); + const root = encodeURI(`file://${dir}`); + const mockSubsetFonts = sinon.stub().resolves({ fontInfo: [] }); + + const originalDir = process.cwd(); + process.chdir(dir); + + try { + await proxyquire('../lib/subfont', { + '../lib/subsetFonts': mockSubsetFonts, + })( + { + root, + inputFiles: [`${root}/index.html`], + silent: true, + dryRun: true, + browsers: 'IE 11, Chrome 80', + }, + mockConsole + ); + expect(mockSubsetFonts, 'to have calls satisfying', () => { + mockSubsetFonts(expect.it('to be an object'), { + formats: ['woff2', 'woff'], + jsPreload: false, + }); + }); + } finally { + process.chdir(originalDir); + } + }); + + it('should pick up the browserslist configuration from package.json', async function () { + const dir = pathModule.resolve( + __dirname, + '..', + 'testdata', + 'browserslistInPackageJson' + ); + const root = encodeURI(`file://${dir}`); + const mockSubsetFonts = sinon.stub().resolves({ fontInfo: [] }); + + const originalDir = process.cwd(); + process.chdir(dir); + + try { + await proxyquire('../lib/subfont', { + '../lib/subsetFonts': mockSubsetFonts, + })( + { + root, + inputFiles: [`${root}/index.html`], + silent: true, + dryRun: true, + }, + mockConsole + ); + expect(mockSubsetFonts, 'to have calls satisfying', () => { + mockSubsetFonts(expect.it('to be an object'), { + formats: ['woff2', 'truetype'], + jsPreload: true, + }); + }); + } finally { + process.chdir(originalDir); + } + }); + }); }); diff --git a/test/subsetFonts.js b/test/subsetFonts.js index 9aaf71bc..dcec6e61 100644 --- a/test/subsetFonts.js +++ b/test/subsetFonts.js @@ -2856,6 +2856,29 @@ describe('subsetFonts', function () { expect(warnings, 'to satisfy', []); }); + describe('with jsPreload:false', function () { + it('should not add the JavaScript-based preload "polyfill"', async function () { + const assetGraph = new AssetGraph({ + root: pathModule.resolve( + __dirname, + // '../testdata/subsetFonts/local-single/' + '../testdata/subsetFonts/unused-variant/' + ), + }); + const [htmlAsset] = await assetGraph.loadAssets('index.html'); + await assetGraph.populate({ + followRelations: { + crossorigin: false, + }, + }); + await subsetFonts(assetGraph, { + jsPreload: false, + }); + + expect(htmlAsset.text, 'not to contain', 'new FontFace'); + }); + }); + it('should error out on multiple @font-face declarations with the same family/weight/style/stretch', async function () { httpception(); diff --git a/testdata/browserslistInPackageJson/index.html b/testdata/browserslistInPackageJson/index.html new file mode 100644 index 00000000..69e9da41 --- /dev/null +++ b/testdata/browserslistInPackageJson/index.html @@ -0,0 +1,2 @@ + + diff --git a/testdata/browserslistInPackageJson/package.json b/testdata/browserslistInPackageJson/package.json new file mode 100644 index 00000000..de5a8013 --- /dev/null +++ b/testdata/browserslistInPackageJson/package.json @@ -0,0 +1,3 @@ +{ + "browserslist": ["Chrome >= 36", "Safari 5"] +}