OverviewFilesCommitsIssuesDocsReleasesPeers

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

1035 lines · javascript ref:
const path = require('bare-path')
const resolve = require('bare-module-resolve')
const lex = require('bare-module-lexer')
const { isURL, fileURLToPath, pathToFileURL } = require('bare-url')
const Bundle = require('bare-bundle')
const Protocol = require('./lib/protocol')
const constants = require('./lib/constants')
const errors = require('./lib/errors')
const binding = require('./binding')

const kind = Symbol.for('bare.module.kind')

const isWindows = Bare.platform === 'win32'

const { startsWithWindowsDriveLetter } = resolve

module.exports = exports = class Module {
  static get [kind]() {
    return 1 // Compatibility version
  }

  constructor(url) {
    this._url = url
    this._state = 0
    this._type = 0
    this._defaultType = 0
    this._main = null
    this._exports = null
    this._imports = null
    this._resolutions = null
    this._builtins = null
    this._conditions = null
    this._protocol = null
    this._cache = null
    this._source = null
    this._function = null
    this._names = null
    this._promise = null
    this._handle = null

    Object.preventExtensions(this)
  }

  get [kind]() {
    return Module[kind]
  }

  get url() {
    return this._url
  }

  get type() {
    return this._type
  }

  get defaultType() {
    return this._defaultType
  }

  get main() {
    return this._main
  }

  get exports() {
    return this._exports
  }

  set exports(value) {
    this._exports = value
  }

  get imports() {
    return this._imports
  }

  get resolutions() {
    return this._resolutions
  }

  get builtins() {
    return this._builtins
  }

  get conditions() {
    return Array.from(this._conditions)
  }

  get protocol() {
    return this._protocol
  }

  get cache() {
    return this._cache
  }

  // For Node.js compatibility
  get filename() {
    return urlToPath(this._url)
  }

  // For Node.js compatibility
  get dirname() {
    return urlToDirname(this._url)
  }

  // For Node.js compatibility
  get id() {
    return this.filename
  }

  // For Node.js compatibility
  get path() {
    return this.dirname
  }

  _transform(isImport, isDynamicImport) {
    if (isDynamicImport) {
      this._run()
    } else if (isImport) {
      this._synthesize()
    } else {
      this._evaluate()
    }

    return this
  }

  _synthesize() {
    if ((this._state & constants.states.SYNTHESIZED) !== 0) return

    this._state |= constants.states.SYNTHESIZED

    if (this._type === constants.types.MODULE) return

    const names = new Set(['default'])
    const queue = [this]
    const seen = new Set()

    while (queue.length) {
      const module = queue.pop()

      if (seen.has(module)) continue

      seen.add(module)

      switch (module._type) {
        case constants.types.SCRIPT: {
          const result = lex(module._source)

          for (const { name } of result.exports) names.add(name)

          const referrer = module

          for (const { specifier, type } of result.imports) {
            if (
              (type & lex.constants.REEXPORT) !== 0 &&
              (type & lex.constants.ADDON) === 0 &&
              (type & lex.constants.ASSET) === 0
            ) {
              const resolved = Module.resolve(specifier, referrer._url, {
                isImport: true,
                referrer
              })

              const module = Module.load(resolved, {
                isImport: true,
                referrer
              })

              if (module._names) {
                for (const name of module._names) names.add(name)
              } else {
                queue.push(module)
              }
            }
          }

          break
        }

        case constants.types.MODULE:
          module._evaluate()

          for (const name of Object.keys(module._exports)) names.add(name)

          break

        case constants.types.JSON:
          for (const name of Object.keys(module._exports)) names.add(name)
      }
    }

    this._names = Array.from(names)

    this._handle = binding.createSyntheticModule(this._url.href, this._names, Module._handle)
  }

  _evaluate() {
    if ((this._state & constants.states.EVALUATED) !== 0) return

    this._state |= constants.states.EVALUATED

    if (this._type === constants.types.SCRIPT) {
      const require = exports.createRequire(this._url, { module: this })

      this._exports = {}

      const fn = this._function // Bind to variable to ensure proper stack trace

      fn(require, this, this._exports, urlToPath(this._url), urlToDirname(this._url))
    } else if (this._type === constants.types.MODULE) {
      this._run()

      this._exports = binding.getNamespace(this._handle)
    }
  }

  _run() {
    if ((this._state & constants.states.RUN) !== 0) return

    this._state |= constants.states.RUN

    this._synthesize()

    this._promise = binding.runModule(this._handle, Module._handle, Module._onrun)
  }

  [Symbol.for('bare.inspect')]() {
    return {
      __proto__: { constructor: Module },

      url: this.url,
      type: this.type,
      defaultType: this.defaultType,
      main: this.main,
      exports: this.exports,
      imports: this.imports,
      resolutions: this.resolutions,
      builtins: this.builtins,
      conditions: this.conditions
    }
  }

  static _extensions = Object.create(null)

  static _protocol = null

  static _cache =
    module[kind] === Module[kind] ? module.cache || Object.create(null) : Object.create(null)

  static _conditions = ['bare', 'node', ...Bare.Addon.host.split('-')]

  static _handle = binding.init(this, this._onimport, this._onevaluate, this._onmeta)

  static _onimport(specifier, attributes, referrerHref, isDynamicImport) {
    const referrer = this._cache[referrerHref] || null

    if (referrer === null) {
      throw errors.MODULE_NOT_FOUND(
        `Cannot find referrer for module '${specifier}' imported from '${referrerHref}'`,
        specifier,
        referrer
      )
    }

    const resolved = this.resolve(specifier, referrer._url, {
      isImport: true,
      referrer,
      attributes
    })

    const module = this.load(resolved, {
      isImport: true,
      isDynamicImport,
      referrer,
      attributes
    })

    return isDynamicImport
      ? module._promise.then(() => binding.getNamespace(module._handle))
      : module._handle
  }

  static _onevaluate(href) {
    const module = this._cache[href] || null

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

    module._evaluate()

    for (const name of module._names) {
      let value

      if (
        name === 'default' &&
        (typeof module._exports !== 'object' ||
          module._exports === null ||
          name in module._exports === false)
      ) {
        value = module._exports
      } else {
        value = module._exports[name]
      }

      binding.setExport(module._handle, name, value)
    }
  }

  static _onmeta(href, meta) {
    const self = Module

    const module = this._cache[href] || null

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

    const referrer = module

    meta.url = module._url.href
    meta.main = module._main === module
    meta.cache = module._cache

    meta.resolve = function resolve(specifier, parentURL = referrer._url) {
      return self.resolve(specifier, toURL(parentURL, referrer._url), {
        referrer
      }).href
    }

    meta.addon = function addon(specifier = '.', parentURL = referrer._url) {
      const resolved = Bare.Addon.resolve(specifier, toURL(parentURL, referrer._url), { referrer })

      const addon = Bare.Addon.load(resolved, { referrer })

      return addon._exports
    }

    meta.addon.resolve = function resolve(specifier = '.', parentURL = referrer._url) {
      return Bare.Addon.resolve(specifier, toURL(parentURL, referrer._url), {
        referrer
      }).href
    }

    meta.addon.host = Bare.Addon.host

    meta.asset = function asset(specifier, parentURL = referrer._url) {
      return self.asset(specifier, toURL(parentURL, referrer._url), {
        referrer
      }).href
    }
  }

  static _onrun(reason, promise, err = reason) {
    if (err) {
      promise.catch(() => {}) // Don't leak the rejection

      throw err
    } else {
      promise.catch((err) =>
        queueMicrotask(() => {
          throw err
        })
      )
    }
  }

  static get protocol() {
    return this._protocol
  }

  static get cache() {
    return this._cache
  }

  static load(url, source = null, opts = {}) {
    const self = Module

    if (
      !ArrayBuffer.isView(source) &&
      !Bundle.isBundle(source) &&
      typeof source !== 'string' &&
      source !== null
    ) {
      opts = source
      source = null
    }

    const {
      isImport = false,
      isDynamicImport = false,
      referrer = null,
      attributes,
      type = typeForAttributes(attributes),
      defaultType = referrer ? referrer._defaultType : 0,
      cache = referrer ? referrer._cache : self._cache,
      main = referrer ? referrer._main : null,
      protocol = referrer ? referrer._protocol : self._protocol,
      imports = referrer ? referrer._imports : null,
      resolutions = referrer ? referrer._resolutions : null,
      builtins = referrer ? referrer._builtins : null,
      conditions = referrer ? referrer._conditions : self._conditions
    } = opts

    let module = cache[url.href] || null

    if (module !== null) {
      if (type !== 0 && type !== module._type) {
        throw errors.TYPE_INCOMPATIBLE(
          `Module '${module.url.href}' is not of type '${nameOfType(type)}'`
        )
      }

      return module._transform(isImport, isDynamicImport)
    }

    module = cache[url.href] = new Module(url)

    try {
      switch (url.protocol) {
        case 'builtin:':
          module._exports = builtins[url.pathname]
          break

        default: {
          module._defaultType = defaultType
          module._cache = cache
          module._main = main || module
          module._protocol = protocol
          module._imports = imports
          module._resolutions = resolutions
          module._builtins = builtins
          module._conditions = conditions

          if (
            typeof attributes === 'object' &&
            attributes !== null &&
            typeof attributes.imports === 'string'
          ) {
            const resolved = self.resolve(attributes.imports, referrer._url, {
              referrer: module
            })

            const imports = self.load(resolved, {
              referrer: module,
              type: constants.types.JSON
            })

            module._imports = mixinImports(module._imports, imports._exports, resolved)
          }

          let extension = canonicalExtensionForType(type) || path.extname(url.pathname)

          if (extension in self._extensions === false) {
            if (defaultType) {
              extension = canonicalExtensionForType(defaultType) || '.js'
            } else {
              extension = '.js'
            }
          }

          self._extensions[extension](module, source, referrer)
        }
      }

      return module._transform(isImport, isDynamicImport)
    } catch (err) {
      delete cache[url.href]

      throw err
    }
  }

  static resolve(specifier, parentURL, opts = {}) {
    const self = Module

    if (typeof specifier !== 'string') {
      throw new TypeError(
        `Specifier must be a string. Received type ${typeof specifier} (${specifier})`
      )
    }

    const {
      isImport = false,
      referrer = null,
      attributes,
      type = typeForAttributes(attributes),
      extensions = extensionsForType(type),
      protocol = referrer ? referrer._protocol : self._protocol,
      imports = referrer ? referrer._imports : null,
      resolutions = referrer ? referrer._resolutions : null,
      builtins = referrer ? referrer._builtins : null,
      conditions = referrer ? referrer._conditions : self._conditions
    } = opts

    const resolved = protocol.preresolve(specifier, parentURL)

    const [resolution] = protocol.resolve(resolved, parentURL, imports)

    if (resolution) return protocol.postresolve(resolution)

    const candidates = []

    for (const resolution of resolve(
      resolved,
      parentURL,
      {
        conditions: isImport ? ['import', ...conditions] : ['require', ...conditions],
        imports,
        resolutions,
        extensions,
        builtins: builtins ? Object.keys(builtins) : [],
        engines: Bare.versions
      },
      readPackage
    )) {
      candidates.push(resolution)

      switch (resolution.protocol) {
        case 'builtin:':
          return resolution
        default:
          if (protocol.exists(resolution, type)) {
            return protocol.postresolve(resolution)
          }
      }
    }

    let message = `Cannot find module '${specifier}' imported from '${parentURL.href}'`

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

    throw errors.MODULE_NOT_FOUND(message, specifier, parentURL, candidates)

    function readPackage(packageURL) {
      if (protocol.exists(packageURL, constants.types.JSON)) {
        return Module.load(packageURL, { protocol })._exports
      }

      return null
    }
  }

  static asset(specifier, parentURL, opts = {}) {
    const self = Module

    if (typeof specifier !== 'string') {
      throw new TypeError(
        `Specifier must be a string. Received type ${typeof specifier} (${specifier})`
      )
    }

    const {
      referrer = null,
      protocol = referrer ? referrer._protocol : self._protocol,
      imports = referrer ? referrer._imports : null,
      resolutions = referrer ? referrer._resolutions : null,
      conditions = referrer ? referrer._conditions : self._conditions
    } = opts

    const resolved = protocol.preresolve(specifier, parentURL)

    const [resolution] = protocol.resolve(resolved, parentURL, imports)

    if (resolution) return protocol.postresolve(resolution)

    const candidates = []

    for (const resolution of resolve(
      resolved,
      parentURL,
      {
        conditions: ['asset', ...conditions],
        imports,
        resolutions,
        engines: Bare.versions
      },
      readPackage
    )) {
      candidates.push(resolution)

      if (protocol.exists(resolution, constants.types.ASSET)) {
        return protocol.postresolve(protocol.asset ? protocol.asset(resolution) : resolution)
      }
    }

    let message = `Cannot find asset '${specifier}' imported from '${parentURL.href}'`

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

    throw errors.ASSET_NOT_FOUND(message, specifier, parentURL, candidates)

    function readPackage(packageURL) {
      if (protocol.exists(packageURL, constants.types.JSON)) {
        return Module.load(packageURL, { protocol })._exports
      }

      return null
    }
  }
}

