const errors = require('./lib/errors')
const kind = Symbol.for('bare.bundle.kind')
class MemoryFile {
constructor(data, opts = {}) {
const { executable = false, mode = executable ? 0o755 : 0o644 } = opts
this._data = typeof data === 'string' ? Buffer.from(data) : data
this._mode = mode
}
size() {
return this._data.byteLength
}
mode() {
return this._mode
}
read() {
return this._data
}
inspect() {
return {
__proto__: { constructor: MemoryFile },
data: this._data,
mode: this._mode.toString(8)
}
}
[Symbol.for('bare.inspect')]() {
return this.inspect()
}
[Symbol.for('nodejs.util.inspect.custom')]() {
return this.inspect()
}
}
module.exports = exports = class Bundle {
static get [kind]() {
return 0 // Compatibility version
}
static get version() {
return 0
}
constructor(opts = {}) {
const { File = MemoryFile } = opts
this._File = File
this._id = null
this._main = null
this._imports = {}
this._resolutions = {}
this._addons = []
this._assets = []
this._files = new Map()
}
get [kind]() {
return Bundle[kind]
}
get version() {
return Bundle.version
}
get id() {
return this._id
}
set id(value) {
if (typeof value !== 'string' && value !== null) {
throw new TypeError(`ID must be a string or null. Received type ${typeof value} (${value})`)
}
this._id = value
}
get main() {
return this._main
}
set main(value) {
if (typeof value !== 'string' && value !== null) {
throw new TypeError(`Main must be a string or null. Received type ${typeof value} (${value})`)
}
this._main = value
}
get imports() {
return this._imports
}
set imports(value) {
this._imports = cloneImportsMap(value)
}
get resolutions() {
return this._resolutions
}
set resolutions(value) {
this._resolutions = cloneResolutionsMap(value)
}
get addons() {
return this._addons
}
set addons(value) {
this._addons = cloneFilesList(value, 'Addons')
}
get assets() {
return this._assets
}
set assets(value) {
this._assets = cloneFilesList(value, 'Assets')
}
get files() {
return Object.fromEntries(this._files.entries())
}
*[Symbol.iterator]() {
for (const [key, file] of this._files) {
yield [key, file.read(), file.mode()]
}
}
empty() {
return this._files.size === 0
}
keys() {
return this._files.keys()
}
exists(key) {
return this._files.has(key)
}
size(key) {
const file = this._files.get(key) || null
if (file === null) return 0
return file.size()
}
mode(key) {
const file = this._files.get(key) || null
if (file === null) return 0
return file.mode()
}
read(key) {
const file = this._files.get(key) || null
if (file === null) return null
return file.read()
}
write(key, data, opts = {}) {
if (typeof key !== 'string') {
throw new TypeError(`File path must be a string. Received type ${typeof key} (${key})`)
}
const { main = false, alias = null, imports = null, addon = false, asset = false } = opts
this._files.set(key, new MemoryFile(data, opts))
if (main) this._main = key
if (alias) this._imports[alias] = key
if (imports) this._resolutions[key] = cloneImportsMap(imports)
if (addon) this._addons.push(key)
if (asset) this._assets.push(key)
return this
}
mount(root, opts = {}) {
const bundle = new Bundle()
// Go through the private API properties as we're operating on already
// validated values.
bundle._File = this._File
bundle._id = this._id
if (this._main) bundle._main = mountSpecifier(this._main, root)
bundle._imports = transformImportsMap(this._imports, root, null, opts, mountSpecifier)
bundle._resolutions = transformResolutionsMap(this._resolutions, root, opts, mountSpecifier)
for (const [key, file] of this._files) {
bundle._files.set(mountSpecifier(key, root), file)
}
bundle._addons = transformFilesList(this._addons, root, mountSpecifier)
bundle._assets = transformFilesList(this._assets, root, mountSpecifier)
return bundle
}
unmount(root, opts = {}) {
const bundle = new Bundle()
// Go through the private API properties as we're operating on already
// validated values.
bundle._File = this._File
bundle._id = this._id
if (this._main) bundle._main = unmountSpecifier(this._main, root)
bundle._imports = transformImportsMap(this._imports, root, null, opts, unmountSpecifier)
bundle._resolutions = transformResolutionsMap(this._resolutions, root, opts, unmountSpecifier)
for (const [key, file] of this._files) {
bundle._files.set(unmountSpecifier(key, root), file)
}
bundle._addons = transformFilesList(this._addons, root, unmountSpecifier)
bundle._assets = transformFilesList(this._assets, root, unmountSpecifier)
return bundle
}
toBuffer(opts = {}) {
const { indent = 0, shared = false } = opts
const header = {
version: Bundle.version,
id: this._id,
main: this._main,
imports: cloneImportsMap(this._imports),
resolutions: cloneResolutionsMap(this._resolutions),
addons: cloneFilesList(this._addons, 'Addons'),
assets: cloneFilesList(this._assets, 'Assets'),
files: {}
}
const keys = [...this._files.keys()].sort()
let offset = 0
for (const key of keys) {
const length = this.size(key)
header.files[key] = { offset, length, mode: this.mode(key) }
offset += length
}
const json = Buffer.from(`\n${JSON.stringify(header, null, indent)}\n`)
const length = Buffer.from(json.byteLength.toString(10))
const total = length.byteLength + json.byteLength + offset
const storage = shared ? new SharedArrayBuffer(total) : new ArrayBuffer(total)
const buffer = Buffer.from(storage)
offset = 0
buffer.set(length, offset)
offset += length.byteLength
buffer.set(json, offset)
offset += json.byteLength
for (const key of keys) {
buffer.set(this.read(key), offset)
offset += this.size(key)
}
return buffer
}
inspect() {
return {
__proto__: { constructor: Bundle },
version: this.version,
id: this.id,
main: this.main,
imports: this.imports,
resolutions: this.resolutions,
addons: this.addons,
assets: this.assets,
files: this.files
}
}
[Symbol.for('bare.inspect')]() {
return this.inspect()
}
[Symbol.for('nodejs.util.inspect.custom')]() {
return this.inspect()
}
}
const Bundle = exports
exports.errors = errors
exports.isBundle = function isBundle(value) {
if (value instanceof Bundle) return true
return typeof value === 'object' && value !== null && value[kind] === Bundle[kind]
}
exports.from = function from(value) {
// from(string)
if (typeof value === 'string') return fromString(value)
// from(buffer)
if (Buffer.isBuffer(value)) return fromBuffer(value)
// from(bundle)
return value
}
function fromString(string) {
return fromBuffer(Buffer.from(string))
}
function fromBuffer(buffer) {
if (buffer[0] === 0x23 /* # */ && buffer[1] === 0x21 /* ! */) {
let end = 2
while (buffer[end] !== 0xa /* \n */) end++
buffer = buffer.subarray(end + 1)
}
let end = 0
while (isDecimal(buffer[end])) end++
const len = parseInt(buffer.toString('utf8', 0, end), 10)
let header
try {
header = JSON.parse(buffer.toString('utf8', end, end + len))
} catch (err) {
throw errors.INVALID_BUNDLE_HEADER('Invalid bundle header', err)
}
const bundle = new Bundle()
// Go through the public API setters to ensure that the header fields are
// validated.
if (header.id) bundle.id = header.id
if (header.main) bundle.main = header.main
if (header.imports) bundle.imports = header.imports
if (header.resolutions) bundle.resolutions = header.resolutions
if (header.addons) bundle.addons = header.addons
if (header.assets) bundle.assets = header.assets
let offset = end + len
for (const [file, info] of Object.entries(header.files)) {
bundle.write(file, buffer.subarray(offset, offset + info.length), {
mode: info.mode || 0o644
})
offset += info.length
}
return bundle
}
function isDecimal(c) {
return c >= 0x30 && c <= 0x39
}
function compareKeys([a], [b]) {
return a > b ? 1 : a < b ? -1 : 0
}
function cloneImportsMap(value) {
if (typeof value === 'object' && value !== null) {
const imports = {}
for (const entry of Object.entries(value).sort(compareKeys)) {
imports[entry[0]] = cloneImportsMapEntry(entry[1])
}
return imports
}
throw new TypeError(`Imports map must be an object. Received type ${typeof value} (${value})`)
}
function cloneImportsMapEntry(value) {
if (typeof value === 'string') return value
if (typeof value === 'object' && value !== null) {
const imports = {}
for (const entry of Object.entries(value)) {
imports[entry[0]] = cloneImportsMapEntry(entry[1])
}
return imports
}
throw new TypeError(
`Imports map entry must be a string or object. Received type ${typeof value} (${value})`
)
}
function cloneResolutionsMap(value) {
if (typeof value === 'object' && value !== null) {
const resolutions = {}
for (const entry of Object.entries(value).sort(compareKeys)) {
resolutions[entry[0]] = cloneImportsMap(entry[1])
}
return resolutions
}
throw new TypeError(`Resolutions map must be an object. Received type ${typeof value} (${value})`)
}
function cloneFilesList(value, name) {
if (Array.isArray(value)) {
const files = []
for (const entry of value) {
if (typeof entry !== 'string') {
throw new TypeError(
`${name} entry must be a string. Received type ${typeof entry} (${entry})`
)
}
files.push(entry)
}
return files.sort()
}
throw new TypeError(`${name} list must be an array. Received type ${typeof value} (${value})`)
}
function transformImportsMap(value, root, conditionalRoot, opts, fn) {
const { conditions = {} } = opts
const imports = {}
for (const entry of Object.entries(value)) {
const condition = entry[0]
imports[condition] = transformImportsMapEntry(
entry[1],
root,
conditionalRoot || conditions[condition],
opts,
fn
)
}
return imports
}
function transformImportsMapEntry(value, root, conditionalRoot, opts, fn) {
const { conditions = {} } = opts
if (typeof value === 'string') {
return fn(value, conditionalRoot || conditions.default || root)
}
return transformImportsMap(value, root, conditionalRoot, opts, fn)
}
function transformResolutionsMap(value, root, opts, fn) {
const resolutions = {}
for (const entry of Object.entries(value)) {
resolutions[fn(entry[0], root)] = transformImportsMap(entry[1], root, null, opts, fn)
}
return resolutions
}
function transformFilesList(value, root, fn) {
const files = []
for (const entry of value) {
files.push(fn(entry, root))
}
return files
}
function mountSpecifier(specifier, root) {
if (startsWithWindowsDriveLetter(specifier)) {
specifier = '/' + specifier
}
if (specifier[0] === '/' || specifier[0] === '\\') {
specifier = '.' + specifier
}
if (specifier.startsWith('./') || specifier.startsWith('.\\')) {
return new URL(specifier, root).href
}
return specifier
}
function unmountSpecifier(specifier, root) {
specifier = new URL(specifier)
if (typeof root === 'string') root = new URL(root)
if (
specifier.protocol !== root.protocol ||
specifier.host !== root.host ||
specifier.port !== root.port
) {
return specifier.href
}
const specifierPath = splitPath(specifier.pathname)
const rootPath = splitPath(root.pathname)
while (specifierPath.length > 0 && rootPath[0] === specifierPath[0]) {
specifierPath.shift()
rootPath.shift()
}
rootPath.fill('..')
return '/' + rootPath.concat(specifierPath).join('/')
}
function splitPath(path) {
const parts = path.split('/')
if (!parts[0]) parts.shift()
if (!parts[parts.length - 1]) parts.pop()
return parts
}
// https://infra.spec.whatwg.org/#ascii-upper-alpha
function isASCIIUpperAlpha(c) {
return c >= 0x41 && c <= 0x5a
}
// https://infra.spec.whatwg.org/#ascii-lower-alpha
function isASCIILowerAlpha(c) {
return c >= 0x61 && c <= 0x7a
}
// https://infra.spec.whatwg.org/#ascii-alpha
function isASCIIAlpha(c) {
return isASCIIUpperAlpha(c) || isASCIILowerAlpha(c)
}
// https://url.spec.whatwg.org/#windows-drive-letter
function isWindowsDriveLetter(input) {
return (
input.length >= 2 &&
isASCIIAlpha(input.charCodeAt(0)) &&
(input.charCodeAt(1) === 0x3a || input.charCodeAt(1) === 0x7c)
)
}
// https://url.spec.whatwg.org/#start-with-a-windows-drive-letter
function startsWithWindowsDriveLetter(input) {
return (
input.length >= 2 &&
isWindowsDriveLetter(input) &&
(input.length === 2 ||
input.charCodeAt(2) === 0x2f ||
input.charCodeAt(2) === 0x5c ||
input.charCodeAt(2) === 0x3f ||
input.charCodeAt(2) === 0x23)
)
}