]>
Commit | Line | Data |
---|---|---|
ae510306 SR |
1 | #!/usr/bin/env node |
2 | ||
2b5f94fa JD |
3 | const path = require('path'); |
4 | const program = require('commander'); | |
5 | const fs = require('fs'); | |
6 | const fse = require('fs-extra'); | |
7 | const babel = require('babel-core'); | |
ae510306 | 8 | |
6cae7b58 | 9 | const SUPPORTED_FORMATS = new Set(['amd', 'commonjs', 'systemjs', 'umd']); |
ae510306 SR |
10 | |
11 | program | |
6cae7b58 SR |
12 | .option('--as [format]', `output files using various import formats instead of ES6 import and export. Supports ${Array.from(SUPPORTED_FORMATS)}.`) |
13 | .option('-m, --with-source-maps [type]', 'output source maps when not generating a bundled app (type may be empty for external source maps, inline for inline source maps, or both) ') | |
14 | .option('--with-app', 'process app files as well as core files') | |
4a65d50d | 15 | .option('--only-legacy', 'only output legacy files (no ES6 modules) for the app') |
c4e5a50e | 16 | .option('--clean', 'clear the lib folder before building') |
ae510306 SR |
17 | .parse(process.argv); |
18 | ||
19 | // the various important paths | |
152c3995 SR |
20 | const paths = { |
21 | main: path.resolve(__dirname, '..'), | |
22 | core: path.resolve(__dirname, '..', 'core'), | |
23 | app: path.resolve(__dirname, '..', 'app'), | |
24 | vendor: path.resolve(__dirname, '..', 'vendor'), | |
25 | out_dir_base: path.resolve(__dirname, '..', 'build'), | |
26 | lib_dir_base: path.resolve(__dirname, '..', 'lib'), | |
27 | }; | |
ae510306 | 28 | |
adfc9d3f SR |
29 | const no_copy_files = new Set([ |
30 | // skip these -- they don't belong in the processed application | |
152c3995 SR |
31 | path.join(paths.vendor, 'sinon.js'), |
32 | path.join(paths.vendor, 'browser-es-module-loader'), | |
d584c5f6 | 33 | path.join(paths.app, 'images', 'icons', 'Makefile'), |
adfc9d3f SR |
34 | ]); |
35 | ||
b88a92af PO |
36 | const only_legacy_scripts = new Set([ |
37 | path.join(paths.vendor, 'promise.js'), | |
38 | ]); | |
39 | ||
adfc9d3f SR |
40 | const no_transform_files = new Set([ |
41 | // don't transform this -- we want it imported as-is to properly catch loading errors | |
152c3995 | 42 | path.join(paths.app, 'error-handler.js'), |
adfc9d3f SR |
43 | ]); |
44 | ||
651c23ec | 45 | no_copy_files.forEach(file => no_transform_files.add(file)); |
152c3995 | 46 | |
21633268 PO |
47 | // util.promisify requires Node.js 8.x, so we have our own |
48 | function promisify(original) { | |
e7777653 | 49 | return function promise_wrap() { |
2b5f94fa | 50 | const args = Array.prototype.slice.call(arguments); |
21633268 | 51 | return new Promise((resolve, reject) => { |
651c23ec | 52 | original.apply(this, args.concat((err, value) => { |
21633268 PO |
53 | if (err) return reject(err); |
54 | resolve(value); | |
55 | })); | |
56 | }); | |
0ae5c54a | 57 | }; |
21633268 PO |
58 | } |
59 | ||
60 | const readFile = promisify(fs.readFile); | |
61 | const writeFile = promisify(fs.writeFile); | |
62 | ||
63 | const readdir = promisify(fs.readdir); | |
64 | const lstat = promisify(fs.lstat); | |
65 | ||
66 | const copy = promisify(fse.copy); | |
be7b4e88 | 67 | const unlink = promisify(fse.unlink); |
21633268 | 68 | const ensureDir = promisify(fse.ensureDir); |
be7b4e88 | 69 | const rmdir = promisify(fse.rmdir); |
21633268 PO |
70 | |
71 | const babelTransformFile = promisify(babel.transformFile); | |
72 | ||
6cae7b58 SR |
73 | // walkDir *recursively* walks directories trees, |
74 | // calling the callback for all normal files found. | |
651c23ec | 75 | function walkDir(base_path, cb, filter) { |
21633268 | 76 | return readdir(base_path) |
7b536961 PO |
77 | .then((files) => { |
78 | const paths = files.map(filename => path.join(base_path, filename)); | |
79 | return Promise.all(paths.map(filepath => lstat(filepath) | |
80 | .then((stats) => { | |
81 | if (filter !== undefined && !filter(filepath, stats)) return; | |
82 | ||
83 | if (stats.isSymbolicLink()) return; | |
84 | if (stats.isFile()) return cb(filepath); | |
85 | if (stats.isDirectory()) return walkDir(filepath, cb, filter); | |
86 | }))); | |
87 | }); | |
651c23ec | 88 | } |
ae510306 | 89 | |
2c5491e1 | 90 | function transform_html(legacy_scripts, only_legacy) { |
ae510306 | 91 | // write out the modified vnc.html file that works with the bundle |
2b5f94fa JD |
92 | const src_html_path = path.resolve(__dirname, '..', 'vnc.html'); |
93 | const out_html_path = path.resolve(paths.out_dir_base, 'vnc.html'); | |
21633268 | 94 | return readFile(src_html_path) |
7b536961 PO |
95 | .then((contents_raw) => { |
96 | let contents = contents_raw.toString(); | |
ae510306 | 97 | |
7b536961 PO |
98 | const start_marker = '<!-- begin scripts -->\n'; |
99 | const end_marker = '<!-- end scripts -->'; | |
100 | const start_ind = contents.indexOf(start_marker) + start_marker.length; | |
101 | const end_ind = contents.indexOf(end_marker, start_ind); | |
ae510306 | 102 | |
7b536961 | 103 | let new_script = ''; |
4a65d50d | 104 | |
7b536961 | 105 | if (only_legacy) { |
4a65d50d | 106 | // Only legacy version, so include things directly |
7b536961 PO |
107 | for (let i = 0;i < legacy_scripts.length;i++) { |
108 | new_script += ` <script src="${legacy_scripts[i]}"></script>\n`; | |
109 | } | |
110 | } else { | |
0b51419c | 111 | // Otherwise include both modules and legacy fallbacks |
7b536961 | 112 | new_script += ' <script type="module" crossorigin="anonymous" src="app/ui.js"></script>\n'; |
0b51419c | 113 | for (let i = 0;i < legacy_scripts.length;i++) { |
c6e37040 | 114 | new_script += ` <script nomodule src="${legacy_scripts[i]}"></script>\n`; |
0b51419c | 115 | } |
7b536961 | 116 | } |
4a65d50d | 117 | |
7b536961 | 118 | contents = contents.slice(0, start_ind) + `${new_script}\n` + contents.slice(end_ind); |
ae510306 | 119 | |
7b536961 PO |
120 | return contents; |
121 | }) | |
122 | .then((contents) => { | |
123 | console.log(`Writing ${out_html_path}`); | |
124 | return writeFile(out_html_path, contents); | |
125 | }); | |
6cae7b58 | 126 | } |
ae510306 | 127 | |
651c23ec | 128 | function make_lib_files(import_format, source_maps, with_app_dir, only_legacy) { |
6cae7b58 SR |
129 | if (!import_format) { |
130 | throw new Error("you must specify an import format to generate compiled noVNC libraries"); | |
131 | } else if (!SUPPORTED_FORMATS.has(import_format)) { | |
132 | throw new Error(`unsupported output format "${import_format}" for import/export -- only ${Array.from(SUPPORTED_FORMATS)} are supported`); | |
133 | } | |
134 | ||
135 | // NB: we need to make a copy of babel_opts, since babel sets some defaults on it | |
136 | const babel_opts = () => ({ | |
137 | plugins: [`transform-es2015-modules-${import_format}`], | |
cdb860ad | 138 | presets: ['es2015'], |
6cae7b58 SR |
139 | ast: false, |
140 | sourceMaps: source_maps, | |
141 | }); | |
6cae7b58 | 142 | |
4a65d50d PO |
143 | // No point in duplicate files without the app, so force only converted files |
144 | if (!with_app_dir) { | |
145 | only_legacy = true; | |
146 | } | |
147 | ||
2b5f94fa JD |
148 | let in_path; |
149 | let out_path_base; | |
6cae7b58 | 150 | if (with_app_dir) { |
8727f598 | 151 | out_path_base = paths.out_dir_base; |
152c3995 | 152 | in_path = paths.main; |
6cae7b58 | 153 | } else { |
8727f598 | 154 | out_path_base = paths.lib_dir_base; |
6cae7b58 | 155 | } |
4a65d50d | 156 | const legacy_path_base = only_legacy ? out_path_base : path.join(out_path_base, 'legacy'); |
6cae7b58 SR |
157 | |
158 | fse.ensureDirSync(out_path_base); | |
159 | ||
160 | const helpers = require('./use_require_helpers'); | |
161 | const helper = helpers[import_format]; | |
a80955ee | 162 | |
be7b4e88 | 163 | const outFiles = []; |
b88a92af | 164 | const legacyFiles = []; |
be7b4e88 | 165 | |
2b5f94fa | 166 | const handleDir = (js_only, vendor_rewrite, in_path_base, filename) => Promise.resolve() |
7b536961 | 167 | .then(() => { |
7b536961 PO |
168 | const out_path = path.join(out_path_base, path.relative(in_path_base, filename)); |
169 | const legacy_path = path.join(legacy_path_base, path.relative(in_path_base, filename)); | |
4a65d50d | 170 | |
35068204 | 171 | if (path.extname(filename) !== '.js') { |
7b536961 PO |
172 | if (!js_only) { |
173 | console.log(`Writing ${out_path}`); | |
174 | return copy(filename, out_path); | |
175 | } | |
176 | return; // skip non-javascript files | |
ae510306 | 177 | } |
ae510306 | 178 | |
6b208034 PO |
179 | if (no_transform_files.has(filename)) { |
180 | return ensureDir(path.dirname(out_path)) | |
181 | .then(() => { | |
182 | console.log(`Writing ${out_path}`); | |
183 | return copy(filename, out_path); | |
184 | }); | |
185 | } | |
186 | ||
b88a92af PO |
187 | if (only_legacy_scripts.has(filename)) { |
188 | legacyFiles.push(legacy_path); | |
189 | return ensureDir(path.dirname(legacy_path)) | |
190 | .then(() => { | |
191 | console.log(`Writing ${legacy_path}`); | |
192 | return copy(filename, legacy_path); | |
193 | }); | |
194 | } | |
195 | ||
7b536961 PO |
196 | return Promise.resolve() |
197 | .then(() => { | |
6b208034 | 198 | if (only_legacy) { |
7b536961 PO |
199 | return; |
200 | } | |
201 | return ensureDir(path.dirname(out_path)) | |
202 | .then(() => { | |
203 | console.log(`Writing ${out_path}`); | |
204 | return copy(filename, out_path); | |
0ae5c54a | 205 | }); |
7b536961 PO |
206 | }) |
207 | .then(() => ensureDir(path.dirname(legacy_path))) | |
208 | .then(() => { | |
7b536961 PO |
209 | const opts = babel_opts(); |
210 | if (helper && helpers.optionsOverride) { | |
211 | helper.optionsOverride(opts); | |
212 | } | |
1524df89 PO |
213 | // Adjust for the fact that we move the core files relative |
214 | // to the vendor directory | |
7b536961 PO |
215 | if (vendor_rewrite) { |
216 | opts.plugins.push(["import-redirect", | |
217 | {"root": legacy_path_base, | |
218 | "redirect": { "vendor/(.+)": "./vendor/$1"}}]); | |
219 | } | |
adfc9d3f | 220 | |
7b536961 PO |
221 | return babelTransformFile(filename, opts) |
222 | .then((res) => { | |
223 | console.log(`Writing ${legacy_path}`); | |
224 | const {map} = res; | |
225 | let {code} = res; | |
226 | if (source_maps === true) { | |
6cae7b58 | 227 | // append URL for external source map |
7b536961 PO |
228 | code += `\n//# sourceMappingURL=${path.basename(legacy_path)}.map\n`; |
229 | } | |
230 | outFiles.push(`${legacy_path}`); | |
231 | return writeFile(legacy_path, code) | |
232 | .then(() => { | |
233 | if (source_maps === true || source_maps === 'both') { | |
234 | console.log(` and ${legacy_path}.map`); | |
235 | outFiles.push(`${legacy_path}.map`); | |
236 | return writeFile(`${legacy_path}.map`, JSON.stringify(map)); | |
237 | } | |
238 | }); | |
239 | }); | |
21633268 | 240 | }); |
6cae7b58 | 241 | }); |
6cae7b58 | 242 | |
21633268 | 243 | Promise.resolve() |
7b536961 PO |
244 | .then(() => { |
245 | const handler = handleDir.bind(null, true, false, in_path || paths.main); | |
246 | const filter = (filename, stats) => !no_copy_files.has(filename); | |
247 | return walkDir(paths.vendor, handler, filter); | |
248 | }) | |
249 | .then(() => { | |
250 | const handler = handleDir.bind(null, true, !in_path, in_path || paths.core); | |
251 | const filter = (filename, stats) => !no_copy_files.has(filename); | |
252 | return walkDir(paths.core, handler, filter); | |
253 | }) | |
254 | .then(() => { | |
255 | if (!with_app_dir) return; | |
256 | const handler = handleDir.bind(null, false, false, in_path); | |
257 | const filter = (filename, stats) => !no_copy_files.has(filename); | |
258 | return walkDir(paths.app, handler, filter); | |
4a65d50d | 259 | }) |
be7b4e88 | 260 | .then(() => { |
7b536961 PO |
261 | if (!with_app_dir) return; |
262 | ||
263 | if (!helper || !helper.appWriter) { | |
264 | throw new Error(`Unable to generate app for the ${import_format} format!`); | |
265 | } | |
266 | ||
267 | const out_app_path = path.join(legacy_path_base, 'app.js'); | |
268 | console.log(`Writing ${out_app_path}`); | |
269 | return helper.appWriter(out_path_base, legacy_path_base, out_app_path) | |
270 | .then((extra_scripts) => { | |
b88a92af PO |
271 | let legacy_scripts = extra_scripts; |
272 | ||
273 | legacyFiles.forEach((file) => { | |
274 | let rel_file_path = path.relative(out_path_base, file); | |
275 | legacy_scripts.push(rel_file_path); | |
276 | }); | |
277 | ||
278 | let rel_app_path = path.relative(out_path_base, out_app_path); | |
279 | legacy_scripts.push(rel_app_path); | |
280 | ||
7b536961 PO |
281 | transform_html(legacy_scripts, only_legacy); |
282 | }) | |
be7b4e88 | 283 | .then(() => { |
7b536961 PO |
284 | if (!helper.removeModules) return; |
285 | console.log(`Cleaning up temporary files...`); | |
286 | return Promise.all(outFiles.map((filepath) => { | |
287 | unlink(filepath) | |
288 | .then(() => { | |
be7b4e88 PO |
289 | // Try to clean up any empty directories if this |
290 | // was the last file in there | |
7b536961 PO |
291 | const rmdir_r = dir => |
292 | rmdir(dir) | |
293 | .then(() => rmdir_r(path.dirname(dir))) | |
294 | .catch(() => { | |
be7b4e88 | 295 | // Assume the error was ENOTEMPTY and ignore it |
7b536961 PO |
296 | }); |
297 | return rmdir_r(path.dirname(filepath)); | |
298 | }); | |
299 | })); | |
be7b4e88 | 300 | }); |
7b536961 PO |
301 | }) |
302 | .catch((err) => { | |
303 | console.error(`Failure converting modules: ${err}`); | |
304 | process.exit(1); | |
be7b4e88 | 305 | }); |
651c23ec | 306 | } |
ae510306 | 307 | |
c4e5a50e SR |
308 | if (program.clean) { |
309 | console.log(`Removing ${paths.lib_dir_base}`); | |
310 | fse.removeSync(paths.lib_dir_base); | |
311 | ||
312 | console.log(`Removing ${paths.out_dir_base}`); | |
313 | fse.removeSync(paths.out_dir_base); | |
314 | } | |
315 | ||
4a65d50d | 316 | make_lib_files(program.as, program.withSourceMaps, program.withApp, program.onlyLegacy); |