1 | #!/usr/bin/env node
|
2 | const fs = require('fs-extra');
|
3 | const program = require('commander');
|
4 | const http = require('http');
|
5 | const execSync = require('child_process').execSync;
|
6 | const execAsync = require('child_process').exec;
|
7 | const tsc = require('typescript');
|
8 | const path = require('path');
|
9 | const WebSocket = require('ws');
|
10 | const chokidar = require('chokidar');
|
11 | const resolveBareSpecifiers = require('./babel-plugin-transform-resolve-bare-specifiers.js');
|
12 | const resolveImportPathExtensions = require('./babel-plugin-transform-resolve-import-path-extensions.js');
|
13 | const babel = require('babel-core');
|
14 | const wast2wasm = require('wast2wasm');
|
15 |
|
16 | program
|
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')
|
26 | .option('--include [include]', 'A comma-separated list of paths to include in the static build')
|
27 | .parse(process.argv);
|
28 |
|
29 |
|
30 | const buildStatic = program.buildStatic;
|
31 | const watchFiles = program.watchFiles || true;
|
32 |
|
33 | const nodePort = +(program.port || 5000);
|
34 | const webSocketPort = nodePort + 1;
|
35 | const tsWarning = program.tsWarning;
|
36 | const tsError = program.tsError;
|
37 | const target = program.target || 'ES2015';
|
38 | const nodeHttpServer = createNodeServer(http, nodePort, webSocketPort, watchFiles, tsWarning, tsError, target);
|
39 | const webSocketServer = createWebSocketServer(webSocketPort, watchFiles);
|
40 | const exclude = program.exclude;
|
41 | const excludeRegex = `${program.exclude ? program.exclude.split(',').join('*|') : 'NO_EXCLUDE'}*`;
|
42 | const include = program.include;
|
43 | const includeRegex = `${program.include ? program.include.split(',').join('*|') : 'NO_INCLUDE'}*`;
|
44 | const disableSpa = program.disableSpa;
|
45 | let clients = {};
|
46 | let compiledFiles = {};
|
47 |
|
48 |
|
49 |
|
50 | nodeHttpServer.listen(nodePort);
|
51 | console.log(`Zwitterion listening on port ${nodePort}`);
|
52 | process.send && process.send('ZWITTERION_LISTENING');
|
53 |
|
54 | if (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 |
|
179 | function 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 |
|
231 | async function handleScriptExtension(req, res, fileExtension) {
|
232 | const nodeFilePath = `.${req.url}`;
|
233 |
|
234 |
|
235 | if (compiledFiles[nodeFilePath]) {
|
236 | res.setHeader('Content-Type', 'application/javascript');
|
237 | res.end(compiledFiles[nodeFilePath]);
|
238 | return;
|
239 | }
|
240 |
|
241 |
|
242 |
|
243 |
|
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 |
|
257 |
|
258 |
|
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 |
|
274 | async function handleGenericFile(req, res, fileExtension) {
|
275 | const nodeFilePath = `.${req.url}`;
|
276 |
|
277 |
|
278 | if (compiledFiles[nodeFilePath]) {
|
279 | res.end(compiledFiles[nodeFilePath]);
|
280 | return;
|
281 | }
|
282 |
|
283 |
|
284 |
|
285 |
|
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 |
|
295 |
|
296 |
|
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 |
|
311 | async function handleWASMExtension(req, res, fileExtension) {
|
312 | const nodeFilePath = `.${req.url}`;
|
313 |
|
314 |
|
315 | if (compiledFiles[nodeFilePath]) {
|
316 | res.setHeader('Content-Type', 'application/javascript');
|
317 | res.end(compiledFiles[nodeFilePath]);
|
318 | return;
|
319 | }
|
320 |
|
321 |
|
322 |
|
323 |
|
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 |
|
335 |
|
336 |
|
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 |
|
351 | async function handleWASTExtension(req, res, fileExtension) {
|
352 | const nodeFilePath = `.${req.url}`;
|
353 |
|
354 |
|
355 | if (compiledFiles[nodeFilePath]) {
|
356 | res.setHeader('Content-Type', 'application/javascript');
|
357 | res.end(compiledFiles[nodeFilePath]);
|
358 | return;
|
359 | }
|
360 |
|
361 |
|
362 |
|
363 |
|
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 |
|
376 |
|
377 |
|
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 |
|
392 | async function compileToWASMBuffer(source) {
|
393 | return (await wast2wasm(source)).buffer;
|
394 | }
|
395 |
|
396 | function 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 |
|
407 | function 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 |
|
422 | function 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 |
|
432 | console.log(error);
|
433 | }
|
434 | });
|
435 | });
|
436 | }
|
437 | }
|
438 |
|
439 | function modifyHTML(originalText, directoryPath, watchFiles, webSocketPort) {
|
440 |
|
441 | return originalText;
|
442 | }
|
443 |
|
444 | function 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 |
|
455 | function 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 |
|
462 | function 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 |
|
475 | function 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 |
|
493 | async 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 | }
|