UNPKG

18.1 kBJavaScriptView Raw
1#!/usr/bin/env node
2const fs = require('fs-extra');
3const program = require('commander');
4const http = require('http');
5const execSync = require('child_process').execSync;
6const execAsync = require('child_process').exec;
7const tsc = require('typescript');
8const path = require('path');
9const WebSocket = require('ws');
10const chokidar = require('chokidar');
11const resolveBareSpecifiers = require('./babel-plugin-transform-resolve-bare-specifiers.js');
12const resolveImportPathExtensions = require('./babel-plugin-transform-resolve-import-path-extensions.js');
13const babel = require('babel-core');
14const wast2wasm = require('wast2wasm');
15
16program
17 .version('0.25.0')
18 .option('-p, --port [port]', 'Specify the server\'s port')
19 .option('-w, --watch-files', 'Watch files in current directory and reload browser on changes')
20 .option('--ts-warning', 'Report TypeScript errors in the browser console as warnings')
21 .option('--ts-error', 'Report TypeScript errors in the browser console as errors')
22 .option('--build-static', 'Create a static build of the current working directory. The output will be in a directory called dist in the current working directory')
23 .option('--target [target]', 'The ECMAScript version to compile to; if omitted, defaults to ES5. Any targets supported by the TypeScript compiler are supported here (ES3, ES5, ES6/ES2015, ES2016, ES2017, ESNext)')
24 .option('--disable-spa', 'Disable the SPA redirect to index.html')
25 .option('--exclude-dirs', 'A space-separated list of directories to exclude from the static build') //TODO I know this is wrong, I need to figure out how to do variadic arguments
26 .parse(process.argv);
27// end side-causes
28// start pure operations, generate the data
29const buildStatic = program.buildStatic;
30const watchFiles = program.watchFiles || true; //TODO I think it should default to watching, not the other way around
31// const spaRoot = program.spaRoot || 'index.html';
32const nodePort = +(program.port || 5000);
33const webSocketPort = nodePort + 1;
34const tsWarning = program.tsWarning;
35const tsError = program.tsError;
36const target = program.target || 'ES2015';
37const nodeHttpServer = createNodeServer(http, nodePort, webSocketPort, watchFiles, tsWarning, tsError, target);
38const webSocketServer = createWebSocketServer(webSocketPort, watchFiles);
39const excludeDirs = program.excludeDirs;
40const excludeDirsRegex = `^${excludeDirs ? program.args.join('|') : 'NO_EXCLUDE_DIRS'}`;
41const disableSpa = program.disableSpa;
42let clients = {};
43let compiledFiles = {};
44//end pure operations
45// start side-effects, change the world
46
47nodeHttpServer.listen(nodePort);
48console.log(`Zwitterion listening on port ${nodePort}`);
49process.send && process.send('ZWITTERION_LISTENING');
50
51if (buildStatic) {
52 const asyncExec = execAsync(`
53 echo "Copy current working directory to ZWITTERION_TEMP directory"
54
55 originalDirectory=$(pwd)
56
57 rm -rf dist
58 cd ..
59 rm -rf ZWITTERION_TEMP
60 cp -r $originalDirectory ZWITTERION_TEMP
61 cd ZWITTERION_TEMP
62
63 echo "Download and save all .html files from Zwitterion"
64
65 shopt -s globstar
66 for file in **/*.html; do
67 if [[ ! $file =~ ${excludeDirsRegex} ]]
68 then
69 wget -q -x -nH "http://localhost:${nodePort}/$file"
70 fi
71 done
72
73 echo "Download and save all .js files from Zwitterion"
74
75 shopt -s globstar
76 for file in **/*.js; do
77 if [[ ! $file =~ ${excludeDirsRegex} ]]
78 then
79 wget -q -x -nH "http://localhost:${nodePort}/$\{file%.*\}.js"
80 fi
81 done
82
83 echo "Download and save all .ts files from Zwitterion"
84
85 shopt -s globstar
86 for file in **/*.ts; do
87 if [[ ! $file =~ ${excludeDirsRegex} ]]
88 then
89 wget -q -x -nH "http://localhost:${nodePort}/$\{file%.*\}.ts"
90 fi
91 done
92
93 echo "Download and save all .tsx files from Zwitterion"
94
95 shopt -s globstar
96 for file in **/*.tsx; do
97 if [[ ! $file =~ ${excludeDirsRegex} ]]
98 then
99 wget -q -x -nH "http://localhost:${nodePort}/$\{file%.*\}.tsx"
100 fi
101 done
102
103 echo "Download and save all .jsx files from Zwitterion"
104
105 shopt -s globstar
106 for file in **/*.jsx; do
107 if [[ ! $file =~ ${excludeDirsRegex} ]]
108 then
109 wget -q -x -nH "http://localhost:${nodePort}/$\{file%.*\}.jsx"
110 fi
111 done
112
113 #echo "Download and save all .c files from Zwitterion"
114
115 #shopt -s globstar
116 #for file in **/*.c; do
117 # if [[ ! $file =~ ${excludeDirsRegex} ]]
118 # then
119 # wget -q -x -nH "http://localhost:${nodePort}/$\{file%.*\}.c"
120 # fi
121 #done
122
123 #echo "Download and save all .cc files from Zwitterion"
124
125 #shopt -s globstar
126 #for file in **/*.cc; do
127 # if [[ ! $file =~ ${excludeDirsRegex} ]]
128 # then
129 # wget -q -x -nH "http://localhost:${nodePort}/$\{file%.*\}.cc"
130 # fi
131 #done
132
133 #echo "Download and save all .cpp files from Zwitterion"
134
135 #shopt -s globstar
136 #for file in **/*.cpp; do
137 # if [[ ! $file =~ ${excludeDirsRegex} ]]
138 # then
139 # wget -q -x -nH "http://localhost:${nodePort}/$\{file%.*\}.cpp"
140 # fi
141 #done
142
143 #echo "Download and save all .wasm files from Zwitterion"
144
145 #shopt -s globstar
146 #for file in **/*.wasm; do
147 # if [[ ! $file =~ ${excludeDirsRegex} ]]
148 # then
149 # wget -q -x -nH "http://localhost:${nodePort}/$\{file%.*\}.wasm"
150 # fi
151 #done
152
153 echo "Copy ZWITTERION_TEMP to dist directory in the project root directory"
154
155 cd ..
156 cp -r ZWITTERION_TEMP $originalDirectory/dist
157 rm -rf ZWITTERION_TEMP
158
159 echo "Static build finished"
160 `, {
161 shell: '/bin/bash'
162 }, () => {
163 process.exit();
164 });
165 asyncExec.stdout.pipe(process.stdout);
166 asyncExec.stderr.pipe(process.stderr);
167 return;
168}
169
170// end side-effects
171function createNodeServer(http, nodePort, webSocketPort, watchFiles, tsWarning, tsError, target) {
172 return http.createServer(async (req, res) => {
173 try {
174 const fileExtension = req.url.slice(req.url.lastIndexOf('.') + 1);
175 switch (fileExtension) {
176 case '/': {
177 const indexFileContents = (await fs.readFile(`./index.html`)).toString();
178 const modifiedIndexFileContents = modifyHTML(indexFileContents, 'index.html', watchFiles, webSocketPort);
179 watchFile(`./index.html`, watchFiles);
180 res.end(modifiedIndexFileContents);
181 return;
182 }
183 case 'js': {
184 await handleScriptExtension(req, res, fileExtension);
185 return;
186 }
187 case 'mjs': {
188 await handleScriptExtension(req, res, fileExtension);
189 return;
190 }
191 case 'ts': {
192 await handleScriptExtension(req, res, fileExtension);
193 return;
194 }
195 case 'tsx': {
196 await handleScriptExtension(req, res, fileExtension);
197 return;
198 }
199 case 'jsx': {
200 await handleScriptExtension(req, res, fileExtension);
201 return;
202 }
203 case 'wast': {
204 await handleWASTExtension(req, res, fileExtension);
205 return;
206 }
207 case 'wasm': {
208 await handleWASMExtension(req, res, fileExtension);
209 return;
210 }
211 default: {
212 await handleGenericFile(req, res, fileExtension);
213 return;
214 }
215 }
216 }
217 catch(error) {
218 console.log(error);
219 }
220 });
221}
222
223async function handleScriptExtension(req, res, fileExtension) {
224 const nodeFilePath = `.${req.url}`;
225
226 // check if the file is in the cache
227 if (compiledFiles[nodeFilePath]) {
228 res.setHeader('Content-Type', 'application/javascript');
229 res.end(compiledFiles[nodeFilePath]);
230 return;
231 }
232
233 // the file is not in the cache
234 // watch the file if necessary
235 // compile the file and return the compiled source
236 if (await fs.exists(nodeFilePath)) {
237 watchFile(nodeFilePath, watchFiles);
238 const source = (await fs.readFile(nodeFilePath)).toString();
239 const compiledToJS = compileToJs(source, target, fileExtension === '.jsx' || fileExtension === '.tsx');
240 const transformedSpecifiers = transformSpecifiers(compiledToJS, nodeFilePath);
241 const globalsAdded = addGlobals(transformedSpecifiers);
242 compiledFiles[nodeFilePath] = globalsAdded;
243 res.setHeader('Content-Type', 'application/javascript');
244 res.end(globalsAdded);
245 return;
246 }
247
248 //TODO this code is repeated multiple times
249 // if SPA is enabled, return the contents to index.html
250 // if SPA is not enabled, return a 404 error
251 if (!disableSpa) {
252 const indexFileContents = (await fs.readFile(`./index.html`)).toString();
253 const modifiedIndexFileContents = modifyHTML(indexFileContents, 'index.html', watchFiles, webSocketPort);
254 const directoryPath = req.url.slice(0, req.url.lastIndexOf('/')) || '/';
255 res.end(modifyHTML(modifiedIndexFileContents, directoryPath, watchFiles, webSocketPort));
256 return;
257 }
258 else {
259 res.statusCode = 404;
260 res.end();
261 return;
262 }
263}
264
265//TODO this code is very similar to handleScriptExtension
266async function handleGenericFile(req, res, fileExtension) {
267 const nodeFilePath = `.${req.url}`;
268
269 // check if the file is in the cache
270 if (compiledFiles[nodeFilePath]) {
271 res.end(compiledFiles[nodeFilePath]);
272 return;
273 }
274
275 // the file is not in the cache
276 // watch the file if necessary
277 // compile the file and return the compiled source
278 if (await fs.exists(nodeFilePath)) {
279 watchFile(nodeFilePath, watchFiles);
280 const source = await fs.readFile(nodeFilePath);
281 compiledFiles[nodeFilePath] = source;
282 res.end(source);
283 return;
284 }
285
286 //TODO this code is repeated multiple times
287 // if SPA is enabled, return the contents to index.html
288 // if SPA is not enabled, return a 404 error
289 if (!disableSpa) {
290 const indexFileContents = (await fs.readFile(`./index.html`)).toString();
291 const modifiedIndexFileContents = modifyHTML(indexFileContents, 'index.html', watchFiles, webSocketPort);
292 const directoryPath = req.url.slice(0, req.url.lastIndexOf('/')) || '/';
293 res.end(modifyHTML(modifiedIndexFileContents, directoryPath, watchFiles, webSocketPort));
294 return;
295 }
296 else {
297 res.statusCode = 404;
298 res.end();
299 return;
300 }
301}
302
303async function handleWASMExtension(req, res, fileExtension) {
304 const nodeFilePath = `.${req.url}`;
305
306 // check if the file is in the cache
307 if (compiledFiles[nodeFilePath]) {
308 res.setHeader('Content-Type', 'application/javascript');
309 res.end(compiledFiles[nodeFilePath]);
310 return;
311 }
312
313 // the file is not in the cache
314 // watch the file if necessary
315 // wrap the file and return the wrapped source
316 if (await fs.exists(nodeFilePath)) {
317 watchFile(nodeFilePath, watchFiles);
318 const sourceBuffer = await fs.readFile(nodeFilePath);
319 const wrappedInJS = wrapWASMInJS(sourceBuffer);
320 compiledFiles[nodeFilePath] = wrappedInJS;
321 res.setHeader('Content-Type', 'application/javascript');
322 res.end(wrappedInJS);
323 return;
324 }
325
326 //TODO this code is repeated multiple times
327 // if SPA is enabled, return the contents to index.html
328 // if SPA is not enabled, return a 404 error
329 if (!disableSpa) {
330 const indexFileContents = (await fs.readFile(`./index.html`)).toString();
331 const modifiedIndexFileContents = modifyHTML(indexFileContents, 'index.html', watchFiles, webSocketPort);
332 const directoryPath = req.url.slice(0, req.url.lastIndexOf('/')) || '/';
333 res.end(modifyHTML(modifiedIndexFileContents, directoryPath, watchFiles, webSocketPort));
334 return;
335 }
336 else {
337 res.statusCode = 404;
338 res.end();
339 return;
340 }
341}
342
343async function handleWASTExtension(req, res, fileExtension) {
344 const nodeFilePath = `.${req.url}`;
345
346 // check if the file is in the cache
347 if (compiledFiles[nodeFilePath]) {
348 res.setHeader('Content-Type', 'application/javascript');
349 res.end(compiledFiles[nodeFilePath]);
350 return;
351 }
352
353 // the file is not in the cache
354 // watch the file if necessary
355 // wrap the file and return the wrapped source
356 if (await fs.exists(nodeFilePath)) {
357 watchFile(nodeFilePath, watchFiles);
358 const source = (await fs.readFile(nodeFilePath)).toString();
359 const compiledToWASMBuffer = await compileToWASMBuffer(source);
360 const wrappedInJS = wrapWASMInJS(compiledToWASMBuffer);
361 compiledFiles[nodeFilePath] = wrappedInJS;
362 res.setHeader('Content-Type', 'application/javascript');
363 res.end(wrappedInJS);
364 return;
365 }
366
367 //TODO this code is repeated multiple times
368 // if SPA is enabled, return the contents to index.html
369 // if SPA is not enabled, return a 404 error
370 if (!disableSpa) {
371 const indexFileContents = (await fs.readFile(`./index.html`)).toString();
372 const modifiedIndexFileContents = modifyHTML(indexFileContents, 'index.html', watchFiles, webSocketPort);
373 const directoryPath = req.url.slice(0, req.url.lastIndexOf('/')) || '/';
374 res.end(modifyHTML(modifiedIndexFileContents, directoryPath, watchFiles, webSocketPort));
375 return;
376 }
377 else {
378 res.statusCode = 404;
379 res.end();
380 return;
381 }
382}
383
384async function compileToWASMBuffer(source) {
385 return (await wast2wasm(source)).buffer;
386}
387
388function wrapWASMInJS(sourceBuffer) {
389 return `
390 //TODO perhaps there is a better way to get the ArrayBuffer that wasm needs...but for now this works
391 const base64EncodedByteCode = Uint8Array.from('${Uint8Array.from(sourceBuffer)}'.split(','));
392 const wasmModule = new WebAssembly.Module(base64EncodedByteCode);
393 const wasmInstance = new WebAssembly.Instance(wasmModule, {});
394
395 export default wasmInstance.exports;
396 `;
397}
398
399function getTypeScriptErrorsString(filePath, tsWarning, tsError) {
400 if (tsWarning || tsError) {
401 const tsProgram = tsc.createProgram([
402 filePath
403 ], {});
404 const semanticDiagnostics = tsProgram.getSemanticDiagnostics();
405 return semanticDiagnostics.reduce((result, diagnostic) => {
406 return `${result}\nconsole.${tsWarning ? 'warn' : 'error'}("TypeScript: ${diagnostic.file ? diagnostic.file.fileName : 'no file name provided'}: ${diagnostic.messageText}")`;
407 }, '');
408 }
409 else {
410 return '';
411 }
412}
413
414function watchFile(filePath, watchFiles) {
415 if (watchFiles) {
416 chokidar.watch(filePath).on('change', () => {
417 compiledFiles[filePath] = null;
418 Object.values(clients).forEach((client) => {
419 try {
420 client.send('RELOAD_MESSAGE');
421 }
422 catch(error) {
423 //TODO something should be done about this. What's happening I believe is that if two files are changed in a very short period of time, one file will start the browser reloading, and the other file will try to send a message to the browser while it is reloading, and thus the websocket connection will not be established with the browser. This is a temporary solution
424 console.log(error);
425 }
426 });
427 });
428 }
429}
430
431function modifyHTML(originalText, directoryPath, watchFiles, webSocketPort) {
432 //TODO we should probably put the web socket creation in the html files as well
433 return originalText;
434}
435
436function compileToJs(source, target, jsx) {
437 const transpileOutput = tsc.transpileModule(source, {
438 compilerOptions: {
439 module: 'es2015',
440 target,
441 jsx
442 }
443 });
444 return transpileOutput.outputText;
445}
446
447function transformSpecifiers(source, filePath) {
448 return babel.transform(source, {
449 babelrc: false,
450 plugins: [resolveBareSpecifiers(filePath, false), resolveImportPathExtensions(filePath)]
451 }).code;
452}
453
454function addGlobals(source) {
455 return `
456 var process = window.process;
457 if (!window.ZWITTERION_SOCKET) {
458 window.ZWITTERION_SOCKET = new WebSocket('ws://localhost:${webSocketPort}');
459 window.ZWITTERION_SOCKET.addEventListener('message', (message) => {
460 window.location.reload();
461 });
462 }
463 ${source}
464 `;
465}
466
467function createWebSocketServer(webSocketPort, watchFiles) {
468 if (watchFiles) {
469 const webSocketServer = new WebSocket.Server({
470 port: webSocketPort
471 });
472 webSocketServer.on('connection', (client, request) => {
473 clients[request.connection.remoteAddress] = client;
474 client.on('error', (error) => {
475 console.log('web socket client error', error);
476 });
477 });
478 return webSocketServer;
479 }
480 else {
481 return null;
482 }
483}
484
485async function compileToWasmJs(filePath) {
486 const filename = filePath.substring(filePath.lastIndexOf('/') + 1, filePath.lastIndexOf('.'));
487 execSync(`cd emsdk && source ./emsdk_env.sh --build=Release && cd .. && emcc ${filePath} -s WASM=1 -o ${filename}.wasm`, {shell: '/bin/bash'});
488 const compiledText = fs.readFileSync(`${filename}.js`).toString();
489 return compiledText;
490}