UNPKG

als-require

Version:

A utility for using CommonJS require in the browser and creating bundles.

212 lines (196 loc) 8.93 kB
const Require = (function(){ const packageJsonCache = {}; function orderKeys(keys,Require) { const orderedKeys = [] for(let key of keys) { let index orderedKeys.forEach((k,i) => { if(Require.contents[k].children.includes(key)) index = i }); if(index !== undefined) orderedKeys.splice(index+1,0,key) else orderedKeys.push(key) } return orderedKeys } function getFullPath(path, relative) { const pathParts = path.split('/'); const relativeParts = relative.split('/').slice(0, -1); const fullPathParts = []; for (let part of [...relativeParts, ...pathParts]) { if (part === '..') { if (fullPathParts.length > 0 && fullPathParts[fullPathParts.length - 1] !== '..') fullPathParts.pop(); else fullPathParts.push(part); } else if (part !== '.') fullPathParts.push(part); } let fullPath = fullPathParts.join('/'); return fullPath.endsWith('.js') ? fullPath : fullPath + '.js' } async function getNodeModules(nodeModules, children, content, Require, logger, cyclicDependencies) { const { standartNodeModules } = Require; if (nodeModules.length === 0) return content for (let { match, modulePath } of nodeModules) { const r = new RegExp(`require\\((["'\`])${modulePath}["'\`]\\)`) const replaceAndWarn = () => { content = content.replace(r, '{}') logger.warn(`The module "${modulePath}" can't be imported and will be replaced with {}`) } if (modulePath.startsWith('node:') || standartNodeModules.includes(modulePath)) { replaceAndWarn(); continue; } let fullPath, relativePath, filename, moduleDir = modulePath if (modulePath.includes('/')) { const arr = modulePath.split('/') moduleDir = arr.shift() relativePath = arr.join('/') } let pkgJsonPath = `/node_modules/${moduleDir}/package.json` const exists = await fetch(pkgJsonPath, { method: 'HEAD' }) if (exists.ok === false) { replaceAndWarn(); continue; } if (packageJsonCache[pkgJsonPath]) filename = packageJsonCache[pkgJsonPath] else { const { main = 'index.js' } = await Require.fetch(pkgJsonPath, 'json') packageJsonCache[pkgJsonPath] = main filename = main } if (relativePath) fullPath = `/node_modules/${moduleDir}/${relativePath}` else fullPath = `/node_modules/${moduleDir}/${filename}` fullPath = fullPath.replace(/\/\.?\//g, '/') if (!fullPath.endsWith('.js')) fullPath += '.js' if (!cyclicDependencies) Require.isCyclyc(fullPath, modulePath) children.push(fullPath); if(content) content = content.replace(match, match.replace(r, (m, quoute) => { return `require(${quoute}${fullPath}${quoute})` })) } return content } async function getContents({contents, fullPath},Require,cyclicDependencies, logger, plugins=[]) { const getContent = async (path) => { if (contents[path] !== undefined) return // allready fetched if (!Require.contents[path]) { let content = await Require.fetch(path) const children = [], nodeModules = []; content = content.replace(/^(?!\/\/|\/\*.*\*\/).*require\(["'`](.*)["'`]\)/gm, (match, modulePath) => { if (!modulePath.startsWith('.')) { nodeModules.push({ match, modulePath }) return match } const fullPath = getFullPath(modulePath, path) if(!cyclicDependencies) Require.isCyclyc(fullPath, path) children.push(fullPath); return match.replace(modulePath, fullPath) }); content = await getNodeModules(nodeModules, children, content,Require, logger, cyclicDependencies) Require.contents[path] = { content, children } } const { content, children } = Require.contents[path] const obj = {content,children,path} plugins.forEach(plugin => { plugin(obj) }); contents[path] = obj.content await Promise.all(children.map(childPath => getContent(childPath))) } await getContent(fullPath) } function parseError(error, modulesLines, curLastLine) { let [message, ...stack] = error.stack.split('\n') stack = stack.map(string => { const m = string.match(/<anonymous>:(\d*):(\d*)\)$/) if (!m) return const line = Number(m[1]) if (line + 1 === curLastLine) return const char = Number(m[2]) const errorLines = Object.entries(modulesLines).filter(([path, { from, to }]) => line >= from && line <= to) if(errorLines.length === 0) return const [path, { from, to }] = errorLines[0] const at = string.match(/at\s(.*?)\s/)[1] return ` at ${at} ${path} (${line - from - 2}:${char})` }).filter(Boolean) error.stack = message + '\n' + stack.join('\n') throw error } function getFn(obj, options={}) { const { scriptBefore='', scriptAfter='', parameters=[] } = options function buildFn(fnBody,path) { return /*js*/`modules['${path}'] = function ____(){ const module = { exports: {} } const exports = module.exports ${fnBody} return module.exports; };` } const before = [ parseError.toString(), /*js*/`function require(path) { if(typeof modules[path] === 'function' && modules[path].name === '____') modules[path] = modules[path]() return modules[path] || null };`, scriptBefore, ].join('\n') const modulesLines = {}; let curLastLine = 3+before.split('\n').length; let lastModule const fns = obj.keys.map((path, i) => { let code = buildFn(obj.contents[path],path) if (i === obj.keys.length - 1) lastModule = path modulesLines[path] = { from: curLastLine + 1 } curLastLine += code.split('\n').length modulesLines[path].to = curLastLine return code }).join('\n') const body = [ before, 'try {', fns, `let result = modules['${lastModule}']()`, scriptAfter, `return result`, `} catch (error) { parseError(error, modulesLines, curLastLine) }`, ].join('\n') parameters.push('modules={}',`curLastLine = ${curLastLine}`,`modulesLines = ${JSON.stringify(modulesLines)}`) const fn = new Function(parameters.join(','), body) return fn } class Require { static standartNodeModules = ["assert", "async_hooks", "buffer", "child_process", "cluster", "console", "constants", "crypto", "dgram", "diagnostics_channel", "dns", "domain", "events", "fs", "http", "http2", "https", "inspector", "module", "net", "os", "path", "perf_hooks", "process", "punycode", "querystring", "readline", "repl", "stream", "string_decoder", "sys", "timers", "timers/promises", "tls", "trace_events", "tty", "url", "util", "v8", "vm", "wasi", "worker_threads", "zlib", "test", "abort_controller"]; static contents = {} static plugins = [] static cyclicDependencies = false static logger = console static version static isCyclyc(fullPath, path) { if (this.contents[fullPath] && this.contents[fullPath].children.includes(path)) { throw `cyclic dependency between ${path} and ${fullPath}` } } static async fetch(path, type = 'text') { if (this.version) path += '?version=' + this.version let response = await fetch(path) if (!response.ok) console.error(`HTTP error! status: ${response.status}`); return await response[type]() } static async getModule(path, options = {},...params) { const mod = new Require(path) await mod.getContent(options) const fn = await mod.fn(options) return fn(...params) } constructor(path,options = {}) { this.contents = {} this.path = path this.fullPath = getFullPath(path, location.pathname) this.contentReady = false this.options = options } async getContent(options = {}) { let { plugins = [], cyclicDependencies = Require.cyclicDependencies, logger = Require.logger } = {...this.options,...options} plugins = [...Require.plugins, ...plugins].filter(p => typeof p === 'function') if (this.contentReady) return this await getContents(this, Require, cyclicDependencies, logger, plugins) this.keys = orderKeys(Object.keys(this.contents),Require).reverse() this.contentReady = true return this } fn(options = {}) { return getFn(this, options) } } Require.orderKeys = orderKeys;Require.getFullPath = getFullPath;Require.getNodeModules = getNodeModules;Require.getContents = getContents;Require.parseError = parseError;Require.getFn = getFn;Require.Require = Require; return Require; })(); const require = (path, options, ...params) => Require.getModule(path, options,...params);