-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
executable file
·341 lines (297 loc) · 11.6 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
const dom = require('domino');
const vm = require('vm');
const fs = require('fs');
const path = require("path");
const COMPILE_SURPLUS = {};
const cache = {};
var compiler = null;
// Compile a file with the supplied compiler (set by serve())
const compile = (file) => {
if (!(file in cache)) {
const data = fs.readFileSync(file, 'utf8');
try {
cache[file] = compiler(data);
}
catch (err) {
console.error("Error occurred when compiling " + file + ":");
console.error(err);
throw err;
}
}
return cache[file];
};
// Initial global context to be used when executing scripts
const global = {
document: dom.createDocument()
};
// 'Node' is required by the surplus runtime (it uses `instanceof Node`), and other types should be present too.
for (let k in dom.impl) {
global[k] = dom.impl[k];
}
// Defer bindings in a string template literal
const defer = (strings, ...keys) => (o) => {
const result = [strings[0]];
keys.forEach((key, i) => {
result.push(o[key], strings[i+1]);
});
return result.join('');
};
// Template for the head of the HTML response
const headtmpl = defer`
<meta charset="UTF-8">
${'extrahead'}
`;
// Template for the javascript source to be sent with client responses.
const jstmpl = defer`
<script type="text/javascript">
var STATE = ${'state'};
var require = null;
var S = null;
var Surplus = null;
var isServer = false;
var modules = ${'modules'};
var loaded = {};
require = function(n) {
if (!(n in loaded)) {
loaded[n] = modules[n]();
}
return loaded[n];
};
S = require('s-js');
Surplus = require('surplus');
function init() {
var root = require("__ROOT__").body;
document.body.replaceChild(root, document.body.firstChild);
}
</script>
`;
const serve = (rootPath, getState, options) => {
options = options || {};
if (typeof options.clientJS != "boolean")
options.clientJS = true;
if (typeof options.pageRoot != "string")
options.pageRoot = "pages";
if (typeof options.compile == "boolean") {
if (options.compile) options.compile = [COMPILE_SURPLUS];
else options.compile = [];
}
else if (typeof options.compile != "object" || !(options.compile instanceof Array))
options.compile = [COMPILE_SURPLUS];
// Find a node module in rootPath
const findNodeModule = (n) => {
const base = path.join(rootPath, "node_modules", n);
let pkg = null;
if (fs.existsSync(path.join(base, "package.json"))) {
pkg = JSON.parse(fs.readFileSync(path.join(base, "package.json"), "utf8"));
if (pkg.main) return path.join(base, pkg.main);
}
if (fs.existsSync(path.join(base, "index.js"))) return path.join(base, "index.js");
else if (pkg && pkg.module) {
return path.join(base, pkg.module);
}
throw "Module not found: " + n;
};
let scriptCtx = null;
const rcache = {};
// Load a file or module in the rootPath.
const load = (n) => {
if (!(n in rcache)) {
let contents = null;
let p = null;
let usesState = false;
// Check if the file exists, relative to the root path and with a '.js' extension
if (fs.existsSync(path.join(rootPath, n) + ".js")) {
p = path.join(rootPath, n) + ".js";
contents = compile(p);
usesState = true;
}
// Check if the file exists, relative to the root path
else if (fs.existsSync(path.join(rootPath, n))) {
p = path.join(rootPath, n);
contents = compile(p);
usesState = true;
}
// Check if the module exists
else {
p = findNodeModule(n);
contents = fs.readFileSync(p, "utf8");
}
// Cache the contents of the loaded code
rcache[n] = {
code: contents
};
// Run the code from filename with the given context. If an error occurs, print out some useful information.
const runCode = (code, ctx, filename) => {
try {
vm.runInNewContext(code, ctx, { filename: filename });
}
catch (err) {
console.error("Error running compiled code for " + filename + ":");
console.error(code);
console.error(err);
throw err;
}
};
// Get the module exports from executing the code of the cache entry, given the request and state.
rcache[n].result = (req, s) => {
// Store a local cache for the request
if (!req._cache) req._cache = {};
if (!(n in req._cache)) {
const e = {};
// Create a context with module.exports and exports in the global scope
const ctx = Object.assign({ module: {exports: e}, exports: e }, global);
let deps = null;
if (!rcache[n].deps) {
deps = {};
// Create a require implementation in the global context that marks dependencies
ctx.require = (n) => {
deps[n] = true;
return load(n).result(req, s);
};
}
else {
// We already know dependencies, so require() doesn't need to mark them.
ctx.require = (n) => load(n).result(req, s);
}
// If the code is (possibly) stateful, it should always be re-executed
if (usesState) {
// Add script context for stateful/user code
Object.assign(ctx, scriptCtx(req, s));
runCode(rcache[n].code, ctx, p);
}
else {
// Non-stateful code execution results may be cached
if (!rcache[n]._cacheResult) {
runCode(rcache[n].code, ctx, p);
rcache[n]._cacheResult = ctx.module.exports;
}
ctx.module.exports = rcache[n]._cacheResult;
}
// The dependencies should be stored on first execution
// XXX: The dependency recording assumes they are static:
// the set required on first execution must be a superset
// of those required in all following executions.
if (!rcache[n].deps) {
rcache[n].deps = Object.keys(deps);
}
req._cache[n] = ctx.module.exports;
}
return req._cache[n];
};
}
return rcache[n];
};
// Use the surplus version installed in rootPath
const surplus_compiler = () => {
return load("surplus/compiler").result({},{});
};
// Create a compiler that runs the provided compile stages
const compile_steps = options.compile.map(f => f === COMPILE_SURPLUS ? surplus_compiler().compile : f);
compiler = s => {
for (const f of compile_steps) {
s = f(s);
}
return s;
};
// Recursively get all dependencies of the given entry point, and collapse
// them into a string representation of a dictionary mapping names to code.
const flattenModules = (root) => {
// Always include s-js and surplus
const used = {'s-js': true, 'surplus': true};
const remaining = [root];
while (remaining.length > 0) {
let n = remaining.pop();
used[n] = true;
remaining.push(...rcache[n].deps);
}
return '{' + Object.keys(used).map(n => {
let name = n;
if (n === root) name = "__ROOT__";
return `"${name}": (function() {
const exports = {};
const module = {exports: exports};
(function() {
${rcache[n].code}
})();
return module.exports;
}),`;
}).join('') + '}';
};
// Return script(user-code)-specific global context.
// A similar context with appropriate values is sent when clientJS is true.
scriptCtx = (req, s) => {
return {
// Always include S
S: load('s-js').result(req, s),
// Always include Surplus
Surplus: load('surplus').result(req, s),
// Expose state in a global STATE variable
STATE: s,
// Allow code to differentiate between running on the server or client.
isServer: true
};
};
const respondWithPage = (req, res, path, args) => {
args = args || [];
// Get the current state
const state = getState(req);
// Load the requested file in the cache
const r = load(path);
// Get the exports of page
let ret = r.result(req, state);
// If the page exports a function, run it to get the response object
if (typeof ret == "function") ret = ret(...args);
// Get the body of the page
let body = ret.body;
// Get the extra head content of the page, if any
let extraHead = "";
if ("head" in ret) {
extraHead = ret.head.map(s => s.outerHTML).join('');
}
// If clientJS is enabled, package and send the JS needed to run the
// page in the client.
if (options.clientJS) {
extraHead += jstmpl({
modules: flattenModules(path),
state: JSON.stringify(state)
});
}
// Body should be an HTML element; get the outerHTML
body = body.outerHTML;
//Create the head content
const head = headtmpl({
extrahead: extraHead
});
// Send the HTML response
res.end(`
<!DOCTYPE html>
<html>
<head>${head}</head>
<body ${options.clientJS ? 'onload="init()"' : ''}>${body}</body>
</html>
`);
};
// Make a middleware function to handle requests and responses.
// It will use pageRoot to find page entry points, and will route requests
// to javascript files (ending in .js) or index.js within directories.
//
// website.com/blah -> {pageRoot}/blah.js or {pageRoot}/blah/index.js
//
const respond = (req, res) => {
// Get the path of the page relative to rootPath
let relpath = path.join(options.pageRoot, req.path);
// Get the full path of the page
let fullpath = path.join(rootPath, relpath);
// If a directory is provided and index.js exists within it, use that file
if (fs.existsSync(fullpath) && fs.statSync(fullpath).isDirectory() && fs.existsSync(path.join(fullpath, "index.js"))) relpath = path.join(relpath, "index");
respondWithPage(req, res, relpath);
};
// Expose the simpler middleware that adds the respondWithPage function
respond.middleware = (req, res, next) => {
res.respondWithPage = (relpath, ...args) => respondWithPage(req, res, relpath, args);
next();
};
return respond;
};
serve.COMPILE_SURPLUS = COMPILE_SURPLUS;
module.exports = serve;