/** * 符合 CommonJS 规范的 类似 Node 的模块化加载 * 一. 注: MiaoScript 中 require.main 不存在 * 二. 加载 require 流程 例如 在 dir 目录下 调用 require('xx'); * a) 加载流程 * 1. 如果xx模块是一个內建模块 * a. 编译并返回该模块 * b. 停止执行 * 2. 如果模块以 `./` `../` 开头 * a. 尝试使用 resolveAsFile(dir/xx) 加载文件 * b. 尝试使用 resolveAsDirectory(dir/xx) 加载目录 * 3. 尝试去 root root/node_modules => xx 加载模块 * a. 尝试使用 resolveAsFile(xx/xx) 加载文件 * b. 尝试使用 resolveAsDirectory(xx/xx) 加载目录 * 4. 抛出 not found 异常 * b) resolveAsFile 解析流程 * 1. 如果 xx 是一个文件 则作为 `javascript` 文本加载 并停止执行 * 2. 如果 xx.js 是一个文件 则作为 `javascript` 文本加载 并停止执行 * 3. 如果 xx.json 是一个文件 则使用 `JSON.parse(xx.json)` 解析为对象加载 并停止执行 * 暂不支持 4. 如果 xx.msm 是一个文件 则使用MScript解析器解析 并停止执行 * c) resolveAsDirectory 解析流程 * 1. 如果 xx/package.json 存在 则使用 `JSON.parse(xx/package.json)` 解析并取得 main 字段使用 resolveAsFile(main) 加载 * 2. 如果 xx/index.js 存在 则使用 resolveAsFile(xx/index.js) 加载 * 3. 如果 xx/index.json 存在 则使用 `xx/index.json` 解析为对象加载 并停止执行 * 暂不支持 4. 如果 xx/index.msm 是一个文件 则使用MScript解析器解析 并停止执行 */ // @ts-check ( /** * @param {string} parent */ function (parent) { 'use strict' // @ts-ignore var File = Java.type('java.io.File') // @ts-ignore var Paths = Java.type('java.nio.file.Paths') // @ts-ignore var Files = Java.type('java.nio.file.Files') // @ts-ignore var StandardCopyOption = Java.type('java.nio.file.StandardCopyOption') // @ts-ignore var TarInputStream = Java.type('org.kamranzafar.jtar.TarInputStream') // @ts-ignore var GZIPInputStream = Java.type('java.util.zip.GZIPInputStream') // @ts-ignore var BufferedInputStream = Java.type('java.io.BufferedInputStream') // @ts-ignore var URL = Java.type('java.net.URL') // @ts-ignore var ByteArrayOutputStream = Java.type("java.io.ByteArrayOutputStream") // @ts-ignore var ByteArray = Java.type("byte[]") // @ts-ignore var Thread = Java.type('java.lang.Thread') // @ts-ignore var Callable = Java.type('java.util.concurrent.Callable') // @ts-ignore var Executors = Java.type('java.util.concurrent.Executors') var separatorChar = File.separatorChar // @ts-ignore var NODE_PATH = java.lang.System.getenv("NODE_PATH") || root + separatorChar + 'node_modules' // @ts-ignore var NODE_REGISTRY = java.lang.System.getenv("NODE_REGISTRY") || 'https://registry.npm.taobao.org' // @ts-ignore var MS_NODE_REGISTRY = java.lang.System.getenv("MS_NODE_REGISTRY") || 'https://repo.yumc.pw/repository/npm' var CoreModules = [ "assert", "async_hooks", "Buffer", "child_process", "cluster", "crypto", "dgram", "dns", "domain", "events", "fs", "http", "http2", "https", "inspector", "net", "os", "path", "perf_hooks", "process", "punycode", "querystring", "readline", "repl", "stream", "string_decoder", "timer", "tls", "trace_events", "tty", "url", "util", "v8", "vm", "wasi", "worker_threads", "zlib" ] var ModulesVersionLock = {} /** * @param {...object} t */ function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i] if (s === undefined) { continue } for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p] } return t } // noinspection JSValidateJSDoc /** * 判断是否为一个文件 * @param {any} file * @returns {*} */ function _isFile(file) { return file && file.isFile && file.isFile() } /** * 获得文件规范路径 * @param {any} file * @returns {*} */ function _canonical(file) { return file.canonicalPath } /** * 获得文件绝对路径 * @param {any} file * @returns {*} */ function _absolute(file) { return file.absolutePath } /** * 解析模块名称为文件 * 按照下列顺序查找 * 当前目录 ./ * 父目录 ../ * 递归模块目录 ../node_modules 到root * 寻找 ${NODE_PATH} * @param {string} name 模块名称 * @param {string} parent 父目录 * @param {any} optional 附加参数 */ function resolve(name, parent, optional) { name = _canonical(name) || name // 解析本地目录 if (optional.local) { return resolveAsFile(name, parent) || resolveAsDirectory(name, parent) || undefined } else { // 解析Node目录 var dir = [parent, 'node_modules'].join(separatorChar) return resolveAsFile(name, dir) || resolveAsDirectory(name, dir) || // @ts-ignore (parent && parent.toString().startsWith(root) ? resolve(name, new File(parent).getParent(), optional) : resolveAsDirectory(name, NODE_PATH) || undefined) } } /** * 解析文件 * @param {string} file 文件 * @param {string | undefined} dir 目录 * @returns {*} */ function resolveAsFile(file, dir) { file = dir !== undefined ? new File(dir, file) : new File(file) // 直接文件 // @ts-ignore if (file.isFile()) { return file } // JS文件 var js = new File(normalizeName(_absolute(file), '.js')) if (js.isFile()) { return js } // JSON文件 var json = new File(normalizeName(_absolute(file), '.json')) if (json.isFile()) { return json } } /** * 解析目录 * @param {string} file 文件 * @param {string | undefined} dir 目录 * @returns {*} */ function resolveAsDirectory(file, dir) { dir = dir !== undefined ? new File(dir, file) : new File(file) var _package = new File(dir, 'package.json') if (_package.exists()) { // @ts-ignore var json = JSON.parse(base.read(_package)) if (json.main) { return resolveAsFile(json.main, dir) } } // if no package or package.main exists, look for index.js return resolveAsFile('index.js', dir) } /** * 后缀检测和添加 * @param {string} fileName 文件名称 * @param {string} ext 后缀 * @returns {*} */ function normalizeName(fileName, ext) { var extension = ext || '.js' if (fileName.endsWith(extension)) { return fileName } return fileName + extension } /** * 检查模块缓存 * @param {string} id 模块ID * @param {string} name 模块名称 * @param {any} file 模块文件 * @param {any} optional 附加选项 * @returns {Object} */ function getCacheModule(id, name, file, optional) { var module = cacheModules[id] if (optional.cache && module) { return module } return createModule(id, name, file, optional) } /** * 编译模块 * @param {string} id 模块ID * @param {string} name 模块名称 * @param {any} file 模块文件 * @param {any} optional 附加选项 * @returns {Object} */ function createModule(id, name, file, optional) { console.trace('Loading module', name + '(' + id + ')', 'Optional', JSON.stringify(optional)) var module = { id: id, exports: {}, loaded: false, require: getRequire(file.parentFile, id) } cacheModules[id] = module var cfile = _canonical(file) if (cfile.endsWith('.js')) { compileJs(module, file, __assign(optional, { id: id })) } else if (cfile.endsWith('.json')) { compileJson(module, file) } else if (cfile.endsWith('.msm')) { throw Error('Unsupported MiaoScript module!') } else { throw Error('Unknown file type ' + cfile) } return module } /** * 预编译JS * @param {any} module JS模块 * @param {any} file JS文件 * @param {any} optional 附加选项 * @returns {void} */ function compileJs(module, file, optional) { // @ts-ignore var origin = base.read(file) if (optional.hook) { origin = optional.hook(origin) } // 2019-09-19 使用 扩展函数直接 load 无需保存/删除文件 // 2020-02-16 结尾新增换行 防止有注释导致加载失败 // @ts-ignore var compiledWrapper = engineLoad({ script: '(function $(module, exports, require, __dirname, __filename) {' + origin + '\n});', name: optional.id }) compiledWrapper.apply(module.exports, [ module, module.exports, module.require, file.parentFile, file ]) module.loaded = true } /** * 预编译Json * @param {{ id?: string | null; exports?: {}; loaded: any; require?: any; }} module Json模块 * @param {any} file Json 文件 * @returns {void} */ function compileJson(module, file) { // @ts-ignore module.exports = JSON.parse(base.read(file)) module.loaded = true } /** * 尝试从网络下载依赖包 * @param {string} name 包名称 */ function download(name) { // handle name es6-map/implement => es6-map @ccms/common/dist/reflect => @ccms/common var name_arr = name.split('/') var module_name = name.startsWith('@') ? name_arr[0] + '/' + name_arr[1] : name_arr[0] // @ts-ignore var target = NODE_PATH + separatorChar + module_name var _package = new File(target, 'package.json') if (_package.exists()) { return } // at windows need replace file name java.lang.IllegalArgumentException: Invalid prefix or suffix var info = fetchPackageInfo(module_name) var url = info.versions[ModulesVersionLock[module_name] || info['dist-tags']['latest']].dist.tarball console.log('fetch node_module ' + module_name + ' from ' + url + ' waiting...') return executor.submit(new Callable(function () { var tis = new TarInputStream(new BufferedInputStream(new GZIPInputStream(new URL(url).openStream()))) // @ts-ignore var entry while ((entry = tis.getNextEntry()) != null) { var targetPath = Paths.get(target + separatorChar + entry.getName().substring(8)) targetPath.toFile().getParentFile().mkdirs() Files.copy(tis, targetPath, StandardCopyOption.REPLACE_EXISTING) } return name })).get() } /** * @param {string} module_name */ function fetchPackageInfo(module_name) { var content = '' try { content = fetchContent(NODE_REGISTRY + '/' + module_name) } catch (ex) { console.debug('can\'t fetch package ' + module_name + ' from ' + NODE_REGISTRY + ' registry. try fetch from ' + MS_NODE_REGISTRY + ' registry...') content = fetchContent(MS_NODE_REGISTRY + '/' + module_name) } return JSON.parse(content) } function fetchContent(url) { return executor.submit(new Callable(function () { var input = new URL(url).openStream() var output = new ByteArrayOutputStream() var buffer = new ByteArray(1024) try { var n while ((n = input.read(buffer)) !== -1) { output.write(buffer, 0, n) } return output.toString("UTF-8") } finally { output.close() } })).get() } var lastModule = '' /** * 检查核心模块 * @param {string} name * @param {string} path */ function checkCoreModule(name, path, optional) { if (name.startsWith('@ms') && lastModule.endsWith('.js')) { // @ts-ignore console.warn(lastModule + ' load deprecated module ' + name + ' auto replace to ' + (name = name.replace('@ms', global.scope)) + '...') return name } else { lastModule = name } if (CoreModules.indexOf(name) !== -1) { // @ts-ignore var newName = global.scope + '/nodejs/dist/' + name if (resolve(newName, path, optional) !== undefined) { return newName } // @ts-ignore throw new Error("Can't load nodejs core module " + name + " . maybe later will auto replace to " + global.scope + "/nodejs/" + name + ' to compatible...') } return name } /** * 检查缓存模块 */ function checkCacheModule(optional) { return optional.local ? cacheModuleIds[optional.parentId] && cacheModuleIds[optional.parentId][optional.path] : cacheModuleIds[optional.path] } /** * 加载模块 * @param {string} name 模块名称 * @param {string} path 路径 * @param {any} optional 附加选项 * @returns {*} */ function _require(name, path, optional) { // require direct file var file = _isFile(name) ? name : new File(name) if (_isFile(file)) { return _requireFile(file, optional) } // require cache module var cachePath = checkCacheModule(optional) var cacheFile = new File(cachePath) if (cachePath && cacheFile.exists()) { return _requireFile(cacheFile, optional) } // search module name = checkCoreModule(name, path, optional) if ((file = resolve(name, path, optional)) === undefined) { // excloud local dir, prevent too many recursive call and cache not found module if (optional.local || optional.recursive || notFoundModules[name]) { throw new Error("Can't found module " + name + '(' + JSON.stringify(optional) + ') at local ' + path + ' or network!') } try { optional.recursive = true return _require(download(name), path, optional) } catch (ex) { notFoundModules[name] = true throw new Error("Can't found module " + name + ' in directory ' + path + ' ERROR: ' + ex) } } setCacheModule(file, optional) return _requireFile(file, optional) } /** * 设置模块缓存 * @param {any} file * @param {any} optional */ function setCacheModule(file, optional) { if (optional.local) { var parent = cacheModuleIds[optional.parentId] if (!parent) { cacheModuleIds[optional.parentId] = {} } return cacheModuleIds[optional.parentId][optional.path] = _canonical(file) } return cacheModuleIds[optional.path] = _canonical(file) } function _requireFile(file, optional) { // 重定向文件名称和类型 return getCacheModule(_canonical(file), file.name.split('.')[0], file, optional) } /** * 闭包方法 * @param {string} parent 父目录 * @param {string} parentId * @returns {Function} */ function exports(parent, parentId) { /** * @param {string} path * @param {any} optional */ return function __DynamicRequire__(path, optional) { if (!path) { throw new Error('require path can\'t be undefined or empty!') } return _require(path, parent, __assign({ cache: true, parentId: parentId, parent: parent, path: path, local: path.startsWith('.') || path.startsWith('/') }, optional)).exports } } /** * @param {string} path * @param {any} optional 附加选项 */ function __DynamicResolve__(path, optional) { return _canonical(new File(resolve(path, parent, __assign({ cache: true, parent: parent, local: path.startsWith('.') || path.startsWith('/') }, optional)))) } /** * @param {string} name */ function __DynamicClear__(name) { for (var cacheModule in cacheModules) { if (cacheModule.indexOf(name) !== -1) { console.trace('Clear module ' + cacheModule + ' ...') delete cacheModules[cacheModule] } } } function __DynamicDisable__() { // @ts-ignore base.save(cacheModuleIdsFile, JSON.stringify(upgradeMode ? {} : cacheModuleIds)) for (var cacheModule in cacheModules) { delete cacheModules[cacheModule] } cacheModules = undefined for (var cacheModuleId in cacheModuleIds) { delete cacheModuleIds[cacheModuleId] } cacheModuleIds = undefined notFoundModules = undefined } function __setUpgradeMode__(status) { upgradeMode = status } /** * @param {string} parent * @param {string} parentId */ function getRequire(parent, parentId) { /** * @type {any} require */ var require = exports(parent, parentId) require.resolve = __DynamicResolve__ require.clear = __DynamicClear__ require.disable = __DynamicDisable__ require.setUpgradeMode = __setUpgradeMode__ require.core_modules = CoreModules return require } if (typeof parent === 'string') { parent = new File(parent) } /** * @type {{[key:string]:any}} cacheModules */ var cacheModules = {} var cacheModuleIdsFile = _canonical(new File(NODE_PATH, 'cacheModuleIds.json')) var localVersionLockFile = _canonical(new File(NODE_PATH, 'moduleVersionLock.json')) /** * @type {{[key:string]:{[key:string]:string}}} cacheModuleIds */ var cacheModuleIds = {} /** * @type {{[key:string]:boolean}} notFoundModules */ var notFoundModules = {} var upgradeMode = false var executor = Executors.newSingleThreadExecutor(function (r) { return new Thread(r, "MiaoScript require thread") }) console.info('Initialization require module. ParentDir:', _canonical(parent)) console.info('Require module env list:') console.info('- NODE_PATH:', NODE_PATH) console.info('- NODE_REGISTRY:', NODE_REGISTRY) console.info('- MS_NODE_REGISTRY:', MS_NODE_REGISTRY) try { // @ts-ignore cacheModuleIds = JSON.parse(base.read(cacheModuleIdsFile)) console.log('Read cacheModuleIds from file ' + cacheModuleIdsFile) } catch (error) { cacheModuleIds = {} console.log('Initialization new cacheModuleIds') } try { ModulesVersionLock = JSON.parse(fetchContent('http://ms.yumc.pw/api/plugin/download/name/version_lock')) try { // @ts-ignore ModulesVersionLock = __assign(ModulesVersionLock, JSON.parse(base.read(localVersionLockFile))) } catch (e) { } console.info('Lock module version List:') for (var key in ModulesVersionLock) { console.info('- ' + key + ': ' + ModulesVersionLock[key]) } } catch (error) { console.debug(error) ModulesVersionLock = {} } return getRequire(parent, "") })