const Module = exports

function extensionsForType(type) {
  switch (type) {
    case constants.types.SCRIPT:
      return ['.js', '.cjs']
    case constants.types.MODULE:
      return ['.js', '.mjs']
    case constants.types.JSON:
      return ['.json']
    case constants.types.BUNDLE:
      return ['.bundle']
    case constants.types.ADDON:
      return ['.bare', '.node']
    case constants.types.BINARY:
      return ['.bin']
    case constants.types.TEXT:
      return ['.txt']
    default:
      return ['.js', '.cjs', '.mjs', '.json', '.bare', '.node']
  }
}

function canonicalExtensionForType(type) {
  switch (type) {
    case constants.types.SCRIPT:
      return '.cjs'
    case constants.types.MODULE:
      return '.esm'
    case constants.types.JSON:
      return '.json'
    case constants.types.BUNDLE:
      return '.bundle'
    case constants.types.ADDON:
      return '.bare'
    case constants.types.BINARY:
      return '.bin'
    case constants.types.TEXT:
      return '.txt'
    default:
      return null
  }
}

function nameOfType(type) {
  switch (type) {
    case constants.types.SCRIPT:
      return 'script'
    case constants.types.MODULE:
      return 'module'
    case constants.types.JSON:
      return 'json'
    case constants.types.BUNDLE:
      return 'bundle'
    case constants.types.ADDON:
      return 'bare'
    case constants.types.BINARY:
      return 'binary'
    case constants.types.TEXT:
      return 'text'
    default:
      return null
  }
}

