UNPKG

@feroomjs/server

Version:
681 lines (654 loc) 27.6 kB
'use strict'; var npmFetcher = require('@feroomjs/npm-fetcher'); var EventEmitter = require('events'); var moost = require('moost'); var infact = require('@prostojs/infact'); var eventHttp = require('@wooksjs/event-http'); var wooks = require('wooks'); var path = require('path'); var eventHttp$1 = require('@moostjs/event-http'); var mate = require('@prostojs/mate'); /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ function __decorate(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; } function __param(paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } } function __metadata(metadataKey, metadataValue) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(metadataKey, metadataValue); } function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } const banner = () => `[${"@feroomjs/server"}][${new Date().toISOString().replace('T', ' ').replace(/\.\d{3}z$/i, '')}] `; /* istanbul ignore file */ function log(text) { console.log('' + '' + banner() + text + ''); } function logError(error) { console.error('' + '' + banner() + error + ''); } function panic(error) { logError(error); return new Error(error); } const registry = {}; function safeString(file) { if (typeof file === 'string') return file; if (file.type === 'Buffer') return Buffer.from(file.data).toString(); throw new Error('unexpected file type ' + file.type); } class FeRegistry extends EventEmitter { normalizeModuleData(data) { var _a, _b, _c, _d; const files = data.files; if (!files) { throw panic(`Failed to normallize module "${data.id}": no files in module`); } let pkg; try { pkg = JSON.parse(files['package.json'] || '{}'); } catch (e) { panic('Could not parse package.json file'); console.error(e); pkg = {}; } let feConf; try { feConf = JSON.parse(safeString(files['dist/feroom.config.json'] || files['feroom.config.json'] || '{}')); } catch (e) { panic('Could not parse feroom.config.json file'); throw e; } feConf.registerOptions = feConf.registerOptions || {}; feConf.extensions = feConf.extensions || {}; Object.assign(feConf.registerOptions, ((_a = data.config) === null || _a === void 0 ? void 0 : _a.registerOptions) || {}); Object.assign(feConf.extensions, ((_b = data.config) === null || _b === void 0 ? void 0 : _b.extensions) || {}); const module = { id: data.id || ((_c = feConf.registerOptions) === null || _c === void 0 ? void 0 : _c.id) || pkg.name, version: data.version || pkg.version, entry: data.entry || ((_d = feConf.registerOptions) === null || _d === void 0 ? void 0 : _d.entry) || pkg.module, files, source: data.source || '', activate: !!data.activate, config: feConf, }; if (!module.id) { throw panic(`Failed to normallize module "${module.id}": missing "id".`); } if (!module.entry) { throw panic(`Failed to normallize module "${module.id}": missing "entry". Please make sure package.json has "module" value.`); } return module; } registerModule(data) { var _a; const normData = this.normalizeModuleData(data); const module = registry[normData.id] = registry[normData.id] || { activeVersion: normData.version, versions: {} }; if (normData.activate) { module.activeVersion = normData.version; } module.versions[normData.version] = normData; log(`Module has been registered ${''}${normData.id} v${normData.version}. Active version: ${module.activeVersion}`); this.emit('register-module', normData); if ((_a = normData.config.registerOptions) === null || _a === void 0 ? void 0 : _a.importNpmDependencies) { for (const [dep, conf] of Object.entries(normData.config.registerOptions.importNpmDependencies)) { void this.registerFromNpm(Object.assign(Object.assign({}, conf), { name: dep, activateIfNewer: normData.activate })); } } return Object.assign(Object.assign({}, normData), { files: Object.keys(normData.files) }); } registerFromNpm(npmData) { return __awaiter(this, void 0, void 0, function* () { if (!npmData.name) { throw panic('Can not register npm module: option "name" is not provided.'); } const registry = npmData.registry || 'https://registry.npmjs.org'; const version = yield npmFetcher.getNpmPackageVersion(registry, npmData.name, npmData.version); const exists = this.exists(npmData.name, version); const activeVersion = this.getActiveVersion(npmData.name, true); if (!npmData.forceRegister && exists) { log(`Module ${''}${npmData.name} v${version}${''} already registered. Nothing changed. Use "forceRegister" option to force re-register of the module.`); return 'Module already exists'; } const files = yield npmFetcher.getNpmPackageFiles(registry, npmData.name, version); const pkg = JSON.parse(files['package.json'] || '{}'); let shouldActivate = npmData.activate; if (!shouldActivate && npmData.activateIfNewer) { if (!activeVersion) { shouldActivate = true; } else { shouldActivate = activeVersion < version; } } const module = { id: npmData.id || pkg.name || npmData.name, version: version, files, source: 'npm:' + registry, activate: shouldActivate, }; return this.registerModule(module); }); } readModule(id, version) { const reg = registry[id]; if (!reg) throw panic(`No module "${id}" found`); const ver = version || reg.activeVersion; if (!reg.versions[ver]) throw panic(`No module version "${ver}" found for module "${id}"`); return reg.versions[ver]; } exists(id, version) { const reg = registry[id]; if (reg) { const ver = version || reg.activeVersion; return !!reg.versions[ver]; } return false; } getActiveVersion(id, silent = false) { const reg = registry[id]; if (silent) return (reg === null || reg === void 0 ? void 0 : reg.activeVersion) || ''; if (!reg) throw panic(`No module "${id}" found`); return reg.activeVersion; } getModulesList() { return Object.keys(registry); } getAllModules() { const list = this.getModulesList(); return list.map(id => this.readModule(id)); } } class FeRoomConfig { constructor(options) { this.options = options; } get modulesPrefixPath() { return this.options.modulesPrefixPath || 'feroom-module/'; } get globals() { return this.options.globals || {}; } get title() { return this.options.title || 'FeRoom'; } get preloadCss() { return this.options.preloadCss || []; } get preloadScript() { return this.options.preloadScript || []; } get preloadModule() { return this.options.preloadModule || []; } get body() { return this.options.body || ''; } get head() { return this.options.head || ''; } get importMap() { return this.options.importMap || {}; } get npmDeps() { const deps = this.options.importNpmDependencies || {}; return Object.entries(deps).map(([name, value]) => (Object.assign(Object.assign({}, value), { name }))); } } function renderCssTag(path) { return `<link type="text/css" rel="stylesheet" href="${path}">`; } function renderModuleScriptTag(path) { return `<script type="module" src="${path}"></script>`; } class FeModule { constructor(data, config) { this.data = data; this.config = config; } get id() { return this.data.id; } get files() { return this.data.files; } getGlobals() { return this.getRegisterOptions().globals || {}; } getRegisterOptions() { return this.data.config.registerOptions || {}; } getExtensions() { return this.data.config.extensions; } buildPath(path$1, version) { return path.join(this.config.modulesPrefixPath, this.data.id + `@${version || this.data.version}`, path$1); } entryPath(version) { return this.buildPath(this.data.entry || '', version || this.data.version); } hasEntry() { return !!this.data.entry; } renderPreloadCss() { const items = [this.getRegisterOptions().preloadCss || []].flat(1); let content = ''; if (items.length) { content += this.renderComment('Preload Css'); } return content + items.map(path => renderCssTag(this.buildPath(path))).join('\n') + '\n'; } renderPreloadScript() { const items = [this.getRegisterOptions().preloadScripts || []].flat(1); let content = ''; if (items.length) { content += this.renderComment('Preload Script'); } return content + items.map(path => renderModuleScriptTag(this.buildPath(path))).join('\n') + '\n'; } renderComment(text) { return `<!-- ${this.id}@${this.data.version}: ${text} -->\n`; } renderPreloadModule() { return this.renderComment('Preload Entry') + renderModuleScriptTag(this.entryPath()); } getImportMap(reg) { var _a; const map = {}; if (this.hasEntry()) { map[this.data.id] = '/' + this.entryPath(this.data.version); } if ((_a = this.data.config.registerOptions) === null || _a === void 0 ? void 0 : _a.lockDependency) { for (const [dep, ver] of Object.entries(this.data.config.registerOptions.lockDependency)) { // const active = reg.getActiveVersion(dep) const m = reg.readModule(dep, ver); if (m) { map[m.id + '@' + ver] = '/' + (new FeModule(m, this.config).entryPath(ver)); } } } return map; } } exports.FeRoomServe = class FeRoomServe { constructor(_registry, wHttp, config) { this._registry = _registry; this.wHttp = wHttp; this.config = config; this.registered = {}; this.updateModulePaths(); this._registry.on('register-module', (module) => this.registerHttpModulePath(module)); } registerHttpModulePath(data) { if (!this.registered[data.id]) { const serve = () => { return this.serveModule(data.id, wooks.useRouteParams().get('version')); }; const serveFile = () => { return this.serveModule(data.id, wooks.useRouteParams().get('version'), wooks.useRouteParams().get('*')); }; this.registered[data.id] = true; this.wHttp.get(this.config.modulesPrefixPath + data.id, serve); this.wHttp.get(this.config.modulesPrefixPath + data.id + '/*', serveFile); this.wHttp.get(this.config.modulesPrefixPath + data.id + '@:version', serve); this.wHttp.get(this.config.modulesPrefixPath + data.id + '@:version' + '/*', serveFile); log(`• ${''}(GET)${''}/${this.config.modulesPrefixPath + data.id} → FeRoomServe[${data.id}]`); } return data; } serveModule(id, version, path) { const status = eventHttp.useStatus(); const location = eventHttp.useSetHeader('location'); const contentType = eventHttp.useSetHeader('content-type'); const module = new FeModule(this._registry.readModule(id, version), this.config); if (!path) { status.value = 307; location.value = '/' + module.entryPath(version); return ''; } const ext = (path.split('.').pop() || ''); // if ((!ext || ext === path || ext === 'vue' || ext === 'sass') && !module.files[path]) { // if (ext === 'vue') { // path = path.replace(/vue$/, 'js') // ext = 'js' // } else if (ext === 'sass') { // path = path.replace(/sass$/, 'css') // ext = 'css' // } else { // if (!module.files[path + '.js']) { // path = path + '/index' // } // path = path + '.js' // ext = 'js' // } // } contentType.value = extensions[ext] || 'text/plain'; const data = module.files[path]; if (typeof data === 'string') { return data; } else if (data instanceof Buffer) { return data; } else if (typeof data === 'object' && data.type === 'Buffer') { return Buffer.from(data.data); } return new eventHttp.HttpError(404); } updateModulePaths() { const list = this._registry.getModulesList(); for (const item of list) { const module = this._registry.readModule(item); this.registerHttpModulePath(module); } } }; exports.FeRoomServe = __decorate([ moost.Controller(), __metadata("design:paramtypes", [FeRegistry, eventHttp.WooksHttp, FeRoomConfig]) ], exports.FeRoomServe); const extensions = { 'js': 'application/javascript', 'vue': 'application/javascript', 'mjs': 'application/javascript', 'json': 'application/json', 'map': 'application/json', 'cjs': 'application/node', 'xhtml': 'application/xhtml+xml', 'otf': 'font/otf', 'ttf': 'font/ttf', 'woff': 'font/woff', 'woff2': 'font/woff2', 'css': 'text/css', 'sass': 'text/css', 'csv': 'text/csv', 'html': 'text/html', 'htm': 'text/html', 'shtml': 'text/html', 'jsx': 'text/jsx', 'jpeg': 'image/jpeg', 'jpg': 'image/jpeg', 'png': 'image/png', 'svg': 'image/svg+xml', 'svgz': 'image/svg+xml', 'webp': 'image/webp', 'md': 'text/markdown', }; exports.FeRoomIndex = class FeRoomIndex { constructor(_registry, config, ext) { this._registry = _registry; this.config = config; this.ext = ext; } getExtInstances() { return __awaiter(this, void 0, void 0, function* () { const instances = []; for (const ext of this.ext) { instances.push(yield ext()); } return instances; }); } getExtHead() { return __awaiter(this, void 0, void 0, function* () { return (yield this.getExtInstances()).map(e => e.instance.injectHead && (`<!-- EXT: ${e.name} -->\n` + e.instance.injectHead())).join('\n') + '\n'; }); } getExtBody() { return __awaiter(this, void 0, void 0, function* () { return (yield this.getExtInstances()).map(e => e.instance.injectIndexBody && (`<!-- EXT: ${e.name} -->\n` + e.instance.injectIndexBody())).join('\n') + '\n'; }); } getModules() { return this._registry.getAllModules().map(data => new FeModule(data, this.config)); } getGlobals(modules) { return __awaiter(this, void 0, void 0, function* () { let obj = {}; (yield this.getExtInstances()).forEach(e => e.instance.injectGlobals && Object.assign(obj, e.instance.injectGlobals())); modules.forEach(m => Object.assign(obj, m.getGlobals())); obj = Object.assign(Object.assign(Object.assign({}, obj), this.config.globals), { __feroom: { modulesPrefixPath: this.config.modulesPrefixPath } }); return Object.keys(obj).map(key => `window[${JSON.stringify(key)}] = ${JSON.stringify(obj[key])};\n`).join(''); }); } getImportmap(modules) { return __awaiter(this, void 0, void 0, function* () { const map = {}; modules.forEach(module => Object.assign(map, module.getImportMap(this._registry))); (yield this.getExtInstances()).map(e => e.instance.injectImportMap && Object.assign(map, e.instance.injectImportMap())); return JSON.stringify(Object.assign(Object.assign({}, map), this.config.importMap), null, ' '); }); } getCss(modules) { const preloadCss = this.config.preloadCss || []; let content = ''; preloadCss.forEach(item => { if (typeof item === 'string') { content += renderCssTag(item) + '\n'; } else { const module = new FeModule(this._registry.readModule(item[0]), this.config); content += renderCssTag(module.buildPath(item[1])) + '\n'; } }); modules.forEach(m => content += m.renderPreloadCss()); return content; } getScripts(modules) { const preloadScript = this.config.preloadScript || []; let content = ''; preloadScript.forEach(item => { if (typeof item === 'string') { content += renderModuleScriptTag(item) + '\n'; } else { const module = new FeModule(this._registry.readModule(item[0]), this.config); content += renderModuleScriptTag(module.buildPath(item[1])) + '\n'; } }); modules.forEach(m => content += m.renderPreloadScript()); return content; } getPreloadModule(modules) { const items = []; modules.forEach(m => (this.config.preloadModule.includes(m.id) || [true, 'head'].includes(m.getRegisterOptions().preloadEntry)) && items.push(m)); return items .map(m => m.renderPreloadModule()) .join('\n'); } getHead(modules) { return __awaiter(this, void 0, void 0, function* () { let content = `<title>${this.config.title}</title>\n` + (this.config.head || '') + '\n'; modules.forEach(m => m.getRegisterOptions().appendHead ? content += m.renderComment('Append Head') + m.getRegisterOptions().appendHead + '\n' : null); return content + (yield this.getExtHead()); }); } getBody(modules) { return __awaiter(this, void 0, void 0, function* () { let content = (this.config.body || '') + '\n'; modules.forEach(m => m.getRegisterOptions().preloadEntry === 'body:first' && (content += m.renderPreloadModule() + '\n')); modules.forEach(m => m.getRegisterOptions().appendBody ? content += m.renderComment('Append Body') + m.getRegisterOptions().appendBody + '\n' : null); modules.forEach(m => m.getRegisterOptions().preloadEntry === 'body:last' && (content += m.renderPreloadModule() + '\n')); return content + (yield this.getExtBody()); }); } index() { return __awaiter(this, void 0, void 0, function* () { const modules = this.getModules(); return `<html> <head> ${yield this.getHead(modules)} <script> // globals ${yield this.getGlobals(modules)} </script> <script> window.__loadCss = function (path) { const head = document.getElementsByTagName('head')[0]; const link = document.createElement('link'); link.rel = 'stylesheet'; link.type = 'text/css'; link.href = './' + path; link.media = 'all'; head.appendChild(link); } </script> <script type="importmap"> { "imports": ${yield this.getImportmap(modules)} } </script> ${this.getScripts(modules)} ${this.getPreloadModule(modules)} ${this.getCss(modules)} </head> <body> ${yield this.getBody(modules)} </body> </html>`; }); } }; __decorate([ eventHttp$1.Get(''), eventHttp$1.Get('index.html'), eventHttp$1.SetHeader('content-type', 'text/html'), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], exports.FeRoomIndex.prototype, "index", null); exports.FeRoomIndex = __decorate([ moost.Controller(), __param(2, moost.Inject('FEROOM_EXT_ARRAY')), __metadata("design:paramtypes", [FeRegistry, FeRoomConfig, Array]) ], exports.FeRoomIndex); let FeRoomApi = class FeRoomApi { constructor(_registry) { this._registry = _registry; this.registered = {}; } registerModule(module) { return this._registry.registerModule(module); } registerFromNpm(npmData) { return __awaiter(this, void 0, void 0, function* () { return yield this._registry.registerFromNpm(npmData); }); } }; __decorate([ eventHttp$1.Post('feroom-module/register'), __param(0, eventHttp$1.Body()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", void 0) ], FeRoomApi.prototype, "registerModule", null); __decorate([ eventHttp$1.Post('feroom-module/register/npm'), __param(0, eventHttp$1.Body()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", Promise) ], FeRoomApi.prototype, "registerFromNpm", null); FeRoomApi = __decorate([ moost.Controller(), __metadata("design:paramtypes", [FeRegistry]) ], FeRoomApi); const feroomMate = moost.getMoostMate(); const insureInjectable = feroomMate.decorate((meta) => { if (!meta.injectable) meta.injectable = true; return meta; }); function FeRoomExtension(name) { if (!name) { throw panic('Decorator @FeRoomExtension(name: string) requires "name" to be filled. Received empty name.'); } return feroomMate.apply(insureInjectable, feroomMate.decorateClass('feroom_isExtension', true), feroomMate.decorateClass('feroom_extensionName', name)); } class FeRoom extends moost.Moost { constructor(options, registry) { super(); this._ext = []; this._registry = registry || new FeRegistry(); this._config = new FeRoomConfig(options || {}); this.setProvideRegistry(infact.createProvideRegistry([FeRegistry, () => this._registry], [FeRoomConfig, () => this._config], ['FEROOM_EXT_ARRAY', () => this._ext])); this.registerControllers(exports.FeRoomServe, exports.FeRoomIndex, FeRoomApi); } init() { const _super = Object.create(null, { init: { get: () => super.init } }); return __awaiter(this, void 0, void 0, function* () { yield _super.init.call(this); for (const dep of this._config.npmDeps) { yield this._registry.registerFromNpm(dep); } }); } ext(...args) { const infact = moost.getMoostInfact(); const thisMeta = feroomMate.read(this); for (const ext of args) { const meta = feroomMate.read(ext); if (!(meta === null || meta === void 0 ? void 0 : meta.feroom_isExtension)) { throw panic('FeRoom.ext() has received class with no @FeRoomExtension decorator. Please use @FeRoomExtension decorator.'); } if (!(meta === null || meta === void 0 ? void 0 : meta.feroom_extensionName)) { throw panic('FeRoom.ext() has received extensiom with no name. Make sure you pass a name to @FeRoomExtension decorator.'); } if (meta === null || meta === void 0 ? void 0 : meta.controller) { this.registerControllers(ext); } if (mate.isConstructor(ext)) { this._ext.push(() => __awaiter(this, void 0, void 0, function* () { infact.silent(); const instance = yield infact.get(ext, { provide: Object.assign(Object.assign({}, ((thisMeta === null || thisMeta === void 0 ? void 0 : thisMeta.provide) || {})), this.provide) }); infact.silent(false); return { instance, name: meta.feroom_extensionName }; })); } else { infact.setProvideRegByInstance(ext, Object.assign(Object.assign({}, ((thisMeta === null || thisMeta === void 0 ? void 0 : thisMeta.provide) || {})), this.provide)); this._ext.push(() => ({ instance: ext, name: meta.feroom_extensionName })); } log(`Extension ${''}${meta === null || meta === void 0 ? void 0 : meta.feroom_extensionName}${'' + ''} has been installed.`); } } } exports.FeRegistry = FeRegistry; exports.FeRoom = FeRoom; exports.FeRoomConfig = FeRoomConfig; exports.FeRoomExtension = FeRoomExtension; exports.feroomMate = feroomMate;