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:
Родитель
4922de723b
Коммит
390427fcb5
|
@ -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') }),
|
||||
],
|
||||
|
|
18
yarn.lock
18
yarn.lock
|
@ -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"
|
||||
|
|
Загрузка…
Ссылка в новой задаче