no-page-custom-font.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. const NodeAttributes = require('../utils/node-attributes.js')
  2. const { sep, posix } = require('path')
  3. const url = 'https://nextjs.org/docs/messages/no-page-custom-font'
  4. module.exports = {
  5. meta: {
  6. docs: {
  7. description: 'Prevent page-only custom fonts.',
  8. recommended: true,
  9. url,
  10. },
  11. type: 'problem',
  12. schema: [],
  13. },
  14. create: function (context) {
  15. const paths = context.getFilename().split('pages')
  16. const page = paths[paths.length - 1]
  17. // outside of a file within `pages`, bail
  18. if (!page) {
  19. return {}
  20. }
  21. const is_Document =
  22. page.startsWith(`${sep}_document`) ||
  23. page.startsWith(`${posix.sep}_document`)
  24. let documentImportName
  25. let localDefaultExportId
  26. let exportDeclarationType
  27. return {
  28. ImportDeclaration(node) {
  29. if (node.source.value === 'next/document') {
  30. const documentImport = node.specifiers.find(
  31. ({ type }) => type === 'ImportDefaultSpecifier'
  32. )
  33. if (documentImport && documentImport.local) {
  34. documentImportName = documentImport.local.name
  35. }
  36. }
  37. },
  38. ExportDefaultDeclaration(node) {
  39. exportDeclarationType = node.declaration.type
  40. if (node.declaration.type === 'FunctionDeclaration') {
  41. localDefaultExportId = node.declaration.id
  42. return
  43. }
  44. if (
  45. node.declaration.type === 'ClassDeclaration' &&
  46. node.declaration.superClass &&
  47. node.declaration.superClass.name === documentImportName
  48. ) {
  49. localDefaultExportId = node.declaration.id
  50. }
  51. },
  52. JSXOpeningElement(node) {
  53. if (node.name.name !== 'link') {
  54. return
  55. }
  56. const ancestors = context.getAncestors()
  57. // if `export default <name>` is further down within the file after the
  58. // currently traversed component, then `localDefaultExportName` will
  59. // still be undefined
  60. if (!localDefaultExportId) {
  61. // find the top level of the module
  62. const program = ancestors.find(
  63. (ancestor) => ancestor.type === 'Program'
  64. )
  65. // go over each token to find the combination of `export default <name>`
  66. for (let i = 0; i <= program.tokens.length - 1; i++) {
  67. if (localDefaultExportId) {
  68. break
  69. }
  70. const token = program.tokens[i]
  71. if (token.type === 'Keyword' && token.value === 'export') {
  72. const nextToken = program.tokens[i + 1]
  73. if (
  74. nextToken &&
  75. nextToken.type === 'Keyword' &&
  76. nextToken.value === 'default'
  77. ) {
  78. const maybeIdentifier = program.tokens[i + 2]
  79. if (maybeIdentifier && maybeIdentifier.type === 'Identifier') {
  80. localDefaultExportId = { name: maybeIdentifier.value }
  81. }
  82. }
  83. }
  84. }
  85. }
  86. const parentComponent = ancestors.find((ancestor) => {
  87. // export default class ... extends ...
  88. if (exportDeclarationType === 'ClassDeclaration') {
  89. return (
  90. ancestor.type === exportDeclarationType &&
  91. ancestor.superClass &&
  92. ancestor.superClass.name === documentImportName
  93. )
  94. }
  95. // export default function ...
  96. if (exportDeclarationType === 'FunctionDeclaration') {
  97. return (
  98. ancestor.type === exportDeclarationType &&
  99. isIdentifierMatch(ancestor.id, localDefaultExportId)
  100. )
  101. }
  102. // function ...() {} export default ...
  103. // class ... extends ...; export default ...
  104. return isIdentifierMatch(ancestor.id, localDefaultExportId)
  105. })
  106. // file starts with _document and this <link /> is within the default export
  107. if (is_Document && parentComponent) {
  108. return
  109. }
  110. const attributes = new NodeAttributes(node)
  111. if (!attributes.has('href') || !attributes.hasValue('href')) {
  112. return
  113. }
  114. const hrefValue = attributes.value('href')
  115. const isGoogleFont =
  116. typeof hrefValue === 'string' &&
  117. hrefValue.startsWith('https://fonts.googleapis.com/css')
  118. if (isGoogleFont) {
  119. const end = `This is discouraged. See: ${url}`
  120. const message = is_Document
  121. ? `Using \`<link />\` outside of \`<Head>\` will disable automatic font optimization. ${end}`
  122. : `Custom fonts not added in \`pages/_document.js\` will only load for a single page. ${end}`
  123. context.report({
  124. node,
  125. message,
  126. })
  127. }
  128. },
  129. }
  130. },
  131. }
  132. function isIdentifierMatch(id1, id2) {
  133. return (id1 === null && id2 === null) || (id1 && id2 && id1.name === id2.name)
  134. }