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.

924 lines (801 loc) 21 kB
/* global ErrorEvent */ /* eslint-disable no-void, no-sequences */ import { globalPaths, createRequire as createRequireImplementation } from './require.js' import { DEFAULT_PACKAGE_PREFIX, Package } from './package.js' import builtins, { defineBuiltin } from './builtins.js' import application from '../application.js' import { Loader } from './loader.js' import location from '../location.js' import process from '../process.js' import path from '../path.js' import fs from '../fs.js' /** * @typedef {function(string, Module, function(string): any): any} ModuleResolver */ /** * @typedef {import('./require.js').RequireFunction} RequireFunction */ /** * @typedef {import('./package.js').PackageOptions} PackageOptions */ /** * @typedef {{ * prefix?: string, * request?: import('./loader.js').RequestOptions, * builtins?: object * } CreateRequireOptions */ /** * @typedef {{ * resolvers?: ModuleResolver[], * importmap?: ImportMap, * loader?: Loader | object, * loaders?: object, * package?: Package | PackageOptions * parent?: Module, * state?: State * }} ModuleOptions */ /** * @typedef {{ * extensions?: object * }} ModuleLoadOptions */ export const builtinModules = builtins /** * CommonJS module scope with module scoped globals. * @ignore * @param {object} exports * @param {function(string): any} require * @param {Module} module * @param {string} __filename * @param {string} __dirname * @param {typeof process} process * @param {object} global */ export function CommonJSModuleScope ( exports, require, module, __filename, __dirname, process, global ) { // eslint-disable-next-line no-unused-vars const crypto = require('socket:crypto') // eslint-disable-next-line no-unused-vars const { Buffer } = require('socket:buffer') // eslint-disable-next-line no-unused-expressions void exports, require, module, __filename, __dirname // eslint-disable-next-line no-unused-expressions void process, console, global, crypto, Buffer return (function () { 'module code' })() } /** * CommonJS module scope source wrapper. * @type {string} */ export const COMMONJS_WRAPPER = CommonJSModuleScope .toString() .split(/'module code'/) /** * A container for imports. * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap} */ export class ImportMap { #imports = {} /** * The imports object for the importmap. * @type {object} */ get imports () { return this.#imports } set imports (imports) { if (imports && typeof imports === 'object' && !Array.isArray(imports)) { this.#imports = {} for (const key in imports) { this.#imports[key] = imports[key] } } } /** * Extends the current imports object. * @param {object} imports * @return {ImportMap} */ extend (importmap) { this.imports = { ...this.#imports, ...importmap?.imports ?? null } return this } } /** * A container for `Module` instance state. */ export class State { loading = false loaded = false error = null /** * `State` class constructor. * @ignore * @param {object|State=} [state] */ constructor (state = null) { if (state && typeof state === 'object') { for (const key in state) { if (key in this && typeof state[key] === typeof this[key]) { this[key] = state[key] } } } } } /** * The module scope for a loaded module. * This is a special object that is seal, frozen, and only exposes an * accessor the 'exports' field. * @ignore */ export class ModuleScope { #module = null #exports = Object.create(null) /** * `ModuleScope` class constructor. * @param {Module} module */ constructor (module) { this.#module = module Object.freeze(this) } get id () { return this.#module.id } get filename () { return this.#module.filename } get loaded () { return this.#module.loaded } get children () { return this.#module.children } get exports () { return this.#exports } set exports (exports) { this.#exports = exports } toJSON () { return { id: this.id, filename: this.filename, children: this.children, exports: this.exports } } } /** * An abstract base class for loading a module. */ export class ModuleLoader { /** * Creates a `ModuleLoader` instance from the `module` currently being loaded. * @param {Module} module * @param {ModuleLoadOptions=} [options] * @return {ModuleLoader} */ static from (module, options = null) { const loader = new this(module, options) return loader } /** * Creates a new `ModuleLoader` instance from the `module` currently * being loaded with the `source` string to parse and load with optional * `ModuleLoadOptions` options. * @param {Module} module * @param {ModuleLoadOptions=} [options] * @return {boolean} */ static load (module, options = null) { return this.from(module, options).load(module, options) } /** * @param {Module} module * @param {ModuleLoadOptions=} [options] * @return {boolean} */ load (module, options = null) { // eslint-disable-next-line void module // eslint-disable-next-line void options return false } } /** * A JavaScript module loader */ export class JavaScriptModuleLoader extends ModuleLoader { /** * Loads the JavaScript module. * @param {Module} module * @param {ModuleLoadOptions=} [options] * @return {boolean} */ load (module, options = null) { const response = module.loader.load(module.id, options) const compiled = Module.compile(response.text, { url: response.id }) const __filename = module.id const __dirname = path.dirname(__filename) // eslint-disable-next-line no-useless-call const result = compiled.call(null, module.scope.exports, module.createRequire(options), module.scope, __filename, __dirname, process, globalThis ) if (typeof result?.catch === 'function') { result.catch((error) => { error.module = module module.dispatchEvent(new ErrorEvent('error', { error })) }) } return true } } /** * A JSON module loader. */ export class JSONModuleLoader extends ModuleLoader { /** * Loads the JSON module. * @param {Module} module * @param {ModuleLoadOptions=} [options] * @return {boolean} */ load (module, options = null) { const response = module.loader.load(module.id, options) if (response.text) { module.scope.exports = JSON.parse(response.text) } return true } } /** * A WASM module loader */ export class WASMModuleLoader extends ModuleLoader { /** * Loads the WASM module. * @param {string} * @param {Module} module * @param {ModuleLoadOptions=} [options] * @return {boolean} */ load (module, options = null) { const response = module.loader.load(module.id, { ...options, responseType: 'arraybuffer' }) const instance = new WebAssembly.Instance( new WebAssembly.Module(response.buffer), options || undefined ) module.scope.exports = instance.exports return true } } /** * A container for a loaded CommonJS module. All errors bubble * to the "main" module and global object (if possible). */ export class Module extends EventTarget { /** * A reference to the currently scoped module. * @type {Module?} */ static current = null /** * A reference to the previously scoped module. * @type {Module?} */ static previous = null /** * A cache of loaded modules * @type {Map<string, Module>} */ static cache = new Map() /** * An array of globally available module loader resolvers. * @type {ModuleResolver[]} */ static resolvers = [] /** * Globally available 'importmap' for all loaded modules. * @type {ImportMap} * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap} */ static importmap = new ImportMap() /** * A limited set of builtins exposed to CommonJS modules. * @type {object} */ static builtins = builtins /** * A limited set of builtins exposed to CommonJS modules. * @type {object} */ static builtinModules = builtinModules /** * CommonJS module scope source wrapper components. * @type {string[]} */ static wrapper = COMMONJS_WRAPPER /** * An array of global require paths, relative to the origin. * @type {string[]} */ static globalPaths = globalPaths /** * Globabl module loaders * @type {object} */ static loaders = Object.assign(Object.create(null), { '.js' (module, options = null) { return JavaScriptModuleLoader.load(module, options) }, '.cjs' (module, options = null) { return JavaScriptModuleLoader.load(module, options) }, '.json' (module, options = null) { return JSONModuleLoader.load(module, options) }, '.wasm' (source, module, options = null) { return WASMModuleLoader.load(module, options) } }) /** * The main entry module, lazily created. * @type {Module} */ static get main () { if (this.cache.has(location.origin)) { return this.cache.get(location.origin) } let packageInfo = '' try { packageInfo = fs.readFileSync('package.json', 'utf8') } catch (err) { } if (packageInfo.length) { try { packageInfo = JSON.parse(packageInfo) } catch (err) { console.warn(err) packageInfo = null } } const main = new Module(location.origin, { state: new State({ loaded: true }), parent: null, package: new Package(location.origin, { id: location.href, type: 'commonjs', info: packageInfo || application.config, index: '', prefix: '', manifest: '', // eslint-disable-next-line name: packageInfo?.name || application.config.build_name, // eslint-disable-next-line version: packageInfo?.version || application.config.meta_version, // eslint-disable-next-line description: packageInfo?.description || application.config.meta_description, imports: packageInfo?.imports || {}, exports: { '.': { default: location.pathname } } }) }) this.cache.set(location.origin, main) if (globalThis.window && globalThis === globalThis.window) { try { const importmapScriptElement = globalThis.document.querySelector('script[type=importmap]') if (importmapScriptElement && importmapScriptElement.textContent) { const importmap = JSON.parse(importmapScriptElement.textContent) Module.importmap.extend(importmap) } } catch (err) { globalThis.reportError(err) } } return main } /** * Wraps source in a CommonJS module scope. * @param {string} source */ static wrap (source) { const [head, tail] = this.wrapper const body = String(source || '') return [head, body, tail].join('\n') } /** * Compiles given JavaScript module source. * @param {string} source * @param {{ url?: URL | string }=} [options] * @return {function( * object, * function(string): any, * Module, * string, * string, * typeof process, * object * ): any} */ static compile (source, options = null) { const wrapped = Module.wrap(source) .replace('function CommonJSModuleScope', `"Module (${options?.url ?? '<anonymous>'})"`) // eslint-disable-next-line const compiled = new Function(` const __commonjs_module_scope_container__ = {${wrapped}}; return __commonjs_module_scope_container__[Object.keys(__commonjs_module_scope_container__)[0]]; // # sourceURL=${options?.url ?? ''} `)() return compiled } /** * Creates a `Module` from source URL and optionally a parent module. * @param {string|URL|Module} url * @param {ModuleOptions=} [options] */ static from (url, options = null) { if (typeof url === 'object' && url instanceof Module) { return this.from(url.id, options) } if (options instanceof Module) { options = { parent: options } } else { options = { ...options } } if (!options.parent) { options.parent = Module.current } url = Loader.resolve(url, options.parent?.id) if (this.cache.has(url)) { return this.cache.get(url) } const module = new Module(url, options) return module } /** * Creates a `require` function from a given module URL. * @param {string|URL} url * @param {ModuleOptions=} [options] */ static createRequire (url, options = null) { const module = Module.from(url, { package: { info: Module.main.package.info }, ...options }) return module.createRequire(options) } #id = null #scope = null #state = new State() #cache = Object.create(null) #loader = null #parent = null #package = null #children = [] #resolvers = [] #importmap = new ImportMap() #loaders = Object.create(null) /** * `Module` class constructor. * @param {string|URL} url * @param {ModuleOptions=} [options] */ constructor (url, options = null) { super() options = { ...options } if (options.parent && options.parent instanceof Module) { this.#parent = options.parent } else if (options.parent === undefined) { this.#parent = Module.main } if (this.#parent !== null) { this.#id = Loader.resolve(url, this.#parent.id) this.#cache = this.#parent.cache } else { this.#id = Loader.resolve(url) } if (options.state && options.state instanceof State) { this.#state = options.state } this.#scope = new ModuleScope(this) this.#loader = new Loader(this.#id, options?.loader) this.#package = options.package instanceof Package ? options.package : this.#parent?.package ?? new Package(options.name ?? this.#id, options.package) this.addEventListener('error', (event) => { if (event.error) { this.#state.error = event.error } if (Module.main === this) { // bubble error to globalThis, if possible if (typeof globalThis.dispatchEvent === 'function') { // @ts-ignore globalThis.dispatchEvent(new ErrorEvent('error', event)) } } else { // bubble errors to main module Module.main.dispatchEvent(new ErrorEvent('error', event)) } }) this.#importmap.extend(Module.importmap) Object.assign(this.#loaders, Module.loaders) if (options.importmap) { this.#importmap.extend(options.importmap) } if (Array.isArray(options.resolvers)) { for (const resolver of options.resolvers) { if (typeof resolver === 'function') { this.#resolvers.push(resolver) } } } // includes `.browser` field mapping for (const key in this.package.imports) { const value = this.package.imports[key] if (!value) continue this.#resolvers.push((specifier, ctx, next) => { if (specifier === key) { return (typeof value === 'string' ? value : value.default ?? value.browser ?? next(specifier) ) } return next(specifier) }) } if (this.#parent) { Object.assign(this.#loaders, this.#parent.loaders) if (Array.isArray(options?.resolvers)) { for (const resolver of options.resolvers) { if (typeof resolver === 'function') { this.#resolvers.push(resolver) } } } this.#importmap.extend(this.#parent.importmap) } if (options.loaders && typeof options.loaders === 'object') { Object.assign(this.#loaders, options.loaders) } Module.cache.set(this.id, this) } /** * A unique ID for this module. * @type {string} */ get id () { return this.#id } /** * A reference to the "main" module. * @type {Module} */ get main () { return Module.main } /** * Child modules of this module. * @type {Module[]} */ get children () { return this.#children } /** * A reference to the module cache. Possibly shared with all * children modules. * @type {object} */ get cache () { return this.#cache } /** * A reference to the module package. * @type {Package} */ get package () { return this.#package } /** * The `ImportMap` for this module. * @type {ImportMap} * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap} */ get importmap () { return this.#importmap } /** * The module level resolvers. * @type {ModuleResolver[]} */ get resolvers () { const resolvers = Array.from(Module.resolvers).concat(this.#resolvers) return Array.from(new Set(resolvers)) } /** * `true` if the module is currently loading, otherwise `false`. * @type {boolean} */ get loading () { return this.#state.loading } /** * `true` if the module is currently loaded, otherwise `false`. * @type {boolean} */ get loaded () { return this.#state.loaded } /** * An error associated with the module if it failed to load. * @type {Error?} */ get error () { return this.#state.error } /** * The exports of the module * @type {object} */ get exports () { return this.#scope.exports } /** * The scope of the module given to parsed modules. * @type {ModuleScope} */ get scope () { return this.#scope } /** * The origin of the loaded module. * @type {string} */ get origin () { return this.#loader.origin } /** * The parent module for this module. * @type {Module?} */ get parent () { return this.#parent } /** * The `Loader` for this module. * @type {Loader} */ get loader () { return this.#loader } /** * The filename of the module. * @type {string} */ get filename () { return this.#id } /** * Known source loaders for this module keyed by file extension. * @type {object} */ get loaders () { return this.#loaders } /** * Factory for creating a `require()` function based on a module context. * @param {CreateRequireOptions=} [options] * @return {RequireFunction} */ createRequire (options = null) { return createRequireImplementation({ builtins, // TODO(@jwerle): make the 'prefix' value configurable somehow prefix: DEFAULT_PACKAGE_PREFIX, ...options, module: this }) } /** * Creates a `Module` from source the URL with this module as * the parent. * @param {string|URL|Module} url * @param {ModuleOptions=} [options] */ createModule (url, options = null) { return Module.from(url, { parent: this, ...options }) } /** * Requires a module at for a given `input` which can be a relative file, * named module, or an absolute URL within the context of this odule. * @param {string|URL} input * @param {RequireOptions=} [options] * @throws ModuleNotFoundError * @throws ReferenceError * @throws SyntaxError * @throws TypeError * @return {any} */ require (url, options = null) { const require = this.createRequire(options) return require(url, options) } /** * Loads the module * @param {ModuleLoadOptions=} [options] * @return {boolean} */ load (options = null) { const extension = path.extname(this.id) if (this.#state.loaded) { return true } if (typeof this.#loaders[extension] !== 'function') { return false } try { this.#state.loading = true if (this.#loaders[extension](this, options)) { if (this.#parent) { this.#parent.children.push(this) } this.#state.loaded = true } } catch (error) { error.module = this this.#state.error = error this.dispatchEvent(new ErrorEvent('error', { error })) throw error } finally { this.#state.loading = false } return this.#state.loaded } resolve (input) { return this.package.resolve(input, { load: false, origin: this.id }) } /** * @ignore */ [Symbol.toStringTag] () { return 'Module' } } /** * Creates a `require` function from a given module URL. * @param {string|URL} url * @param {ModuleOptions=} [options] * @return {RequireFunction} */ export function createRequire (url, options = null) { return Module.createRequire(url, options) } Module.Module = Module export default Module defineBuiltin('commonjs/module', Module, false)