function typeForAttributes(attributes) {
  if (typeof attributes !== 'object' || attributes === null) return 0

  switch (attributes.type) {
    case 'script':
      return constants.types.SCRIPT
    case 'module':
      return constants.types.MODULE
    case 'json':
      return constants.types.JSON
    case 'bundle':
      return constants.types.BUNDLE
    case 'addon':
      return constants.types.ADDON
    case 'binary':
      return constants.types.BINARY
    case 'text':
      return constants.types.TEXT
    default:
      return 0
  }
}

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 }
}

exports.Protocol = Protocol

exports.constants = constants

// For Node.js compatibility
exports.builtinModules = []

// For Node.js compatibility
exports.isBuiltin = function isBuiltin() {
  return false
}

exports.createRequire = function createRequire(parentURL, opts = {}) {
  const self = Module

  let {
    module = null,
    referrer = null,
    type = constants.types.SCRIPT,
    defaultType = referrer ? referrer._defaultType : constants.types.SCRIPT,
    cache = referrer ? referrer._cache : self._cache,
    main = referrer ? referrer._main : null,
    protocol = referrer ? referrer._protocol : self._protocol,
    imports = referrer ? referrer._imports : null,
    resolutions = referrer ? referrer._resolutions : null,
    builtins = referrer ? referrer._builtins : null,
    conditions = referrer ? referrer._conditions : self._conditions
  } = opts

  if (module === null) {
    module = new Module(toURL(parentURL))

    module._type = type
    module._defaultType = defaultType
    module._cache = cache
    module._main = main || module
    module._protocol = protocol
    module._imports = imports
    module._resolutions = resolutions
    module._builtins = builtins
    module._conditions = conditions
  }

  referrer = module

  function require(specifier, opts = {}) {
    const attributes = opts && opts.with

    const resolved = self.resolve(specifier, referrer._url, {
      referrer,
      attributes
    })

    const module = self.load(resolved, { referrer, attributes })

    return module._exports
  }

  require.main = module._main
  require.cache = module._cache

  require.resolve = function resolve(specifier, parentURL = referrer._url) {
    return urlToPath(self.resolve(specifier, toURL(parentURL, referrer._url), { referrer }))
  }

  require.addon = function addon(specifier = '.', parentURL = referrer._url) {
    const resolved = Bare.Addon.resolve(specifier, toURL(parentURL, referrer._url), { referrer })

    const addon = Bare.Addon.load(resolved, { referrer })

    return addon._exports
  }

  require.addon.host = Bare.Addon.host

  require.addon.resolve = function resolve(specifier = '.', parentURL = referrer._url) {
    return urlToPath(
      Bare.Addon.resolve(specifier, toURL(parentURL, referrer._url), {
        referrer
      })
    )
  }

  require.asset = function asset(specifier, parentURL = referrer._url) {
    return urlToPath(self.asset(specifier, toURL(parentURL, referrer._url), { referrer }))
  }

  return require
}

