OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-booking / node_modules / bare-module-traverse / index.js

696 lines · javascript ref:
const { lookupPackageScope, conditionMatches } = require('bare-module-resolve')
const lex = require('bare-module-lexer')
const resolve = require('./lib/resolve')
const errors = require('./lib/errors')

const constants = {
  SCRIPT: 1,
  MODULE: 2,
  JSON: 3,
  BUNDLE: 4,
  ADDON: 5,
  BINARY: 6,
  TEXT: 7,
  ASSET: 8
}

module.exports = exports = function traverse(entry, opts, readModule, listPrefix) {
  if (typeof opts === 'function') {
    listPrefix = readModule
    readModule = opts
    opts = {}
  }

  return {
    *[Symbol.iterator]() {
      const artifacts = { addons: [], assets: [] }

      const queue = [exports.module(entry, null, {}, artifacts, new Set(), opts)]

      while (queue.length > 0) {
        const generator = queue.pop()

        let next = generator.next()

        while (next.done !== true) {
          const value = next.value

          if (value.module) {
            next = generator.next(readModule(value.module))
          } else if (value.prefix) {
            const result = []

            if (typeof listPrefix === 'function') {
              for (const url of listPrefix(value.prefix)) {
                result.push(url)
              }
            } else {
              if (readModule(value.prefix) !== null) {
                result.push(value.prefix)
              }
            }

            next = generator.next(result)
          } else {
            if (value.children) queue.push(value.children)
            else yield value.dependency

            next = generator.next()
          }
        }
      }

      return artifacts
    },

    async *[Symbol.asyncIterator]() {
      const artifacts = { addons: [], assets: [] }

      const queue = [exports.module(entry, null, {}, artifacts, new Set(), opts)]

      while (queue.length > 0) {
        const generator = queue.pop()

        let next = generator.next()

        while (next.done !== true) {
          const value = next.value

          if (value.module) {
            next = generator.next(await readModule(value.module))
          } else if (value.prefix) {
            const result = []

            if (typeof listPrefix === 'function') {
              for await (const url of listPrefix(value.prefix)) {
                result.push(url)
              }
            } else {
              if ((await readModule(value.prefix)) !== null) {
                result.push(value.prefix)
              }
            }

            next = generator.next(result)
          } else {
            if (value.children) queue.push(value.children)
            else yield value.dependency

            next = generator.next()
          }
        }
      }

      return artifacts
    }
  }
}

exports.constants = constants
exports.resolve = resolve

