@maskedeng-tom/ssrsx
Version:
server side renderer with tsx
353 lines • 15.8 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (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());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ssrsx = void 0;
const mime_types_1 = __importDefault(require("mime-types"));
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const events_1 = require("events");
const ws_1 = require("ws");
const log_1 = require("./lib/log");
const getDir_1 = require("./lib/getDir");
const compiler_1 = require("./core/compiler");
const errorConsole_1 = require("./lib/errorConsole");
const jsx_parser_1 = require("ssrsxjsx/jsx-parser");
const addSlash_1 = require("./router/lib/addSlash");
const sendData_1 = require("./server/sendData");
const support_1 = require("./server/support");
////////////////////////////////////////////////////////////////////////////////
let HotReloadDefault = 33730;
////////////////////////////////////////////////////////////////////////////////
const ssrsx = (ssrsxOption) => __awaiter(void 0, void 0, void 0, function* () {
//////////////////////////////////////////////////////////////////////////////
var _a, _b, _c, _d, _e, _f;
const option = ssrsxOption !== null && ssrsxOption !== void 0 ? ssrsxOption : {};
if (option === null || option === void 0 ? void 0 : option.development) {
option.hotReload = (_a = option.hotReload) !== null && _a !== void 0 ? _a : true;
option.sourceMap = (_b = option.sourceMap) !== null && _b !== void 0 ? _b : true;
}
option.ignoreExtensions = (_c = option.ignoreExtensions) !== null && _c !== void 0 ? _c : [
'js', 'ts', 'tsx',
'css',
'json',
'png', 'jpg', 'jpeg', 'gif', 'svg',
'ico',
'mp4', 'webm', 'ogg', 'mp3', 'wav',
'woff', 'woff2', 'ttf', 'otf', 'eot',
];
//////////////////////////////////////////////////////////////////////////////
(0, log_1.log)('-----------------------------------------------');
(0, log_1.log)('Start ssrsx');
(0, log_1.log)('-----------------------------------------------');
(0, log_1.log)('option =', option);
(0, log_1.log)('-----------------------------------------------');
//////////////////////////////////////////////////////////////////////////////
const clientScriptBaseUrl = (_d = option === null || option === void 0 ? void 0 : option.clientScriptBaseUrl) !== null && _d !== void 0 ? _d : '/ssrsx-client-script/';
const requireJs = 'require.js';
const eventLoaderJs = 'event-loader.js';
//////////////////////////////////////////////////////////////////////////////
const clientRoot = (0, getDir_1.getDir)(option === null || option === void 0 ? void 0 : option.clientRoot, './src/client');
const staticRoot = (0, getDir_1.getDir)(option === null || option === void 0 ? void 0 : option.staticRoot, './src/static');
const requireJsRoot = (0, getDir_1.getDir)(option === null || option === void 0 ? void 0 : option.requireJsRoot, './src/requireJs');
const jsRoot = (0, getDir_1.getDir)(option === null || option === void 0 ? void 0 : option.jsRoot, './src/js');
//
const clientOffset = clientRoot.replace(RegExp(`^${process.cwd()}`), '');
//
const baseUrl = (0, addSlash_1.removeLastSlash)((0, addSlash_1.addFirstSlash)((_e = option === null || option === void 0 ? void 0 : option.baseUrl) !== null && _e !== void 0 ? _e : ''));
(0, log_1.log)('clientRoot:', clientRoot);
(0, log_1.log)('clientOffset:', clientOffset);
(0, log_1.log)('staticRoot:', staticRoot);
(0, log_1.log)('requireJsRoot:', requireJsRoot);
(0, log_1.log)('jsRoot:', jsRoot);
(0, log_1.log)('baseUrl:', baseUrl);
(0, log_1.log)('clientScriptBaseUrl:', clientScriptBaseUrl);
(0, log_1.log)('-----------------------------------------------');
//////////////////////////////////////////////////////////////////////////////
let serviceStart = new Date();
//////////////////////////////////////////////////////////////////////////////
// source map handler
const sourceMapHandler = (server) => {
if (!(option === null || option === void 0 ? void 0 : option.sourceMap)) {
return false;
}
const url = (0, support_1.getPathname)(server);
if (url.slice(-3) === '.js' || url.indexOf(clientOffset) < 0) {
return false;
}
const targetMapPath = path_1.default.normalize(path_1.default.join(clientRoot.slice(0, -clientOffset.length), url));
if (!fs_1.default.existsSync(targetMapPath)) {
return false;
}
// for directory traversal attack
if (targetMapPath.indexOf(clientRoot) !== 0) {
return false;
}
(0, sendData_1.sendData)(server, {
status: 200,
type: 'text/plain',
body: fs_1.default.readFileSync(targetMapPath).toString(),
serviceStart,
lastModified: fs_1.default.statSync(targetMapPath).mtime,
source: targetMapPath,
});
return true;
};
//////////////////////////////////////////////////////////////////////////////
// ssrsx handler
yield (0, compiler_1.compileAll)(clientRoot, path_1.default.join(jsRoot, clientScriptBaseUrl), {
target: 'ES6',
module: 'umd',
inlineSourceMap: (_f = option === null || option === void 0 ? void 0 : option.sourceMap) !== null && _f !== void 0 ? _f : false,
removeComments: true,
esModuleInterop: true,
forceConsistentCasingInFileNames: true,
strict: true,
skipLibCheck: true,
moduleResolution: 'node',
});
const jsHandler = (server) => {
// target pathname
const url = (0, support_1.getPathname)(server);
// ssrsx
if (url.indexOf(clientScriptBaseUrl) < 0) {
return false;
}
const targetPathname = url.replace(RegExp(`^${clientScriptBaseUrl}`), '');
// compiled jsRoot
const staticJsFile = path_1.default.join(jsRoot, clientScriptBaseUrl, targetPathname);
if (fs_1.default.existsSync(staticJsFile)) {
if (staticJsFile.indexOf(jsRoot) === 0) {
(0, sendData_1.sendData)(server, {
status: 200,
type: 'text/javascript',
body: fs_1.default.readFileSync(staticJsFile).toString(),
source: staticJsFile,
serviceStart,
});
return true;
}
}
// requireJsRoot
const requireJsFile = path_1.default.join(requireJsRoot, targetPathname);
if (fs_1.default.existsSync(requireJsFile)) {
if (requireJsFile.indexOf(requireJsRoot) === 0) {
(0, sendData_1.sendData)(server, {
status: 200,
type: 'text/javascript',
body: fs_1.default.readFileSync(requireJsFile).toString(),
source: requireJsFile,
serviceStart,
});
return true;
}
}
// error
(0, sendData_1.sendData)(server, {
status: 200,
type: 'text/javascript',
body: (0, errorConsole_1.errorConsole)('Invalid event handler', `${targetPathname.split('.').slice(0, -1).join('.')} (${targetPathname})`),
source: targetPathname,
serviceStart,
errorLog: true,
});
return true;
};
//////////////////////////////////////////////////////////////////////////////
// static file
const staticFileHandlerCore = (server, root) => {
const url = (0, support_1.getPathname)(server);
// static file route
const filepath = path_1.default.normalize(path_1.default.join(root, url.replace(RegExp(`^${baseUrl}`), '')));
// for directory traversal attack
if (filepath.indexOf(root) !== 0) {
return false;
}
// check ext
const ext = path_1.default.extname(filepath);
if (ext === '.ts' || ext === '.tsx' ||
!fs_1.default.existsSync(filepath) ||
fs_1.default.lstatSync(filepath).isDirectory()) {
return false;
}
//
(0, sendData_1.sendData)(server, {
status: 200,
type: mime_types_1.default.lookup(path_1.default.extname(filepath).slice(1)) || 'text/plain',
body: fs_1.default.readFileSync(filepath),
source: filepath,
serviceStart,
lastModified: fs_1.default.statSync(filepath).mtime,
});
return true;
};
const staticFileHandler = (server) => {
if (staticFileHandlerCore(server, staticRoot)) {
return true;
}
if (staticFileHandlerCore(server, jsRoot)) { // TODO jsRoot -> workRoot
return true;
}
if (staticFileHandlerCore(server, clientRoot)) {
return true;
}
return false;
};
//////////////////////////////////////////////////////////////////////////////
// app
const addHead = (body, head) => {
// find /head
const headCloseTag = /<(\s*)\/(\s*)head(\s*)>/i;
// insert style
return `${String(body).replace(headCloseTag, `${head}</head>`)}`;
};
/////////////////////////////////////////
const addStyles = (body, styles) => {
// add styles
const addStyle = `<style>${styles}</style>`;
// find /head
const headCloseTag = /<(\s*)\/(\s*)head(\s*)>/i;
// insert style
return `${String(body).replace(headCloseTag, `${addStyle}</head>`)}`;
};
/////////////////////////////////////////
const addScripts = (server, body, events) => {
var _a, _b, _c;
// requireJs
const requireJsOptions = {
baseUrl: clientScriptBaseUrl,
urlArgs: 't=' + serviceStart.getTime().toString(36),
paths: option === null || option === void 0 ? void 0 : option.requireJsPaths,
};
// nonce
const nonce = (0, support_1.getNonce)(server);
// add scripts
const addScript = `
<script src="${clientScriptBaseUrl}${requireJs}"></script>
<script ${nonce ? `nonce="${nonce}"` : ''}>
ssrsxOptions = {
events: ${JSON.stringify(events)},
hotReload: ${(option === null || option === void 0 ? void 0 : option.hotReload) ? hotReloadPort : false},
hotReloadWait: ${(_a = option === null || option === void 0 ? void 0 : option.hotReloadWait) !== null && _a !== void 0 ? _a : 1000},
hotReloadWaitMax: ${(_b = option === null || option === void 0 ? void 0 : option.hotReloadWaitMax) !== null && _b !== void 0 ? _b : 1000 * 5},
hotReloadWaitInclement: ${(_c = option === null || option === void 0 ? void 0 : option.hotReloadWaitInclement) !== null && _c !== void 0 ? _c : 500},
requireJsConfig: ${JSON.stringify(requireJsOptions)},
};
</script>
<script src="${clientScriptBaseUrl}${eventLoaderJs}"></script>
`.split('\n').map((s) => s.trim()).join('');
// find /body
const bodyCloseTag = /<(\s*)\/(\s*)body(\s*)>/i;
// body tag not found
if (bodyCloseTag.test(body) === false) {
return `<!DOCTYPE html>${body}${addScript}<script>${(0, errorConsole_1.errorConsole)('body tag not found', 'by ROUTER')}</script>`;
}
// insert script
return `<!DOCTYPE html>${String(body).replace(bodyCloseTag, `${addScript}</body>`)}`;
};
/////////////////////////////////////////
const appHandler = (server) => __awaiter(void 0, void 0, void 0, function* () {
var _g;
const url = (0, support_1.getPathname)(server);
if (url.indexOf(baseUrl) !== 0 && !(option === null || option === void 0 ? void 0 : option.app)) {
return false;
}
const ext = url.split('.').slice(-1).join('');
if (((_g = option.ignoreExtensions) !== null && _g !== void 0 ? _g : []).indexOf(ext) >= 0) {
return false;
}
// userContext
const userContext = yield (0, support_1.getUserContext)(server, option);
// parse
const result = yield (0, jsx_parser_1.parse)(option.app, server, userContext, baseUrl);
// global context
const globalContext = result.context.parseContext.global;
// add head
if (globalContext.head) {
result.body = addHead(result.body, (yield (0, jsx_parser_1.parse)(globalContext.head, server, userContext, baseUrl)).body);
}
//
(0, sendData_1.sendData)(server, {
status: 200,
type: 'text/html',
body: addScripts(server, addStyles(result.body, result.context.styles.join('\n')), result.context.events),
source: url,
serviceStart,
lastModified: globalContext.lastModified,
redirect: globalContext.redirect
});
//
return true;
});
//////////////////////////////////////////////////////////////////////////////
// for hot reload
const hotReloadPort = HotReloadDefault++;
const eventEmitter = new events_1.EventEmitter();
let wsServer;
if (option === null || option === void 0 ? void 0 : option.hotReload) {
const port = (typeof option.hotReload === 'boolean') ? hotReloadPort : option.hotReload;
//
eventEmitter.on('hotReload', () => {
if (wsServer) {
wsServer.clients.forEach((socket) => {
socket.close();
process.nextTick(() => {
if (socket.readyState === socket.OPEN || socket.readyState === socket.CLOSING) {
socket.terminate();
}
});
});
wsServer.close();
}
(0, log_1.log)('Hot reload start:', port);
serviceStart = new Date();
wsServer = new ws_1.Server({ port });
});
//
eventEmitter.emit('hotReload');
//
}
const hotReload = () => {
eventEmitter.emit('hotReload');
};
//////////////////////////////////////////////////////////////////////////////
const middleware = (server) => __awaiter(void 0, void 0, void 0, function* () {
// source map request
if (sourceMapHandler(server)) {
return true;
}
// js handler
if (jsHandler(server)) {
return true;
}
// static handler
if (staticFileHandler(server)) {
return true;
}
// app
if (yield appHandler(server)) {
return true;
}
// next
return false;
});
return {
hotReloadPort,
hotReload,
middleware,
};
});
exports.ssrsx = ssrsx;
////////////////////////////////////////////////////////////////////////////////
exports.default = ssrsx;
//# sourceMappingURL=core.js.map