Module._extensions['.js'] = function (module, source, referrer) {
  const self = Module

  const protocol = module._protocol
  const resolutions = module._resolutions

  let pkg

  for (const packageURL of resolve.lookupPackageScope(module._url, {
    resolutions
  })) {
    if (self._cache[packageURL.href]) {
      pkg = self._cache[packageURL.href]
      break
    }

    if (protocol.exists(packageURL, constants.types.JSON)) {
      pkg = self.load(packageURL, { protocol })
      break
    }
  }

  const info = (pkg && pkg._exports) || {}

  const isESM =
    // The default type is ES modules.
    constants.types.MODULE === module._defaultType ||
    // The package is explicitly declared as an ES module.
    (info && info.type === 'module')

  return self._extensions[isESM ? '.mjs' : '.cjs'](module, source, referrer)
}

Module._extensions['.cjs'] = function (module, source, referrer) {
  const protocol = module._protocol

  module._type = constants.types.SCRIPT

  if (source === null) source = protocol.read(module._url)

  if (typeof source === 'string') source = Buffer.from(source)

  module._source = source

  module._function = binding.createFunction(
    module._url.href,
    ['require', 'module', 'exports', '__filename', '__dirname'],
    source.toString(),
    0
  )
}

Module._extensions['.mjs'] = function (module, source, referrer) {
  const self = Module

  const protocol = module._protocol

  module._type = constants.types.MODULE

  if (source === null) source = protocol.read(module._url)

  if (typeof source === 'string') source = Buffer.from(source)

  module._source = source

  module._handle = binding.createModule(module._url.href, source.toString(), 0, self._handle)
}