exports.module = function* (url, source, attributes, artifacts, visited, opts = {}) {
  const { resolutions = null, defaultType = constants.SCRIPT } = opts

  if (visited.has(url.href)) return false

  if (source === null) {
    source = yield { module: url }

    if (source === null) {
      throw errors.MODULE_NOT_FOUND(`Cannot find module '${url.href}'`, url.href)
    }
  }

  visited.add(url.href)

  if (resolutions) {
    if (yield* exports.preresolved(url, source, resolutions, artifacts, visited, opts)) {
      return true
    }
  }

  attributes = attributes || {}

  const imports = {}

  let info = null

  for (const packageURL of lookupPackageScope(url, opts)) {
    const source = yield { module: packageURL }

    if (source !== null) {
      info = JSON.parse(source)

      imports['#package'] = packageURL.href

      yield {
        children: exports.package(packageURL, source, artifacts, visited, opts)
      }

      break
    }
  }

  if (typeof attributes.imports === 'string') {
    const url = new URL(attributes.imports)

    const source = yield { module: url }

    if (source !== null) {
      opts = {
        ...opts,
        imports: mixinImports(opts.imports, JSON.parse(source), url)
      }
    }
  }

  let type = 0

  if (typeof attributes.type === 'string') {
    switch (attributes.type) {
      case 'script':
        type = constants.SCRIPT
        break
      case 'module':
        type = constants.MODULE
        break
      case 'json':
        type = constants.JSON
        break
      case 'bundle':
        type = constants.BUNDLE
        break
      case 'addon':
        type = constants.ADDON
        break
      case 'binary':
        type = constants.BINARY
        break
      case 'text':
        type = constants.TEXT
        break
    }
  } else {
    const match = url.pathname.match(/\.[a-z]+$/)

    if (match !== null) {
      const [extension] = match

      switch (extension) {
        case '.js':
          const isESM =
            defaultType === constants.MODULE || (info !== null && info.type === 'module')

          type = isESM ? constants.MODULE : constants.SCRIPT
          break
        case '.cjs':
          type = constants.SCRIPT
          break
        case '.mjs':
          type = constants.MODULE
          break
        case '.json':
          type = constants.JSON
          break
        case '.bundle':
          type = constants.BUNDLE
          break
        case '.bare':
        case '.node':
          type = constants.ADDON
          break
        case '.bin':
          type = constants.BINARY
          break
        case '.txt':
          type = constants.TEXT
          break
      }
    }
  }

  const lexer = { imports: [] }

  if (type === constants.SCRIPT || type === constants.MODULE) {
    yield* exports.imports(url, source, imports, artifacts, lexer, visited, opts)
  } else if (type === constants.ADDON) {
    yield* exports.addons(url, artifacts, visited, opts)
  }

  yield {
    dependency: { url, source, imports: compressImportsMap(imports), lexer }
  }

  return true
}

exports.package = function* (url, source, artifacts, visited, opts = {}) {
  if (visited.has(url.href)) return false

  if (source === null) {
    source = yield { module: url }

    if (source === null) return false
  }

  visited.add(url.href)

  const info = JSON.parse(source)

  if (info) {
    yield { dependency: { url, source, imports: {}, lexer: { imports: [] } } }

    if (info.assets) {
      yield {
        children: exports.assets(info.assets, url, artifacts, visited, opts)
      }
    }

    return true
  }

  return false
}

exports.preresolved = function* (url, source, resolutions, artifacts, visited, opts = {}) {
  const {
    builtinProtocol = 'builtin:',
    linkedProtocol = 'linked:',
    deferredProtocol = 'deferred:'
  } = opts

  const imports = resolutions[url.href]

  if (typeof imports !== 'object' || imports === null) return false

  for (const [specifier, entry] of Object.entries(imports)) {
    const stack = [entry]

    while (stack.length > 0) {
      const entry = stack.pop()

      if (typeof entry === 'string') {
        const url = new URL(entry)

        if (specifier === '#package') {
          yield {
            children: exports.package(url, null, artifacts, visited, opts)
          }
        } else if (
          url.protocol !== builtinProtocol &&
          url.protocol !== linkedProtocol &&
          url.protocol !== deferredProtocol
        ) {
          yield {
            children: exports.module(url, null, {}, artifacts, visited, opts)
          }
        }
      } else {
        stack.unshift(...Object.values(entry))
      }
    }
  }

  yield {
    dependency: {
      url,
      source,
      imports: compressImportsMap(imports),
      lexer: { imports: [] }
    }
  }

  return true
}

