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