etherpad-require-kernel
Version:
A reference implementation of a CommonJS module loader for Etherpad.
260 lines (230 loc) • 7.65 kB
JavaScript
;
/*
* require-kernel
*
* Created by Chad Weider on 01/04/11.
* Released to the Public Domain on 17/01/12.
*/
const fs = require('fs');
const pathutil = require('path');
const events = require('events');
const kernelPath = pathutil.join(__dirname, 'kernel.js');
const kernel = fs.readFileSync(kernelPath, 'utf8');
const buildKernel = require('vm').runInThisContext(
`(function (XMLHttpRequest) {return ${kernel}})`, kernelPath);
/* Cheap URL request implementation */
const fsClient = (new function () {
const STATUS_MESSAGES = {
403: '403: Access denied.',
404: '404: File not found.',
405: '405: Only the HEAD or GET methods are allowed.',
500: '500: Error reading file.',
};
this.request = (options, callback) => {
const path = fsPathForURIPath(options.path);
const method = options.method;
const response = new (require('events').EventEmitter)();
response.setEncoding = function (encoding) { this._encoding = encoding; };
response.statusCode = 504;
response.headers = {};
const request = new (require('events').EventEmitter)();
request.end = () => {
if (options.method !== 'HEAD' && options.method !== 'GET') {
response.statusCode = 405;
response.headers.Allow = 'HEAD, GET';
callback(response);
response.emit('data', STATUS_MESSAGES[response.statusCode]);
response.emit('end');
} else {
fs.stat(path, (error, stats) => {
if (error) {
if (error.code === 'ENOENT') {
response.StatusCode = 404;
} else if (error.code === 'EACCESS') {
response.StatusCode = 403;
} else {
response.StatusCode = 502;
}
} else if (stats.isFile()) {
const date = new Date();
const modifiedLast = new Date(stats.mtime);
const modifiedSince = (options.headers || {})['if-modified-since'];
response.headers.Date = date.toUTCString();
response.headers['Last-Modified'] = modifiedLast.toUTCString();
if (modifiedSince && modifiedLast &&
modifiedSince >= modifiedLast) {
response.StatusCode = 304;
} else {
response.statusCode = 200;
}
} else {
response.StatusCode = 404;
}
if (method === 'HEAD') {
callback(response);
response.emit('end');
} else if (response.statusCode !== 200) {
response.headers['Content-Type'] = 'text/plain; charset=utf-8';
callback(response);
response.emit('data', STATUS_MESSAGES[response.statusCode]);
response.emit('end');
} else {
fs.readFile(path, (error, text) => {
if (error) {
if (error.code === 'ENOENT') {
response.statusCode = 404;
} else if (error.code === 'EACCESS') {
response.statusCode = 403;
} else {
response.statusCode = 502;
}
response.headers['Content-Type'] = 'text/plain; charset=utf-8';
callback(response);
response.emit('data', STATUS_MESSAGES[response.statusCode]);
response.emit('end');
} else {
response.statusCode = 200;
response.headers['Content-Type'] =
'application/javascript; charset=utf-8';
callback(response);
response.emit('data', text);
response.emit('end');
}
});
}
});
}
};
return request;
};
}());
const requestURL = (url, method, headers, callback) => {
const parsedURL = new URL(url);
let client = undefined;
if (parsedURL.protocol === 'file:') {
client = fsClient;
} else if (parsedURL.protocol === 'http:') {
client = require('http');
} else if (parsedURL.protocol === 'https:') {
client = require('https');
}
if (client) {
const request = client.request({
host: parsedURL.host,
port: parsedURL.port,
path: parsedURL.pathname + parsedURL.search,
method,
headers,
}, (response) => {
let buffer = undefined;
response.setEncoding('utf8');
response.on('data', (chunk) => {
buffer = buffer || '';
buffer += chunk;
});
response.on('close', () => {
callback(502, {});
});
response.on('end', () => {
callback(response.statusCode, response.headers, buffer);
});
});
request.on('error', () => {
callback(502, {});
});
request.end();
}
};
const fsPathForURIPath = (path) => {
path = decodeURIComponent(path);
if (path.charAt(0) === '/') { // Account for '/C:\Windows' type of paths.
path = pathutil.resolve('/', path.slice(1));
}
path = pathutil.normalize(path);
return path;
};
const normalizePathAsURI = (path) => {
const parsedUrl = new URL(path, 'file:///');
if (parsedUrl.protocol === 'file:') parsedUrl.pathname = pathutil.resolve(parsedUrl.pathname);
return parsedUrl.href;
};
const buildMockXMLHttpRequestClass = () => {
const emitter = new events.EventEmitter();
let requestCount = 0;
let idleTimer = undefined;
const idleHandler = () => {
emitter.emit('idle');
};
const requested = (info) => {
clearTimeout(idleTimer);
requestCount++;
emitter.emit('requested', info);
};
const responded = (info) => {
emitter.emit('responded', info);
requestCount--;
if (requestCount === 0) {
idleTimer = setTimeout(idleHandler, 0);
}
};
const MockXMLHttpRequest = class {
open(method, url, async) {
this.async = async;
this.url = normalizePathAsURI(url);
}
send() {
const parsedURL = new URL(this.url);
const info = {
async: !!this.async,
url: this.url,
};
if (!this.async) {
if (parsedURL.protocol === 'file:') {
requested(info);
try {
this.status = 200;
const path = fsPathForURIPath(parsedURL.pathname);
this.responseText = fs.readFileSync(path);
} catch (e) {
this.status = 404;
}
this.readyState = 4;
responded(info);
} else {
throw new Error(
`The resource at ${JSON.stringify(this.url)} cannot be retrieved synchronously.`);
}
} else {
const self = this;
requestURL(this.url, 'GET', {},
(status, headers, content) => {
self.status = status;
self.responseText = content;
self.readyState = 4;
const handler = self.onreadystatechange;
handler && handler();
responded(info);
}
);
requested(info);
}
}
};
MockXMLHttpRequest.emitter = emitter;
MockXMLHttpRequest.withCredentials = false; // Pass CORS capability checks.
return MockXMLHttpRequest;
};
const requireForPaths = (rootPath, libraryPath) => {
const MockXMLHttpRequest = buildMockXMLHttpRequestClass();
const mockRequire = buildKernel(MockXMLHttpRequest);
if (rootPath !== undefined) {
mockRequire.setRootURI(normalizePathAsURI(rootPath));
}
if (libraryPath !== undefined) {
mockRequire.setLibraryURI(normalizePathAsURI(libraryPath));
}
mockRequire.emitter = MockXMLHttpRequest.emitter;
return mockRequire;
};
exports.kernelSource = kernel;
exports.requireForPaths = requireForPaths;