als-require
Version:
A utility for using CommonJS require in the browser and creating bundles.
212 lines (196 loc) • 8.93 kB
JavaScript
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);