Fix preloading of subset fonts in production environment (#10506)

* Fix preloading of subset fonts in production environment

* Fix pluginName

* Update src/amo/server/webpack-assets-fonts.js

Co-authored-by: William Durand <will+git@drnd.me>

* Rename the file, use fs-extra

* Add tests

* Add test proving errors are caught and show up in stats.compilation.errors

* Validate plugin options with schema-utils

Co-authored-by: William Durand <will+git@drnd.me>
This commit is contained in:
Mathieu Pillard 2021-04-30 10:50:03 +02:00 коммит произвёл GitHub
Родитель 4922de723b
Коммит 390427fcb5
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 208 добавлений и 10 удалений

Просмотреть файл

@ -226,6 +226,7 @@
"regenerator-runtime": "0.13.7",
"response-time": "2.3.2",
"serialize-javascript": "5.0.1",
"schema-utils": "3.0.0",
"touch": "3.1.0",
"typescript": "4.2.4",
"ua-parser-js": "0.7.28",

Просмотреть файл

@ -126,8 +126,10 @@ export default class ServerHtml extends Component {
getFontPreload() {
const { assets, htmlLang } = this.props;
// Preload relevant minimal subset font if available for this language.
// Note the .* after '.var': this is for the contenthash that is added in
// production builds.
const subsetFontPattern = new RegExp(
`subset-([a-zA-Z-]+\\+)*${htmlLang}(\\+[a-zA-Z-]+)*.var.woff2$`,
`subset-([a-z-]+\\+)*${htmlLang}(\\+[a-z-]+)*\\.var.*\\.woff2$`,
'i',
);

Просмотреть файл

@ -0,0 +1,57 @@
import fs from 'fs-extra';
import { validate } from 'schema-utils';
// This is a webpack plugin to add .woff2 fonts in webpack-assets.json that
// webpack-isomorphic-tools generates. We can then use `assets` to reference
// these fonts in server side rendered code to preload some font subsets, like
// we can in development mode.
// schema for options object
const schema = {
type: 'object',
properties: {
webpackAssetsFileName: {
type: 'string',
},
},
'required': ['webpackAssetsFileName'],
additionalProperties: false,
};
const pluginName = 'WebpackAssetsFontsPlugin';
export default class WebpackAssetsFontsPlugin {
constructor(options = {}) {
validate(schema, options, {
name: pluginName,
baseDataPath: 'options',
});
this.webpackAssetsFileName = options.webpackAssetsFileName;
}
apply(compiler) {
compiler.hooks.done.tap(pluginName, (stats) => {
const subsetFonts = {};
const { assets, publicPath } = stats.toJson();
try {
assets.forEach((asset) => {
const {
name,
info: { sourceFilename },
} = asset;
if (name.endsWith('.woff2')) {
subsetFonts[`./${sourceFilename}`] = `${publicPath}${name}`;
}
});
const data = fs.readJsonSync(this.webpackAssetsFileName);
data.assets = { ...data.assets, ...subsetFonts };
fs.writeJsonSync(this.webpackAssetsFileName, data);
} catch (error) {
stats.compilation.errors.push(error);
}
});
}
}

Просмотреть файл

@ -0,0 +1,124 @@
import fs from 'fs-extra';
import tmp from 'tmp';
import WebpackAssetsFontsPlugin from 'amo/server/WebpackAssetsFontsPlugin';
describe(__filename, () => {
let assetsFile;
const fakeAssets = {
'styles': {
'./app.css': 'apps-somehash.css',
},
'javascript': {
'./app.js': 'apps-somehash.js',
},
'assets': {
'./app.css': '// whatever',
// Out of the box without our plugin, the final font URL (hashed with
// publicPath prepended) would be absent. Here we add one of the two to
// ensure we're overwriting whatever is already there for the font.
'./font1.woff2': '// nothing',
},
};
const fakeStats = {
compilation: {},
toJson: () => {
return {
publicPath: 'https://cdn.example.com/static/',
assets: [
{
name: 'app-somehash.js',
info: {
sourceFilename: 'app.js',
},
},
{
name: 'app-somehash.css',
info: {
sourceFilename: 'app.css',
},
},
{
name: 'font1-somehash.woff2',
info: {
sourceFilename: 'font1.woff2',
},
},
{
name: 'font2-somehash.woff2',
info: {
sourceFilename: 'font2.woff2',
},
},
],
};
},
};
const fakeCompiler = {
hooks: {
done: {
tap: (name, callback) => {
callback(fakeStats);
},
},
},
};
beforeEach(() => {
fakeStats.compilation.errors = [];
assetsFile = tmp.fileSync({ unsafeCleanup: true });
fs.writeJsonSync(assetsFile.name, fakeAssets);
});
it('adds .woff2 file info to assets json file', () => {
const plugin = new WebpackAssetsFontsPlugin({
webpackAssetsFileName: assetsFile.name,
});
plugin.apply(fakeCompiler);
expect(fakeStats.compilation.errors).toEqual([]);
const assetsAfterPlugin = fs.readJsonSync(assetsFile.name);
expect(assetsAfterPlugin.assets['./font1.woff2']).toEqual(
'https://cdn.example.com/static/font1-somehash.woff2',
);
expect(assetsAfterPlugin.assets['./font2.woff2']).toEqual(
'https://cdn.example.com/static/font2-somehash.woff2',
);
});
it('does not touch non fonts assets, javascript or styles', () => {
const plugin = new WebpackAssetsFontsPlugin({
webpackAssetsFileName: assetsFile.name,
});
plugin.apply(fakeCompiler);
expect(fakeStats.compilation.errors).toEqual([]);
const assetsAfterPlugin = fs.readJsonSync(assetsFile.name);
expect(assetsAfterPlugin.javascript).toEqual(fakeAssets.javascript);
expect(assetsAfterPlugin.styles).toEqual(fakeAssets.styles);
expect(assetsAfterPlugin.assets['./app.css']).toEqual(
fakeAssets.assets['./app.css'],
);
});
it('errors out if the assets file did not exist', () => {
const plugin = new WebpackAssetsFontsPlugin({
webpackAssetsFileName: 'foo.json',
});
plugin.apply(fakeCompiler);
expect(fakeStats.compilation.errors.length).toEqual(1);
expect(fakeStats.compilation.errors[0].code).toEqual('ENOENT');
});
it('requires a webpackAssetsFileName parameter', () => {
expect(() => new WebpackAssetsFontsPlugin()).toThrowError(
/options misses the property 'webpackAssetsFileName'/,
);
});
});

Просмотреть файл

@ -73,6 +73,16 @@ function getAssetRules({ fileLimit }) {
// If a media file is less than this size in bytes, it will be linked as a
// data: URL. Otherwise it will be linked as a separate file URL.
limit: fileLimit,
// This is the default value.
fallback: 'file-loader',
// We want to use predictable filenames for "subset" fonts.
name(resourcePath) {
if (/-subset-/.test(resourcePath)) {
return '[name].[contenthash].[ext]';
}
return '[contenthash].[ext]';
},
};
return [

Просмотреть файл

@ -10,6 +10,7 @@ import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
import SriDataPlugin from './src/amo/server/sriDataPlugin';
import { getPlugins, getRules } from './webpack-common';
import WebpackAssetsFontsPlugin from './src/amo/server/WebpackAssetsFontsPlugin';
import webpackIsomorphicToolsConfig from './src/amo/server/webpack-isomorphic-tools-config';
import { WEBPACK_ENTRYPOINT } from './src/amo/constants';
@ -73,6 +74,9 @@ export default {
chunkFilename: '[name]-[contenthash].css',
}),
new WebpackIsomorphicToolsPlugin(webpackIsomorphicToolsConfig),
new WebpackAssetsFontsPlugin({
webpackAssetsFileName: 'webpack-assets.json',
}),
new SubresourceIntegrityPlugin({ hashFuncNames: ['sha512'] }),
new SriDataPlugin({ saveAs: path.join(DIST_DIR, 'sri.json') }),
],

Просмотреть файл

@ -11238,6 +11238,15 @@ scheduler@^0.19.1:
loose-envify "^1.1.0"
object-assign "^4.1.1"
schema-utils@3.0.0, schema-utils@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.0.0.tgz#67502f6aa2b66a2d4032b4279a2944978a0913ef"
integrity sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==
dependencies:
"@types/json-schema" "^7.0.6"
ajv "^6.12.5"
ajv-keywords "^3.5.2"
schema-utils@^2.6.5:
version "2.7.1"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
@ -11247,15 +11256,6 @@ schema-utils@^2.6.5:
ajv "^6.12.4"
ajv-keywords "^3.5.2"
schema-utils@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.0.0.tgz#67502f6aa2b66a2d4032b4279a2944978a0913ef"
integrity sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==
dependencies:
"@types/json-schema" "^7.0.6"
ajv "^6.12.5"
ajv-keywords "^3.5.2"
scss-tokenizer@^0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1"