font-stylesheet-gathering-plugin.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. var _webpack = require("next/dist/compiled/webpack/webpack");
  6. var _fontUtils = require("../../../server/font-utils");
  7. var _postcss = _interopRequireDefault(require("postcss"));
  8. var _cssnanoSimple = _interopRequireDefault(require("next/dist/compiled/cssnano-simple"));
  9. var _constants = require("../../../shared/lib/constants");
  10. var Log = _interopRequireWildcard(require("../../output/log"));
  11. function _interopRequireDefault(obj) {
  12. return obj && obj.__esModule ? obj : {
  13. default: obj
  14. };
  15. }
  16. function _getRequireWildcardCache() {
  17. if (typeof WeakMap !== "function") return null;
  18. var cache = new WeakMap();
  19. _getRequireWildcardCache = function() {
  20. return cache;
  21. };
  22. return cache;
  23. }
  24. function _interopRequireWildcard(obj) {
  25. if (obj && obj.__esModule) {
  26. return obj;
  27. }
  28. if (obj === null || typeof obj !== "object" && typeof obj !== "function") {
  29. return {
  30. default: obj
  31. };
  32. }
  33. var cache = _getRequireWildcardCache();
  34. if (cache && cache.has(obj)) {
  35. return cache.get(obj);
  36. }
  37. var newObj = {};
  38. var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
  39. for(var key in obj){
  40. if (Object.prototype.hasOwnProperty.call(obj, key)) {
  41. var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
  42. if (desc && (desc.get || desc.set)) {
  43. Object.defineProperty(newObj, key, desc);
  44. } else {
  45. newObj[key] = obj[key];
  46. }
  47. }
  48. }
  49. newObj.default = obj;
  50. if (cache) {
  51. cache.set(obj, newObj);
  52. }
  53. return newObj;
  54. }
  55. function minifyCss(css) {
  56. return (0, _postcss).default([
  57. (0, _cssnanoSimple).default({
  58. excludeAll: true,
  59. discardComments: true,
  60. normalizeWhitespace: {
  61. exclude: false
  62. }
  63. }, _postcss.default),
  64. ]).process(css, {
  65. from: undefined
  66. }).then((res)=>res.css);
  67. }
  68. function isNodeCreatingLinkElement(node) {
  69. const callee = node.callee;
  70. if (callee.type !== "Identifier") {
  71. return false;
  72. }
  73. const componentNode = node.arguments[0];
  74. if (componentNode.type !== "Literal") {
  75. return false;
  76. }
  77. // React has pragma: _jsx.
  78. // Next has pragma: __jsx.
  79. return (callee.name === "_jsx" || callee.name === "__jsx") && componentNode.value === "link";
  80. }
  81. class FontStylesheetGatheringPlugin {
  82. gatheredStylesheets = [];
  83. manifestContent = [];
  84. constructor({ isLikeServerless , adjustFontFallbacks }){
  85. this.isLikeServerless = isLikeServerless;
  86. this.adjustFontFallbacks = adjustFontFallbacks;
  87. }
  88. parserHandler = (factory)=>{
  89. const JS_TYPES = [
  90. "auto",
  91. "esm",
  92. "dynamic"
  93. ];
  94. // Do an extra walk per module and add interested visitors to the walk.
  95. for (const type of JS_TYPES){
  96. factory.hooks.parser.for("javascript/" + type).tap(this.constructor.name, (parser)=>{
  97. /**
  98. * Webpack fun facts:
  99. * `parser.hooks.call.for` cannot catch calls for user defined identifiers like `__jsx`
  100. * it can only detect calls for native objects like `window`, `this`, `eval` etc.
  101. * In order to be able to catch calls of variables like `__jsx`, first we need to catch them as
  102. * Identifier and then return `BasicEvaluatedExpression` whose `id` and `type` webpack matches to
  103. * invoke hook for call.
  104. * See: https://github.com/webpack/webpack/blob/webpack-4/lib/Parser.js#L1931-L1932.
  105. */ parser.hooks.evaluate.for("Identifier").tap(this.constructor.name, (node)=>{
  106. var ref, ref1;
  107. // We will only optimize fonts from first party code.
  108. if (parser == null ? void 0 : (ref = parser.state) == null ? void 0 : (ref1 = ref.module) == null ? void 0 : ref1.resource.includes("node_modules")) {
  109. return;
  110. }
  111. let result;
  112. if (node.name === "_jsx" || node.name === "__jsx") {
  113. result = new _webpack.BasicEvaluatedExpression();
  114. // @ts-ignore
  115. result.setRange(node.range);
  116. result.setExpression(node);
  117. result.setIdentifier(node.name);
  118. // This was added in webpack 5.
  119. result.getMembers = ()=>[];
  120. }
  121. return result;
  122. });
  123. const jsxNodeHandler = (node)=>{
  124. var ref, ref2;
  125. if (node.arguments.length !== 2) {
  126. // A font link tag has only two arguments rel=stylesheet and href='...'
  127. return;
  128. }
  129. if (!isNodeCreatingLinkElement(node)) {
  130. return;
  131. }
  132. // node.arguments[0] is the name of the tag and [1] are the props.
  133. const arg1 = node.arguments[1];
  134. const propsNode = arg1.type === "ObjectExpression" ? arg1 : undefined;
  135. const props = {};
  136. if (propsNode) {
  137. propsNode.properties.forEach((prop)=>{
  138. if (prop.type !== "Property") {
  139. return;
  140. }
  141. if (prop.key.type === "Identifier" && prop.value.type === "Literal") {
  142. props[prop.key.name] = prop.value.value;
  143. }
  144. });
  145. }
  146. if (!props.rel || props.rel !== "stylesheet" || !props.href || !_constants.OPTIMIZED_FONT_PROVIDERS.some(({ url })=>props.href.startsWith(url))) {
  147. return false;
  148. }
  149. this.gatheredStylesheets.push(props.href);
  150. const buildInfo = parser == null ? void 0 : (ref = parser.state) == null ? void 0 : (ref2 = ref.module) == null ? void 0 : ref2.buildInfo;
  151. if (buildInfo) {
  152. buildInfo.valueDependencies.set(_constants.FONT_MANIFEST, this.gatheredStylesheets);
  153. }
  154. };
  155. // React JSX transform:
  156. parser.hooks.call.for("_jsx").tap(this.constructor.name, jsxNodeHandler);
  157. // Next.js JSX transform:
  158. parser.hooks.call.for("__jsx").tap(this.constructor.name, jsxNodeHandler);
  159. // New React JSX transform:
  160. parser.hooks.call.for("imported var").tap(this.constructor.name, jsxNodeHandler);
  161. });
  162. }
  163. };
  164. apply(compiler) {
  165. this.compiler = compiler;
  166. compiler.hooks.normalModuleFactory.tap(this.constructor.name, this.parserHandler);
  167. compiler.hooks.make.tapAsync(this.constructor.name, (compilation, cb)=>{
  168. if (this.isLikeServerless) {
  169. /**
  170. * Inline font manifest for serverless case only.
  171. * For target: server drive the manifest through physical file and less of webpack magic.
  172. */ const mainTemplate = compilation.mainTemplate;
  173. mainTemplate.hooks.requireExtensions.tap(this.constructor.name, (source)=>{
  174. return `${source}
  175. // Font manifest declaration
  176. __webpack_require__.__NEXT_FONT_MANIFEST__ = ${JSON.stringify(this.manifestContent)};
  177. // Enable feature:
  178. process.env.__NEXT_OPTIMIZE_FONTS = JSON.stringify(true);`;
  179. });
  180. }
  181. compilation.hooks.finishModules.tapAsync(this.constructor.name, async (modules, modulesFinished)=>{
  182. let fontStylesheets = this.gatheredStylesheets;
  183. const fontUrls = new Set();
  184. modules.forEach((module)=>{
  185. var ref, ref3;
  186. const fontDependencies = module == null ? void 0 : (ref = module.buildInfo) == null ? void 0 : (ref3 = ref.valueDependencies) == null ? void 0 : ref3.get(_constants.FONT_MANIFEST);
  187. if (fontDependencies) {
  188. fontDependencies.forEach((v)=>fontUrls.add(v));
  189. }
  190. });
  191. fontStylesheets = Array.from(fontUrls);
  192. const fontDefinitionPromises = fontStylesheets.map((url)=>(0, _fontUtils).getFontDefinitionFromNetwork(url));
  193. this.manifestContent = [];
  194. for(let promiseIndex in fontDefinitionPromises){
  195. let css = await fontDefinitionPromises[promiseIndex];
  196. if (this.adjustFontFallbacks) {
  197. css += (0, _fontUtils).getFontOverrideCss(fontStylesheets[promiseIndex], css);
  198. }
  199. if (css) {
  200. try {
  201. const content = await minifyCss(css);
  202. this.manifestContent.push({
  203. url: fontStylesheets[promiseIndex],
  204. content
  205. });
  206. } catch (err) {
  207. Log.warn(`Failed to minify the stylesheet for ${fontStylesheets[promiseIndex]}. Skipped optimizing this font.`);
  208. console.error(err);
  209. }
  210. }
  211. }
  212. // @ts-expect-error invalid assets type
  213. compilation.assets[_constants.FONT_MANIFEST] = new _webpack.sources.RawSource(JSON.stringify(this.manifestContent, null, " "));
  214. modulesFinished();
  215. });
  216. cb();
  217. });
  218. compiler.hooks.make.tap(this.constructor.name, (compilation)=>{
  219. // @ts-ignore TODO: Remove ignore when webpack 5 is stable
  220. compilation.hooks.processAssets.tap({
  221. name: this.constructor.name,
  222. // @ts-ignore TODO: Remove ignore when webpack 5 is stable
  223. stage: _webpack.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS
  224. }, (assets)=>{
  225. assets["../" + _constants.FONT_MANIFEST] = new _webpack.sources.RawSource(JSON.stringify(this.manifestContent, null, " "));
  226. });
  227. });
  228. }
  229. }
  230. exports.FontStylesheetGatheringPlugin = FontStylesheetGatheringPlugin;
  231. //# sourceMappingURL=font-stylesheet-gathering-plugin.js.map