exports.imports = function* (parentURL, source, imports, artifacts, lexer, visited, opts = {}) {
  const {
    resolve = exports.resolve.default,
    builtinProtocol = 'builtin:',
    linkedProtocol = 'linked:',
    deferredProtocol = 'deferred:',
    matchedConditions = []
  } = opts

  let yielded = false

  const queue = []

  for (const entry of lex(source).imports) {
    let specifier = entry.specifier
    let condition = 'default'

    if (entry.type & lex.constants.ADDON) {
      specifier = specifier || '.'
      condition = 'addon'
    } else if (entry.type & lex.constants.ASSET) {
      condition = 'asset'
    }

    if (entry.attributes.imports) {
      const specifier = entry.attributes.imports

      queue.push({
        entry: {
          type: 0,
          specifier,
          names: [],
          attributes: {},
          position: [0, 0, 0]
        },
        specifier,
        condition: 'default'
      })
    }

    lexer.imports.push(entry)

    queue.push({ entry, specifier, condition })
  }

  while (queue.length > 0) {
    const { entry, specifier, condition } = queue.shift()

    matchedConditions.push(condition)

    const resolver = resolve(entry, parentURL, { ...opts, matchedConditions })
    const candidates = []

    let next = resolver.next()
    let resolutions = 0

    while (next.done !== true) {
      const value = next.value

      if (value.package) {
        next = resolver.next(JSON.parse(yield { module: value.package }))
      } else {
        const url = value.resolution

        candidates.push(url)

        let resolved = false

        if (
          url.protocol === builtinProtocol ||
          url.protocol === linkedProtocol ||
          url.protocol === deferredProtocol
        ) {
          addResolution(imports, specifier, matchedConditions, url)

          resolved = yielded = true
        } else if (condition === 'asset') {
          const prefix = url

          for (const url of yield { prefix }) {
            yield {
              children: exports.module(url, null, {}, artifacts, visited, opts)
            }

            addURL(artifacts.assets, url)

            resolved = yielded = true
          }

          if (resolved) addResolution(imports, specifier, matchedConditions, url)
        } else {
          const source = yield { module: url }

          if (source !== null) {
            addResolution(imports, specifier, matchedConditions, url)

            let attributes = entry.attributes

            if (attributes.imports) {
              attributes = { ...attributes, imports: imports[attributes.imports].default }
            }

            yield {
              children: exports.module(url, source, attributes, artifacts, visited, opts)
            }

            resolved = yielded = true
          }
        }

        if (resolved) {
          if (condition === 'addon') addURL(artifacts.addons, url)

          resolutions++
        }

        next = resolver.next(resolved)
      }
    }

    matchedConditions.pop()

    if (resolutions === 0) {
      let message = `Cannot find ${condition === 'default' ? 'module' : condition} '${specifier}' imported from '${parentURL.href}'`

      if (candidates.length > 0) {
        message += '\nCandidates:'
        message += '\n' + candidates.map((url) => '- ' + url.href).join('\n')
      }

      switch (condition) {
        case 'addon':
          throw errors.ADDON_NOT_FOUND(message, specifier, parentURL, candidates)
        case 'asset':
          throw errors.ASSET_NOT_FOUND(message, specifier, parentURL, candidates)
        default:
          throw errors.MODULE_NOT_FOUND(message, specifier, parentURL, candidates)
      }
    }
  }

  return yielded
}

const ADDON_EXTENSION = /\.(bare|node)$/

exports.addons = function* (parentURL, artifacts, visited, opts = {}) {
  let yielded = false

  if (ADDON_EXTENSION.test(parentURL.pathname)) {
    const prefix = new URL(parentURL)

    prefix.pathname = prefix.pathname.replace(ADDON_EXTENSION, '') + '/'

    for (const url of yield { prefix }) {
      yield {
        children: exports.module(url, null, {}, artifacts, visited, opts)
      }

      addURL(artifacts.addons, url)

      yielded = true
    }
  }

  return yielded
}

exports.assets = function* (patterns, parentURL, artifacts, visited, opts = {}) {
  const matches = yield* exports.patternMatches(patterns, parentURL, [], opts)

  let yielded = false

  for (const url of matches) {
    const source = yield { module: url }

    if (source !== null) {
      addURL(artifacts.assets, url)

      yield {
        children: exports.module(url, source, {}, artifacts, visited, opts)
      }

      yielded = true
    }
  }

  return yielded
}

