UNPKG

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