UNPKG

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