Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use browserslist to configure which font formats to supply subsets and fallbacks in, and whether to add the JS-based preload polyfill #120

Merged
merged 8 commits into from
Aug 1, 2020
23 changes: 17 additions & 6 deletions lib/parseCommandLineOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@ module.exports = function parseCommandLineOptions(argv) {
type: 'string',
demand: false,
})
.options('browsers', {
describe:
"Specify which browsers to support. Controls which font formats to provide, and whether to apply a JavaScript-based polyfill for preloading fonts. Defaults to browserslist's default query: /~https://github.com/browserslist/browserslist#best-practices",
papandreou marked this conversation as resolved.
Show resolved Hide resolved
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('js-preload', {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we should expose this option. I'd rather promote usage of browserslist project configuration to have the tool just do the right thing

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, yeah, I agree. I've taken it out in dccddcf

describe:
'Whether to include a JavaScript-based "polyfill" for browsers that do not support <link rel=preload>. The default is based on whether --browsers or the browserslist configuration specifies a browser that needs it. Disable with --no-js-preload',
type: 'boolean',
})
.options('fallbacks', {
describe:
'Include fallbacks so the original font will be loaded when dynamic content gets injected at runtime. Disable with --no-fallbacks',
Expand Down Expand Up @@ -61,12 +78,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',
Expand Down
42 changes: 41 additions & 1 deletion lib/subfont.js
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -16,17 +17,55 @@ module.exports = async function subfont(
inlineFonts = false,
inlineCss = false,
fontDisplay = 'swap',
formats = ['woff2', 'woff'],
formats,
jsPreload,
subsetPerPage = false,
inPlace = false,
inputFiles = [],
recursive = false,
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'),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't seen this interface before. That's pretty awesome!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's pretty nifty. A shame that we have to use lodash to combine it with the user's query though.

selectedBrowsers
).length > 0
) {
formats.push('woff');
}
if (
_.intersection(
browsersList('supports ttf, not supports woff'),
selectedBrowsers
).length > 0
) {
formats.push('truetype');
}
}

if (jsPreload === undefined) {
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;
Expand Down Expand Up @@ -185,6 +224,7 @@ module.exports = async function subfont(
fontDisplay,
subsetPerPage,
formats,
jsPreload,
omitFallbacks: !fallbacks,
harfbuzz,
dynamic,
Expand Down
105 changes: 54 additions & 51 deletions lib/subsetFonts.js
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,7 @@ async function subsetFonts(
{
formats = ['woff2', 'woff'],
subsetPath = 'subfont/',
jsPreload = true,
omitFallbacks = false,
subsetPerPage,
inlineFonts,
Expand Down Expand Up @@ -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 <link rel="preload">
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 <link rel="preload">
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) {
Expand Down Expand Up @@ -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 <link rel="preload"> support
const fontFaceContructorCalls = [];

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
113 changes: 113 additions & 0 deletions test/subfont.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
});
});
});
Loading