exports.patternMatches = function* patternMatches(pattern, parentURL, matches, opts = {}) {
  const { conditions = [], matchedConditions = [] } = opts

  if (typeof pattern === 'string') {
    let patternNegate = false
    let patternBase
    let patternTrailer

    if (pattern[0] === '!') {
      pattern = pattern.substring(1)
      patternNegate = true
    }

    const patternIndex = pattern.indexOf('*')

    if (patternIndex === -1) {
      patternBase = pattern
      patternTrailer = ''
    } else {
      patternBase = pattern.substring(0, patternIndex)
      patternTrailer = pattern.substring(patternIndex + 1)
    }

    const prefix = new URL(patternBase, parentURL)

    for (const url of yield { prefix }) {
      if (patternIndex === -1) {
        if (patternNegate) removeURL(matches, url)
        else addURL(matches, url)
      } else if (patternTrailer === '' || url.href.endsWith(patternTrailer)) {
        addURL(matches, url)
      } else if (patternNegate) {
        removeURL(matches, url)
      }
    }
  } else if (Array.isArray(pattern)) {
    for (const patternValue of pattern) {
      yield* patternMatches(patternValue, parentURL, matches, opts)
    }
  } else if (typeof pattern === 'object' && pattern !== null) {
    let yielded = false

    for (const [condition, patternValue, subset] of conditionMatches(pattern, conditions, opts)) {
      matchedConditions.push(condition)

      if (
        yield* patternMatches(patternValue, parentURL, matches, {
          ...opts,
          conditions: subset
        })
      ) {
        yielded = true
      }

      matchedConditions.pop()
    }

    if (yielded) return true
  }

  return matches
}

function addURL(array, url) {
  let lo = 0
  let hi = array.length - 1

  while (lo <= hi) {
    const mid = lo + ((hi - lo) >> 1)
    const found = array[mid]

    if (found.href === url.href) return

    if (found.href < url.href) {
      lo = mid + 1
    } else {
      hi = mid - 1
    }
  }

  array.splice(lo, 0, url)
}

function removeURL(array, url) {
  let lo = 0
  let hi = array.length - 1

  while (lo <= hi) {
    const mid = lo + ((hi - lo) >> 1)
    const found = array[mid]

    if (found.href === url.href) break

    if (found.href < url.href) {
      lo = mid + 1
    } else {
      hi = mid - 1
    }
  }

  if (array[lo].href === url.href) array.splice(lo, 1)
}

function addResolution(imports, specifier, conditions, url) {
  imports[specifier] = imports[specifier] || {}

  let current = imports[specifier]

  for (let i = 0, n = conditions.length - 1; i < n; i++) {
    const key = conditions[i]

    if (key in current === false) {
      current[key] = {}
    } else if (typeof current[key] !== 'object') {
      current[key] = { default: current[key] }
    }

    current = current[key]
  }

  const last = conditions[conditions.length - 1]

  current[last] = url.href

  if ('default' in current) {
    const value = current.default

    delete current.default

    current.default = value
  }
}

function compressImportsMap(imports) {
  const entries = []

  for (const entry of Object.entries(imports)) {
    entry[1] = compressImportsMapEntry(entry[1])

    entries.push(entry)
  }

  return Object.fromEntries(entries)
}

function compressImportsMapEntry(resolved) {
  if (typeof resolved === 'string') return resolved

  let entries = []
  let primary = null

  for (const entry of Object.entries(resolved)) {
    entry[1] = compressImportsMapEntry(entry[1])

    entries.push(entry)

    if (entry[0] === 'default') primary = entry[1]
  }

  entries = entries.filter(
    ([condition, resolved]) => condition === 'default' || resolved !== primary
  )

  if (entries.length === 1) return entries[0][1]

  return Object.fromEntries(entries)
}

function mixinImports(target, imports, url) {
  if (typeof imports === 'object' && imports !== null && 'imports' in imports) {
    imports = imports.imports
  }

  if (typeof imports !== 'object' || imports === null) {
    throw errors.INVALID_IMPORTS_MAP(`Imports map at '${url.href}' is not valid`)
  }

  return { ...target, ...imports }
}