]>
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'); | |
0dd439a8 | 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'), | |
8b0034ee SM |
25 | outDirBase: path.resolve(__dirname, '..', 'build'), |
26 | libDirBase: path.resolve(__dirname, '..', 'lib'), | |
152c3995 | 27 | }; |
ae510306 | 28 | |
8b0034ee | 29 | const noCopyFiles = new Set([ |
adfc9d3f | 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 | ||
8b0034ee | 36 | const onlyLegacyScripts = new Set([ |
b88a92af PO |
37 | path.join(paths.vendor, 'promise.js'), |
38 | ]); | |
39 | ||
8b0034ee | 40 | const noTransformFiles = new Set([ |
adfc9d3f | 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 | ||
8b0034ee | 45 | noCopyFiles.forEach(file => noTransformFiles.add(file)); |
152c3995 | 46 | |
21633268 PO |
47 | // util.promisify requires Node.js 8.x, so we have our own |
48 | function promisify(original) { | |
8b0034ee | 49 | return function promiseWrap() { |
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. | |
8b0034ee SM |
75 | function walkDir(basePath, cb, filter) { |
76 | return readdir(basePath) | |
7b536961 | 77 | .then((files) => { |
8b0034ee | 78 | const paths = files.map(filename => path.join(basePath, filename)); |
7b536961 PO |
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 | |
8b0034ee | 90 | function transformHtml(legacyScripts, onlyLegacy) { |
ae510306 | 91 | // write out the modified vnc.html file that works with the bundle |
8b0034ee SM |
92 | const srcHtmlPath = path.resolve(__dirname, '..', 'vnc.html'); |
93 | const outHtmlPath = path.resolve(paths.outDirBase, 'vnc.html'); | |
94 | return readFile(srcHtmlPath) | |
95 | .then((contentsRaw) => { | |
96 | let contents = contentsRaw.toString(); | |
ae510306 | 97 | |
8b0034ee SM |
98 | const startMarker = '<!-- begin scripts -->\n'; |
99 | const endMarker = '<!-- end scripts -->'; | |
100 | const startInd = contents.indexOf(startMarker) + startMarker.length; | |
101 | const endInd = contents.indexOf(endMarker, startInd); | |
ae510306 | 102 | |
8b0034ee | 103 | let newScript = ''; |
4a65d50d | 104 | |
8b0034ee | 105 | if (onlyLegacy) { |
4a65d50d | 106 | // Only legacy version, so include things directly |
8b0034ee SM |
107 | for (let i = 0;i < legacyScripts.length;i++) { |
108 | newScript += ` <script src="${legacyScripts[i]}"></script>\n`; | |
7b536961 PO |
109 | } |
110 | } else { | |
0b51419c | 111 | // Otherwise include both modules and legacy fallbacks |
8b0034ee SM |
112 | newScript += ' <script type="module" crossorigin="anonymous" src="app/ui.js"></script>\n'; |
113 | for (let i = 0;i < legacyScripts.length;i++) { | |
114 | newScript += ` <script nomodule src="${legacyScripts[i]}"></script>\n`; | |
0b51419c | 115 | } |
7b536961 | 116 | } |
4a65d50d | 117 | |
8b0034ee | 118 | contents = contents.slice(0, startInd) + `${newScript}\n` + contents.slice(endInd); |
ae510306 | 119 | |
7b536961 PO |
120 | return contents; |
121 | }) | |
122 | .then((contents) => { | |
8b0034ee SM |
123 | console.log(`Writing ${outHtmlPath}`); |
124 | return writeFile(outHtmlPath, contents); | |
7b536961 | 125 | }); |
6cae7b58 | 126 | } |
ae510306 | 127 | |
8b0034ee SM |
128 | function makeLibFiles(importFormat, sourceMaps, withAppDir, onlyLegacy) { |
129 | if (!importFormat) { | |
6cae7b58 | 130 | throw new Error("you must specify an import format to generate compiled noVNC libraries"); |
8b0034ee SM |
131 | } else if (!SUPPORTED_FORMATS.has(importFormat)) { |
132 | throw new Error(`unsupported output format "${importFormat}" for import/export -- only ${Array.from(SUPPORTED_FORMATS)} are supported`); | |
6cae7b58 SR |
133 | } |
134 | ||
8b0034ee SM |
135 | // NB: we need to make a copy of babelOpts, since babel sets some defaults on it |
136 | const babelOpts = () => ({ | |
0dd439a8 PO |
137 | plugins: [], |
138 | presets: [ | |
139 | [ '@babel/preset-env', | |
140 | { targets: 'ie >= 11', | |
8b0034ee | 141 | modules: importFormat } ] |
0dd439a8 | 142 | ], |
6cae7b58 | 143 | ast: false, |
8b0034ee | 144 | sourceMaps: sourceMaps, |
6cae7b58 | 145 | }); |
6cae7b58 | 146 | |
4a65d50d | 147 | // No point in duplicate files without the app, so force only converted files |
8b0034ee SM |
148 | if (!withAppDir) { |
149 | onlyLegacy = true; | |
4a65d50d PO |
150 | } |
151 | ||
8b0034ee SM |
152 | let inPath; |
153 | let outPathBase; | |
154 | if (withAppDir) { | |
155 | outPathBase = paths.outDirBase; | |
156 | inPath = paths.main; | |
6cae7b58 | 157 | } else { |
8b0034ee | 158 | outPathBase = paths.libDirBase; |
6cae7b58 | 159 | } |
8b0034ee | 160 | const legacyPathBase = onlyLegacy ? outPathBase : path.join(outPathBase, 'legacy'); |
6cae7b58 | 161 | |
8b0034ee | 162 | fse.ensureDirSync(outPathBase); |
6cae7b58 SR |
163 | |
164 | const helpers = require('./use_require_helpers'); | |
8b0034ee | 165 | const helper = helpers[importFormat]; |
a80955ee | 166 | |
be7b4e88 | 167 | const outFiles = []; |
b88a92af | 168 | const legacyFiles = []; |
be7b4e88 | 169 | |
8b0034ee | 170 | const handleDir = (jsOnly, vendorRewrite, inPathBase, filename) => Promise.resolve() |
7b536961 | 171 | .then(() => { |
8b0034ee SM |
172 | const outPath = path.join(outPathBase, path.relative(inPathBase, filename)); |
173 | const legacyPath = path.join(legacyPathBase, path.relative(inPathBase, filename)); | |
4a65d50d | 174 | |
35068204 | 175 | if (path.extname(filename) !== '.js') { |
8b0034ee SM |
176 | if (!jsOnly) { |
177 | console.log(`Writing ${outPath}`); | |
178 | return copy(filename, outPath); | |
7b536961 PO |
179 | } |
180 | return; // skip non-javascript files | |
ae510306 | 181 | } |
ae510306 | 182 | |
8b0034ee SM |
183 | if (noTransformFiles.has(filename)) { |
184 | return ensureDir(path.dirname(outPath)) | |
6b208034 | 185 | .then(() => { |
8b0034ee SM |
186 | console.log(`Writing ${outPath}`); |
187 | return copy(filename, outPath); | |
6b208034 PO |
188 | }); |
189 | } | |
190 | ||
8b0034ee SM |
191 | if (onlyLegacyScripts.has(filename)) { |
192 | legacyFiles.push(legacyPath); | |
193 | return ensureDir(path.dirname(legacyPath)) | |
b88a92af | 194 | .then(() => { |
8b0034ee SM |
195 | console.log(`Writing ${legacyPath}`); |
196 | return copy(filename, legacyPath); | |
b88a92af PO |
197 | }); |
198 | } | |
199 | ||
7b536961 PO |
200 | return Promise.resolve() |
201 | .then(() => { | |
8b0034ee | 202 | if (onlyLegacy) { |
7b536961 PO |
203 | return; |
204 | } | |
8b0034ee | 205 | return ensureDir(path.dirname(outPath)) |
7b536961 | 206 | .then(() => { |
8b0034ee SM |
207 | console.log(`Writing ${outPath}`); |
208 | return copy(filename, outPath); | |
0ae5c54a | 209 | }); |
7b536961 | 210 | }) |
8b0034ee | 211 | .then(() => ensureDir(path.dirname(legacyPath))) |
7b536961 | 212 | .then(() => { |
8b0034ee | 213 | const opts = babelOpts(); |
7b536961 PO |
214 | if (helper && helpers.optionsOverride) { |
215 | helper.optionsOverride(opts); | |
216 | } | |
1524df89 PO |
217 | // Adjust for the fact that we move the core files relative |
218 | // to the vendor directory | |
8b0034ee | 219 | if (vendorRewrite) { |
7b536961 | 220 | opts.plugins.push(["import-redirect", |
8b0034ee | 221 | {"root": legacyPathBase, |
7b536961 PO |
222 | "redirect": { "vendor/(.+)": "./vendor/$1"}}]); |
223 | } | |
adfc9d3f | 224 | |
7b536961 PO |
225 | return babelTransformFile(filename, opts) |
226 | .then((res) => { | |
8b0034ee | 227 | console.log(`Writing ${legacyPath}`); |
7b536961 PO |
228 | const {map} = res; |
229 | let {code} = res; | |
8b0034ee | 230 | if (sourceMaps === true) { |
6cae7b58 | 231 | // append URL for external source map |
8b0034ee | 232 | code += `\n//# sourceMappingURL=${path.basename(legacyPath)}.map\n`; |
7b536961 | 233 | } |
8b0034ee SM |
234 | outFiles.push(`${legacyPath}`); |
235 | return writeFile(legacyPath, code) | |
7b536961 | 236 | .then(() => { |
8b0034ee SM |
237 | if (sourceMaps === true || sourceMaps === 'both') { |
238 | console.log(` and ${legacyPath}.map`); | |
239 | outFiles.push(`${legacyPath}.map`); | |
240 | return writeFile(`${legacyPath}.map`, JSON.stringify(map)); | |
7b536961 PO |
241 | } |
242 | }); | |
243 | }); | |
21633268 | 244 | }); |
6cae7b58 | 245 | }); |
6cae7b58 | 246 | |
21633268 | 247 | Promise.resolve() |
7b536961 | 248 | .then(() => { |
8b0034ee SM |
249 | const handler = handleDir.bind(null, true, false, inPath || paths.main); |
250 | const filter = (filename, stats) => !noCopyFiles.has(filename); | |
7b536961 PO |
251 | return walkDir(paths.vendor, handler, filter); |
252 | }) | |
253 | .then(() => { | |
8b0034ee SM |
254 | const handler = handleDir.bind(null, true, !inPath, inPath || paths.core); |
255 | const filter = (filename, stats) => !noCopyFiles.has(filename); | |
7b536961 PO |
256 | return walkDir(paths.core, handler, filter); |
257 | }) | |
258 | .then(() => { | |
8b0034ee SM |
259 | if (!withAppDir) return; |
260 | const handler = handleDir.bind(null, false, false, inPath); | |
261 | const filter = (filename, stats) => !noCopyFiles.has(filename); | |
7b536961 | 262 | return walkDir(paths.app, handler, filter); |
4a65d50d | 263 | }) |
be7b4e88 | 264 | .then(() => { |
8b0034ee | 265 | if (!withAppDir) return; |
7b536961 PO |
266 | |
267 | if (!helper || !helper.appWriter) { | |
8b0034ee | 268 | throw new Error(`Unable to generate app for the ${importFormat} format!`); |
7b536961 PO |
269 | } |
270 | ||
8b0034ee SM |
271 | const outAppPath = path.join(legacyPathBase, 'app.js'); |
272 | console.log(`Writing ${outAppPath}`); | |
273 | return helper.appWriter(outPathBase, legacyPathBase, outAppPath) | |
274 | .then((extraScripts) => { | |
275 | let legacyScripts = []; | |
b88a92af PO |
276 | |
277 | legacyFiles.forEach((file) => { | |
8b0034ee SM |
278 | let relFilePath = path.relative(outPathBase, file); |
279 | legacyScripts.push(relFilePath); | |
b88a92af PO |
280 | }); |
281 | ||
8b0034ee | 282 | legacyScripts = legacyScripts.concat(extraScripts); |
66ab0d98 | 283 | |
8b0034ee SM |
284 | let relAppPath = path.relative(outPathBase, outAppPath); |
285 | legacyScripts.push(relAppPath); | |
b88a92af | 286 | |
8b0034ee | 287 | transformHtml(legacyScripts, onlyLegacy); |
7b536961 | 288 | }) |
be7b4e88 | 289 | .then(() => { |
7b536961 PO |
290 | if (!helper.removeModules) return; |
291 | console.log(`Cleaning up temporary files...`); | |
292 | return Promise.all(outFiles.map((filepath) => { | |
293 | unlink(filepath) | |
294 | .then(() => { | |
8b0034ee SM |
295 | // Try to clean up any empty directories if |
296 | // this was the last file in there | |
297 | const rmdirR = dir => | |
7b536961 | 298 | rmdir(dir) |
8b0034ee | 299 | .then(() => rmdirR(path.dirname(dir))) |
7b536961 | 300 | .catch(() => { |
8b0034ee | 301 | // Assume the error was ENOTEMPTY and ignore it |
7b536961 | 302 | }); |
8b0034ee | 303 | return rmdirR(path.dirname(filepath)); |
7b536961 PO |
304 | }); |
305 | })); | |
be7b4e88 | 306 | }); |
7b536961 PO |
307 | }) |
308 | .catch((err) => { | |
309 | console.error(`Failure converting modules: ${err}`); | |
310 | process.exit(1); | |
be7b4e88 | 311 | }); |
651c23ec | 312 | } |
ae510306 | 313 | |
c4e5a50e | 314 | if (program.clean) { |
8b0034ee SM |
315 | console.log(`Removing ${paths.libDirBase}`); |
316 | fse.removeSync(paths.libDirBase); | |
c4e5a50e | 317 | |
8b0034ee SM |
318 | console.log(`Removing ${paths.outDirBase}`); |
319 | fse.removeSync(paths.outDirBase); | |
c4e5a50e SR |
320 | } |
321 | ||
8b0034ee | 322 | makeLibFiles(program.as, program.withSourceMaps, program.withApp, program.onlyLegacy); |