url.js 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. const fs = require('fs')
  2. const path = require('path')
  3. // Cache for fs.lstatSync lookup.
  4. // Prevent multiple blocking IO requests that have already been calculated.
  5. const fsLstatSyncCache = {}
  6. const fsLstatSync = (source) => {
  7. fsLstatSyncCache[source] = fsLstatSyncCache[source] || fs.lstatSync(source)
  8. return fsLstatSyncCache[source]
  9. }
  10. /**
  11. * Checks if the source is a directory.
  12. * @param {string} source
  13. */
  14. function isDirectory(source) {
  15. return fsLstatSync(source).isDirectory()
  16. }
  17. /**
  18. * Checks if the source is a directory.
  19. * @param {string} source
  20. */
  21. function isSymlink(source) {
  22. return fsLstatSync(source).isSymbolicLink()
  23. }
  24. /**
  25. * Gets the possible URLs from a directory.
  26. * @param {string} urlprefix
  27. * @param {string[]} directories
  28. */
  29. function getUrlFromPagesDirectories(urlPrefix, directories) {
  30. return Array.from(
  31. // De-duplicate similar pages across multiple directories.
  32. new Set(
  33. directories
  34. .map((directory) => parseUrlForPages(urlPrefix, directory))
  35. .flat()
  36. .map(
  37. // Since the URLs are normalized we add `^` and `$` to the RegExp to make sure they match exactly.
  38. (url) => `^${normalizeURL(url)}$`
  39. )
  40. )
  41. ).map((urlReg) => {
  42. urlReg = urlReg.replace(/\[.*\]/g, '((?!.+?\\..+?).*?)')
  43. return new RegExp(urlReg)
  44. })
  45. }
  46. // Cache for fs.readdirSync lookup.
  47. // Prevent multiple blocking IO requests that have already been calculated.
  48. const fsReadDirSyncCache = {}
  49. /**
  50. * Recursively parse directory for page URLs.
  51. * @param {string} urlprefix
  52. * @param {string} directory
  53. */
  54. function parseUrlForPages(urlprefix, directory) {
  55. fsReadDirSyncCache[directory] =
  56. fsReadDirSyncCache[directory] || fs.readdirSync(directory)
  57. const res = []
  58. fsReadDirSyncCache[directory].forEach((fname) => {
  59. // TODO: this should account for all page extensions
  60. // not just js(x) and ts(x)
  61. if (/(\.(j|t)sx?)$/.test(fname)) {
  62. if (/^index(\.(j|t)sx?)$/.test(fname)) {
  63. res.push(`${urlprefix}${fname.replace(/^index(\.(j|t)sx?)$/, '')}`)
  64. }
  65. res.push(`${urlprefix}${fname.replace(/(\.(j|t)sx?)$/, '')}`)
  66. } else {
  67. const dirPath = path.join(directory, fname)
  68. if (isDirectory(dirPath) && !isSymlink(dirPath)) {
  69. res.push(...parseUrlForPages(urlprefix + fname + '/', dirPath))
  70. }
  71. }
  72. })
  73. return res
  74. }
  75. /**
  76. * Takes a URL and does the following things.
  77. * - Replaces `index.html` with `/`
  78. * - Makes sure all URLs are have a trailing `/`
  79. * - Removes query string
  80. * @param {string} url
  81. */
  82. function normalizeURL(url) {
  83. if (!url) {
  84. return
  85. }
  86. url = url.split('?')[0]
  87. url = url.split('#')[0]
  88. url = url = url.replace(/(\/index\.html)$/, '/')
  89. // Empty URLs should not be trailed with `/`, e.g. `#heading`
  90. if (url === '') {
  91. return url
  92. }
  93. url = url.endsWith('/') ? url : url + '/'
  94. return url
  95. }
  96. function execOnce(fn) {
  97. let used = false
  98. let result
  99. return (...args) => {
  100. if (!used) {
  101. used = true
  102. result = fn(...args)
  103. }
  104. return result
  105. }
  106. }
  107. module.exports = {
  108. getUrlFromPagesDirectories,
  109. normalizeURL,
  110. execOnce,
  111. }