UNPKG

@socketsupply/socket

Version:

A Cross-Platform, Native Runtime for Desktop and Mobile Apps — Create apps using HTML, CSS, and JavaScript. Written from the ground up to be small and maintainable.

1,312 lines (1,133 loc) 32.8 kB
/** * @module commonjs.package */ import { ModuleNotFoundError } from '../errors.js' import { defineBuiltin } from './builtins.js' import { isESMSource } from '../util.js' import { Loader } from './loader.js' import location from '../location.js' import path from '../path.js' import URL from '../url.js' /** * `true` if in a worker scope. * @type {boolean} * @ignore */ const isWorkerScope = globalThis.self === globalThis && !globalThis.window /** * @ignore * @param {string} source * @return {boolean} */ export function detectESMSource (source) { return isESMSource(source) } /** * @typedef {{ * manifest?: string, * index?: string, * description?: string, * version?: string, * license?: string, * exports?: object, * type?: 'commonjs' | 'module', * info?: object, * origin?: string, * dependencies?: Dependencies | object | Map * }} PackageOptions */ /** * @typedef {import('./loader.js').RequestOptions & { * type?: 'commonjs' | 'module' * prefix?: string * }} PackageLoadOptions */ /** * {import('./loader.js').RequestOptions & { * load?: boolean, * type?: 'commonjs' | 'module', * browser?: boolean, * children?: string[] * extensions?: string[] | Set<string> * }} PackageResolveOptions */ /** * @typedef {{ * organization: string | null, * name: string, * version: string | null, * pathname: string, * url: URL, * isRelative: boolean, * hasManifest: boolean * }} ParsedPackageName */ /** * @typedef {{ * require?: string | string[], * import?: string | string[], * default?: string | string[], * default?: string | string[], * worker?: string | string[], * browser?: string | string[] * }} PackageExports /** * The default package index file such as 'index.js' * @type {string} */ export const DEFAULT_PACKAGE_INDEX = 'index.js' /** * The default package manifest file name such as 'package.json' * @type {string} */ export const DEFAULT_PACKAGE_MANIFEST_FILE_NAME = 'package.json' /** * The default package path prefix such as 'node_modules/' * @type {string} */ export const DEFAULT_PACKAGE_PREFIX = 'node_modules/' /** * The default package version, when one is not provided * @type {string} */ export const DEFAULT_PACKAGE_VERSION = '0.0.1' /** * The default license for a package' * @type {string} */ export const DEFAULT_LICENSE = 'Unlicensed' /** * A container for a package name that includes a package organization identifier, * its fully qualified name, or for relative package names, its pathname */ export class Name { /** * Parses a package name input resolving the actual module name, including an * organization name given. If a path includes a manifest file * ('package.json'), then the directory containing that file is considered a * valid package and it will be included in the returned value. If a relative * path is given, then the path is returned if it is a valid pathname. This * function returns `null` for bad input. * @param {string|URL} input * @param {{ origin?: string | URL, manifest?: string }=} [options] * @return {ParsedPackageName?} */ static parse (input, options = null) { if (typeof input === 'string') { input = input.trim() } if (!input) { return null } const origin = options?.origin ?? location.origin const manifest = options?.manifest ?? DEFAULT_PACKAGE_MANIFEST_FILE_NAME // relative or absolute path given, which is still relative to the origin const isRelative = ( typeof input === 'string' && (input.startsWith('.') || input.startsWith('/')) ) let hasManifest = false let url = null // URL already given, ignore the origin // @ts-ignore if (URL.canParse(input) || input instanceof URL) { url = new URL(input) } else { url = new URL(input, origin) } // invalid input if a URL was unable to be determined if (!url) { return null } let pathname = url.pathname.replace(new URL(origin).pathname, '') if (isRelative && pathname.startsWith('/') && input.startsWith('.')) { pathname = `./${pathname.slice(1)}` } else if (pathname.startsWith('/')) { pathname = pathname.slice(1) } // manifest was given in name, just use the directory name if (pathname.endsWith(`/${manifest}`)) { hasManifest = true pathname = pathname.split('/').slice(0, -1).join('/') } // name included organization if (pathname.startsWith('@')) { const components = pathname.split('/') const organization = components[0] let [name, version = null] = components[1].split('@') pathname = [organization || '', name, ...components.slice(2)].filter(Boolean).join('/') // manifest was given, this could be a nested package if (hasManifest) { name = [organization, name].filter(Boolean).concat(components.slice(2)).join('/') const r = { name, version, organization, pathname, url, isRelative, hasManifest } return r } // only a organization was given, return `null` if (components.length === 1) { return null } name = `${organization}/${name}` // only `@<org>/<package>` was given if (components.length === 2) { return { name, version, organization, pathname, url, isRelative, hasManifest } } // `@<org>/<package>/...` was given return { name, version, organization, pathname, url, isRelative, hasManifest } } // a valid relative path was given, just return it normalized if (isRelative) { if (input.startsWith('/')) { pathname = `/${pathname}` } else { pathname = `./${pathname}` } return { organization: null, version: null, name: pathname, pathname, url, isRelative, hasManifest } } // at this point, a named module was given const components = pathname.split('/') const [name, version = null] = components[0].split('@') pathname = [name, ...components.slice(1)].filter(Boolean).join('/') // manifest was given, this could be a nested package if (hasManifest) { return { organization: null, name: pathname, pathname, url, isRelative, hasManifest } } return { organization: null, name: name || pathname, version, pathname: name && pathname ? pathname : null, url, isRelative, hasManifest } } /** * Returns `true` if the given `input` can be parsed by `Name.parse` or given * as input to the `Name` class constructor. * @param {string|URL} input * @param {{ origin?: string | URL, manifest?: string }=} [options] * @return {boolean} */ static canParse (input, options = null) { const origin = options?.origin ?? location.origin if (typeof input === 'string') { input = input.trim() } if (!input) { return null } // URL already given, ignore the origin // @ts-ignore return input instanceof URL || URL.canParse(input, origin) } /** * Creates a new `Name` from input. * @param {string|URL} input * @param {{ origin?: string | URL, manifest?: string }=} [options] * @return {Name} */ static from (input, options = null) { if (!Name.canParse(input, options)) { throw new TypeError( `Cannot create new 'Name'. Invalid 'input' given. Received: ${input}` ) } return new Name(Name.parse(input, options)) } #name = null #origin = null #version = null #pathname = null #organization = null #isRelative = false /** * `Name` class constructor. * @param {string|URL|NameOptions|Name} name * @param {{ origin?: string | URL, manifest?: string }=} [options] * @throws TypeError */ constructor (name, options = null) { /** @type {ParsedPackageName?} */ let parsed = null if (typeof name === 'string' || name instanceof URL) { parsed = Name.parse(name, options) } else if (name && typeof name === 'object') { parsed = { organization: name.organization || null, name: name.name || null, pathname: name.pathname || null, version: name.version || null, // @ts-ignore url: name.url instanceof URL || URL.canParse(name.url) ? new URL(name.url) : null, isRelative: name.isRelative || false, hasManifest: name.hasManifest || false } } if (parsed === null) { throw new TypeError(`Invalid 'name' given. Received: ${name}`) } this.#name = parsed.name this.#origin = parsed.url?.origin ?? location.origin this.#version = parsed.version ?? null this.#pathname = parsed.pathname this.#organization = parsed.organization this.#isRelative = parsed.isRelative } /** * The id of this package name. * @type {string} */ get id () { return this.#pathname } /** * The actual package name. * @type {string} */ get name () { return this.#name } /** * An alias for 'name'. * @type {string} */ get value () { return this.#name } /** * The origin of the package, if available. * This value may be `null`. * @type {string?} */ get origin () { return this.#origin } /** * The package version if available. * This value may be `null`. * @type {string?} */ get version () { return this.#version } /** * The actual package pathname, if given in name string. * This value is always a string defaulting to '.' if no path * was given in name string. * @type {string} */ get pathname () { return this.#pathname || '.' } /** * The organization name. * This value may be `null`. * @type {string?} */ get organization () { return this.#organization } /** * `true` if the package name was relative, otherwise `false`. * @type {boolean} */ get isRelative () { return this.#isRelative } /** * Converts this package name to a string. * @ignore * @return {string} */ toString () { const { organization, name } = this let pathname = this.#pathname !== name ? this.#pathname : '' if (pathname && pathname.startsWith('/')) { pathname = pathname.slice(1) } if (organization) { if (pathname) { return `@${organization}/${name}/${pathname}` } return `@${organization}/${name}` } else if (this.isRelative) { return `./${pathname}` } else if (pathname) { return `${name}/${pathname}` } return name } /** * Converts this `Name` instance to JSON. * @ignore * @return {object} */ toJSON () { return { name: this.name, origin: this.origin, version: this.version, pathname: this.pathname, organization: this.organization } } } /** * A container for package dependencies that map a package name to a `Package` instance. */ export class Dependencies { #map = new Map() #origin = null #package = null constructor (parent, options = null) { this.#package = parent this.#origin = options?.origin ?? parent?.origin } get map () { return this.#map } get origin () { return this.#origin ?? null } add (name, info = null) { if (info instanceof Package) { this.#map.set(name, info) } else { this.#map.set(name, new Package(name, { loader: this.#package.loader, origin: this.#origin, info })) } this.#map.get(name) } get (name, options = null) { const dependency = this.#map.get(name) if (dependency) { // try to load dependency.load(options) return dependency } } entries () { return this.#map.entries() } keys () { return this.#map.keys() } values () { return this.#map.values() } load (options = null) { for (const dependency of this.values()) { dependency.load(options) } } [Symbol.iterator] () { return this.#map.entries() } } /** * A container for CommonJS module metadata, often in a `package.json` file. */ export class Package { /** * A high level class for a package name. * @type {typeof Name} */ static Name = Name /** * A high level container for package dependencies. * @type {typeof Dependencies} */ static Dependencies = Dependencies /** * Creates and loads a package * @param {string|URL|NameOptions|Name} name * @param {PackageOptions & PackageLoadOptions=} [options] * @return {Package} */ static load (name, options = null) { const manifest = options?.manifest ?? DEFAULT_PACKAGE_MANIFEST_FILE_NAME const pkg = new this(name, options) if (name.endsWith(manifest) || !path.extname(name)) { pkg.load(options) } return pkg } #id = null #name = null #type = 'commonjs' #license = null #version = null #description = null #dependencies = null #info = null #loader = null #imports = {} /** * @type {Record<string, PackageExports>} */ #exports = {} /** * `Package` class constructor. * @param {string|URL|NameOptions|Name} name * @param {PackageOptions=} [options] */ constructor (name, options = null) { options = /** @type {PackageOptions} */ ({ ...options }) if (typeof name !== 'string' && !(name instanceof Name) && !(name instanceof URL)) { throw new TypeError(`Expecting 'name' to be a string or URL. Received: ${name}`) } // the module loader this.#loader = new Loader(options.loader) this.#id = options.id ?? null this.#name = Name.from(name, { origin: options.origin ?? options.loader?.origin ?? this.#loader.origin ?? null, manifest: options.manifest ?? DEFAULT_PACKAGE_MANIFEST_FILE_NAME }) // early meta data this.#info = options.info ?? null this.#type = options.type && /(commonjs|module)/.test(options.type) ? options.type : this.#type this.#exports = options.exports ?? this.#exports this.#imports = options.imports ?? this.#imports this.#license = options.license ?? DEFAULT_LICENSE this.#version = options.version ?? this.#name.version ?? DEFAULT_PACKAGE_VERSION this.#description = options.description ?? '' this.#dependencies = new Dependencies(this) if (options.dependencies && typeof options.dependencies === 'object') { if (options.dependencies instanceof Dependencies || typeof options.dependencies.entries === 'function') { for (const [key, value] of options.dependencies.entries()) { this.#dependencies.add(key, value) } } else { for (const key in options.dependencies) { const value = options.dependencies[key] this.#dependencies.add(key, value) } } } if (!this.#exports || typeof this.#exports !== 'object') { this.#exports = { '.': null } } if (!this.#exports['.']) { this.#exports = { '.': { require: options.index ?? DEFAULT_PACKAGE_INDEX, import: options.index ?? DEFAULT_PACKAGE_INDEX, default: options.index ?? DEFAULT_PACKAGE_INDEX } } } } /** * The unique ID of this `Package`, which is the absolute * URL of the directory that contains its manifest file. * @type {string} */ get id () { return this.#id } /** * The absolute URL to the package manifest file * @type {string} */ get url () { return new URL(this.#id).href } /** * A reference to the package subpath imports and browser mappings. * These values are typically used with its corresponding `Module` * instance require resolvers. * @type {object} */ get imports () { return this.#imports } /** * A loader for this package, if available. This value may be `null`. * @type {Loader} */ get loader () { return this.#loader } /** * `true` if the package was actually "loaded", otherwise `false`. * @type {boolean} */ get loaded () { return this.#info !== null } /** * The name of the package. * @type {string} */ get name () { return this.#name?.id ?? '' } /** * The description of the package. * @type {string} */ get description () { return this.#description ?? '' } /** * The organization of the package. This value may be `null`. * @type {string?} */ get organization () { return this.#name.organization ?? null } /** * The license of the package. * @type {string} */ get license () { return this.#license ?? DEFAULT_LICENSE } /** * The version of the package. * @type {string} */ get version () { return this.#version ?? DEFAULT_PACKAGE_VERSION } /** * The origin for this package. * @type {string} */ get origin () { return this.loader.origin } /** * The exports mappings for the package * @type {object} */ get exports () { return this.#exports } /** * The package type. * @type {'commonjs'|'module'} */ get type () { return this.#type } /** * The raw package metadata object. * @type {object?} */ get info () { return this.#info ?? null } /** * @type {Dependencies} */ get dependencies () { return this.#dependencies } /** * An alias for `entry` * @type {string?} */ get main () { return this.entry } /** * The entry to the package * @type {string?} */ get entry () { let entry = null if (Array.isArray(this.#exports['.'])) { for (const exports of this.#exports['.']) { if (typeof exports === 'string') { entry = exports } else if (exports && typeof exports === 'object') { if (isWorkerScope && exports.worker) { entry = exports.worker } else if (this.type === 'commonjs') { entry = exports.require || exports.default } else if (this.type === 'module') { entry = exports.import || exports.default } } if (entry) { break } } } if (this.type === 'commonjs') { entry = this.#exports['.'].require } else if (this.type === 'module') { entry = this.#exports['.'].import } if (!entry && this.#exports['.'].default) { entry = this.#exports['.'].default } if (isWorkerScope) { if (!entry && this.#exports['.'].worker) { entry = this.#exports['.'].worker } } if (!entry && this.#exports['.'].browser) { entry = this.#exports['.'].browser } if (entry) { if (!entry.startsWith('./')) { entry = `./${entry}` } else if (!entry.startsWith('.')) { entry = `.${entry}` } // @ts-ignore if (URL.canParse(entry, this.id)) { return new URL(entry, this.id).href } } return null } /** * Load the package information at an optional `origin` with * optional request `options`. * @param {PackageLoadOptions=} [options] * @throws SyntaxError * @return {boolean} */ load (origin = null, options = null) { if (origin && typeof origin === 'object' && !(origin instanceof URL)) { options = origin origin = options.origin } if (!this.origin || origin === this.origin) { if (options?.force !== true && this.#info) { return true } } if (!origin) { origin = this.origin } const prefix = options?.prefix ?? '' const manifest = options?.manifest ?? DEFAULT_PACKAGE_MANIFEST_FILE_NAME const pathname = `${prefix}${this.name}/${manifest}` const response = this.loader.load(pathname, origin, options) if (!response.text) { const entry = path.basename(this.entry ?? './index.js') const pathname = `${prefix}${this.name}/${entry}` const response = this.loader.load(pathname, origin, options) if (response.text) { this.#id = response.id this.#info = { name: this.name.value, main: entry, module: entry } if (detectESMSource(response.text)) { this.#info.type = 'module' } else { this.#info.type = 'commonjs' } this.#type = this.#info.type return true } return false } const info = JSON.parse(response.text) if (!info || typeof info !== 'object') { return false } const type = options?.type ?? info.type ?? this.#type this.#info = info this.#type = type this.#id = new URL('./', response.id).href this.#name = info.name ? Name.from(info.name, { origin }) : Name.from(this.#name.value, { origin }) this.#license = info.license ?? 'Unlicensed' this.#version = info.version this.#description = info.description this.#loader.origin = origin if (info.dependencies && typeof info.dependencies === 'object') { for (const name in info.dependencies) { const version = info.dependencies[name] if (typeof version === 'string') { this.#dependencies.add(name, { version }) } } } if (info.main && !info.module && !info.type) { this.#type = 'commonjs' } if (info.main) { if (info.type === 'module') { this.#exports['.'].import = info.main } else { this.#exports['.'].require = info.main } } if (info.module && !info.main) { this.#exports['.'].import = info.module this.#type = 'module' } if (!this.#exports['.']) { this.#exports['.'] = {} } if (typeof info.exports === 'string') { if (this.#type === 'commonjs') { this.#exports['.'].require = info.exports } else if (this.#type === 'module') { this.#exports['.'].import = info.exports } } if (info.exports && typeof info.exports === 'object') { if (info.exports.import || info.exports.require || info.exports.default) { this.#exports['.'] = info.exports } else { for (const key in info.exports) { const exports = info.exports[key] if (!exports) { continue } if (typeof exports === 'string') { this.#exports[key] = {} if (this.#type === 'commonjs') { this.#exports[key].require = exports } else if (this.#type === 'module') { this.#exports[key].import = exports } } else if (typeof exports === 'object') { this.#exports[key] = exports } } } } for (const key in this.#exports) { const exports = this.#exports[key] if (Array.isArray(exports)) { for (let i = 0; i < exports.length; ++i) { const value = exports[i] if (typeof value === 'string') { if (value.startsWith('/')) { exports[i] = `.${value}` } else if (!value.startsWith('.')) { exports[i] = `./${value}` } } } } else { for (const condition in exports) { const value = exports[condition] if (Array.isArray(value)) { for (let i = 0; i < value.length; ++i) { if (typeof value[i] === 'string') { if (value[i].startsWith('/')) { value[i] = `.${value[i]}` } else if (!value[i].startsWith('.')) { value[i] = `./${value[i]}` } } } } else if (typeof value === 'string') { if (value.startsWith('/')) { exports[condition] = `.${value}` } else if (!value.startsWith('.')) { exports[condition] = `./${value}` } } } } } if ( this.#info.imports && !Array.isArray(this.#info.imports) && typeof this.#info.imports === 'object' ) { for (const key in this.#info.imports) { const value = this.#info.imports[key] if (typeof value === 'string') { this.#imports[key] = { default: value } } else if (value && typeof value === 'object') { this.#imports[key] = {} if (value.default) { this.#imports[key].default = value.default } if (value.browser) { this.#imports[key].browser = value.browser } } } } if ( this.#info.browser && !Array.isArray(this.#info.browser) && typeof this.#info.browser === 'object' ) { for (const key in this.#info.browser) { const value = this.#info.browser[key] if (typeof value === 'string') { if (key.startsWith('.')) { if (this.#exports[key]) { this.#exports[key].browser = value } } else { this.#imports[key] ??= { } this.#imports[key].browser = value } } else if (value && typeof value === 'object') { this.#imports[key] ??= {} if (value.default) { this.#imports[key].default = value.default } if (value.browser) { this.#imports[key].browser = value.browser } } } } if (this.#type === 'module') { if (this.#info.type !== 'module' && this.entry) { const source = this.loader.load(this.entry, origin, options).text if (!detectESMSource(source)) { this.#type = 'commonjs' } } } return true } /** * Resolve a file's `pathname` within the package. * @param {string|URL} pathname * @param {PackageResolveOptions=} [options] * @return {string} */ resolve (pathname, options = null) { if (options?.load !== false) { this.load(options) } const { info } = this const manifest = options?.manifest ?? DEFAULT_PACKAGE_MANIFEST_FILE_NAME const type = options?.type ?? this.type if (info?.addon === true) { throw new ModuleNotFoundError( `Cannot find module '${pathname}' (requested module is a Node.js addon)`, options?.children?.map?.((mod) => mod.id) ) } let origin = this.id // an absolute URL was given, just try to resolve it // @ts-ignore if (pathname instanceof URL || URL.canParse(pathname)) { const url = new URL(pathname) const response = this.loader.status(url.href, options) if (response.ok) { return interpolateBrowserResolution(response.id) } pathname = url.pathname origin = url.origin } if (pathname === '.') { pathname = './' } else if (pathname.startsWith('/')) { pathname = `.${pathname}` } else if (!pathname.startsWith('.')) { pathname = `./${pathname}` } if (options?.origin) { origin = options.origin } else if (!origin) { origin = new URL(this.name, this.origin).href if (!origin.endsWith('/')) { origin += '/' } } // if the pathname ends with the manifest file ('package.json'), then // construct a new `Package` with the this packages loader and resolve it if (pathname.endsWith(`/${manifest}`)) { const url = new URL(`${this.name}/${pathname}`, origin) const childPackage = new Package(url, { loader: this.loader, origin }) // if it loaded, then just return the URL if (childPackage.load()) { return url.href } } const extname = path.extname(pathname) const extensions = extname !== '' && this.loader.extensions.has(extname) ? new Set([extname]) : new Set(Array .from(options?.extensions ?? []) .concat('') .concat(Array.from(this.loader.extensions)) .filter((e) => typeof e === 'string') ) if (pathname.endsWith('/')) { pathname += 'index.js' } for (const extension of extensions) { for (const key in this.#exports) { const query = pathname !== '.' && pathname !== './' ? pathname + extension : pathname let exports = this.#exports[key] let filename = null exports = exports?.browser ?? exports?.node ?? exports?.default ?? exports if (!exports || typeof exports !== 'object') { exports = this.#exports[key] } if (!exports) { continue } if ( key === query || key === pathname.replace(extname, '') || (pathname === './' && key === '.') || (pathname === './index' && key === '.') || (pathname === './index.js' && key === '.') ) { if (Array.isArray(exports)) { for (const entry of exports) { if (typeof entry === 'string') { const response = this.loader.load(entry, origin, options) if (response.ok) { return interpolateBrowserResolution(response.id) } } else if (entry && typeof entry === 'object') { exports = entry break } } } if (exports && !Array.isArray(exports) && typeof exports === 'object') { if (isWorkerScope && exports.worker) { filename = exports.worker } if (type === 'commonjs' && exports.require) { filename = exports.require } else if (type === 'module' && exports.import) { filename = exports.import } else if (exports.browser) { filename = exports.browser } else if (exports.default) { filename = exports.default } else { filename = ( exports.require || exports.import || exports.browser || exports.default ) } } if (filename) { const response = this.loader.load(filename, origin, options) if (response.ok) { return interpolateBrowserResolution(response.id) } } } } if (extension && (!extname || !this.loader.extensions.has(extname))) { let response = this.loader.load(pathname + extension, origin, options) if (response.ok) { return interpolateBrowserResolution(response.id) } response = this.loader.load(`${pathname}/index${extension}`, origin, options) if (response.ok) { return interpolateBrowserResolution(response.id) } } } const response = this.loader.load(pathname, origin, options) if (response.ok) { return interpolateBrowserResolution(response.id) } // try to load 'package.json' const url = new URL(`${this.name}/${pathname}/${manifest}`, origin) const childPackage = new Package(url, { loader: this.loader, origin }) // if it loaded, then return the package entry if (childPackage.load()) { return childPackage.entry } throw new ModuleNotFoundError( `Cannot find module '${pathname}'`, options?.children?.map?.((mod) => mod.id) ) function interpolateBrowserResolution (id) { if (!info || options?.browser === false) { return id } const url = new URL(id) const prefix = new URL('.', origin || location.origin).href const pathname = `./${url.href.replace(prefix, '')}` if (info.browser && typeof info.browser === 'object') { for (const key in info.browser) { const value = info.browser[key] const filename = !key.startsWith('./') ? `./${key}` : key if (filename === pathname) { return new URL(value, prefix).href } } } return id } } /** * @ignore */ [Symbol.for('socket.runtime.util.inspect.custom')] () { if (this.name && this.version) { return `Package '(${this.name}@${this.version}') { }` } else if (this.name) { return `Package ('${this.name}') { }` } else { return 'Package { }' } } } export default Package defineBuiltin('commonjs/package', { DEFAULT_PACKAGE_MANIFEST_FILE_NAME, DEFAULT_PACKAGE_VERSION, DEFAULT_PACKAGE_PREFIX, DEFAULT_PACKAGE_INDEX, DEFAULT_LICENSE, detectESMSource, Dependencies, Package, Name })