Module._extensions['.json'] = function (module, source, referrer) {
  const protocol = module._protocol

  module._type = constants.types.JSON

  if (source === null) source = protocol.read(module._url)

  if (typeof source === 'string') source = Buffer.from(source)

  module._source = source
  module._exports = JSON.parse(source.toString())
}

Module._extensions['.bare'] = function (module, source, referrer) {
  module._type = constants.types.ADDON

  referrer = module

  module._exports = Bare.Addon.load(module._url, { referrer }).exports
}

Module._extensions['.node'] = function (module, source, referrer) {
  module._type = constants.types.ADDON

  referrer = module

  module._exports = Bare.Addon.load(module._url, { referrer }).exports
}

Module._extensions['.bundle'] = function (module, source, referrer) {
  const self = Module

  const protocol = module._protocol

  module._type = constants.types.BUNDLE

  if (source === null) source = protocol.read(module._url)

  if (typeof source === 'string') source = Buffer.from(source)

  referrer = module

  const bundle = Bundle.from(source).mount(module._url.href + '/')

  module._source = source
  module._imports = bundle.imports
  module._resolutions = bundle.resolutions

  module._protocol = protocol.extend({
    postresolve(context, url) {
      return bundle.exists(url.href) ? url : context.postresolve(url)
    },

    exists(context, url, type) {
      return bundle.exists(url.href) || context.exists(url, type)
    },

    read(context, url) {
      return bundle.read(url.href) || context.read(url)
    }
  })

  if (bundle.main) {
    module._exports = self.load(new URL(bundle.main), bundle.read(bundle.main), {
      referrer
    })._exports
  }
}

Module._extensions['.bin'] = function (module, source, referrer) {
  const protocol = module._protocol

  module._type = constants.types.BINARY

  if (source === null) source = protocol.read(module._url)

  if (typeof source === 'string') source = Buffer.from(source)

  module._source = module._exports = source
}

Module._extensions['.txt'] = function (module, source, referrer) {
  const protocol = module._protocol

  module._type = constants.types.TEXT

  if (source === null) source = protocol.read(module._url)

  if (typeof source === 'string') source = Buffer.from(source)

  module._source = source
  module._exports = source.toString()
}

Module._protocol = new Protocol({
  postresolve(url) {
    switch (url.protocol) {
      case 'file:':
        return pathToFileURL(binding.realpath(path.toNamespacedPath(fileURLToPath(url))))
      default:
        return url
    }
  },

  exists(url, type = 0) {
    switch (url.protocol) {
      case 'file:':
        return binding.exists(
          path.toNamespacedPath(fileURLToPath(url)),
          type === constants.types.ASSET ? binding.FILE | binding.DIR : binding.FILE
        )
      default:
        return false
    }
  },

  read(url) {
    switch (url.protocol) {
      case 'file:':
        return Buffer.from(binding.read(path.toNamespacedPath(fileURLToPath(url))))
      default:
        throw errors.UNKNOWN_PROTOCOL(`Cannot load module '${url.href}'`)
    }
  }
})

function toURL(value, base) {
  if (isURL(value)) return value

  if (startsWithWindowsDriveLetter(value)) {
    return pathToFileURL(value)
  }

  return URL.parse(value, base) || pathToFileURL(value)
}

function urlToPath(url) {
  if (url.protocol === 'file:') return fileURLToPath(url)

  if (isWindows) {
    if (/%2f|%5c/i.test(url.pathname)) {
      throw errors.INVALID_URL_PATH('The URL path must not include encoded \\ or / characters')
    }
  } else {
    if (/%2f/i.test(url.pathname)) {
      throw errors.INVALID_URL_PATH('The URL path must not include encoded / characters')
    }
  }

  return decodeURIComponent(url.pathname)
}

function urlToDirname(url) {
  if (url.protocol === 'file:') return path.dirname(fileURLToPath(url))

  if (isWindows) {
    if (/%2f|%5c/i.test(url.pathname)) {
      throw errors.INVALID_URL_PATH('The URL path must not include encoded \\ or / characters')
    }
  } else {
    if (/%2f/i.test(url.pathname)) {
      throw errors.INVALID_URL_PATH('The URL path must not include encoded / characters')
    }
  }

  return decodeURIComponent(new URL('.', url).pathname).replace(/\/$/, '')
}