mirror_novnc/utils/use_require.js
Pierre Ossman 4a65d50d0c Only use converted modules as legacy fallback for app
Several of the major browsers now natively support modules, so we
only need the converted modules to handle older browsers. Make sure
it's only used when necessary.
2018-02-28 13:28:07 +01:00

315 lines
12 KiB
JavaScript
Executable File

#!/usr/bin/env node
var path = require('path');
var program = require('commander');
var fs = require('fs');
var fse = require('fs-extra');
var babel = require('babel-core');
const SUPPORTED_FORMATS = new Set(['amd', 'commonjs', 'systemjs', 'umd']);
program
.option('--as [format]', `output files using various import formats instead of ES6 import and export. Supports ${Array.from(SUPPORTED_FORMATS)}.`)
.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) ')
.option('--with-app', 'process app files as well as core files')
.option('--only-legacy', 'only output legacy files (no ES6 modules) for the app')
.option('--clean', 'clear the lib folder before building')
.parse(process.argv);
// the various important paths
const paths = {
main: path.resolve(__dirname, '..'),
core: path.resolve(__dirname, '..', 'core'),
app: path.resolve(__dirname, '..', 'app'),
vendor: path.resolve(__dirname, '..', 'vendor'),
out_dir_base: path.resolve(__dirname, '..', 'build'),
lib_dir_base: path.resolve(__dirname, '..', 'lib'),
};
const no_copy_files = new Set([
// skip these -- they don't belong in the processed application
path.join(paths.vendor, 'sinon.js'),
path.join(paths.vendor, 'browser-es-module-loader'),
path.join(paths.vendor, 'promise.js'),
path.join(paths.app, 'images', 'icons', 'Makefile'),
]);
const no_transform_files = new Set([
// don't transform this -- we want it imported as-is to properly catch loading errors
path.join(paths.app, 'error-handler.js'),
]);
no_copy_files.forEach((file) => no_transform_files.add(file));
// util.promisify requires Node.js 8.x, so we have our own
function promisify(original) {
return function () {
let obj = this;
let args = Array.prototype.slice.call(arguments);
return new Promise((resolve, reject) => {
original.apply(obj, args.concat((err, value) => {
if (err) return reject(err);
resolve(value);
}));
});
}
}
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
const readdir = promisify(fs.readdir);
const lstat = promisify(fs.lstat);
const copy = promisify(fse.copy);
const unlink = promisify(fse.unlink);
const ensureDir = promisify(fse.ensureDir);
const rmdir = promisify(fse.rmdir);
const babelTransformFile = promisify(babel.transformFile);
// walkDir *recursively* walks directories trees,
// calling the callback for all normal files found.
var walkDir = function (base_path, cb, filter) {
return readdir(base_path)
.then(files => {
let paths = files.map(filename => path.join(base_path, filename));
return Promise.all(paths.map((filepath) => {
return lstat(filepath)
.then(stats => {
if (filter !== undefined && !filter(filepath, stats)) return;
if (stats.isSymbolicLink()) return;
if (stats.isFile()) return cb(filepath);
if (stats.isDirectory()) return walkDir(filepath, cb, filter);
});
}));
});
};
var transform_html = function (legacy_scripts, only_legacy) {
// write out the modified vnc.html file that works with the bundle
var src_html_path = path.resolve(__dirname, '..', 'vnc.html');
var out_html_path = path.resolve(paths.out_dir_base, 'vnc.html');
return readFile(src_html_path)
.then(contents_raw => {
var contents = contents_raw.toString();
var start_marker = '<!-- begin scripts -->\n';
var end_marker = '<!-- end scripts -->';
var start_ind = contents.indexOf(start_marker) + start_marker.length;
var end_ind = contents.indexOf(end_marker, start_ind);
new_script = '';
if (only_legacy) {
// Only legacy version, so include things directly
for (let i = 0;i < legacy_scripts.length;i++) {
new_script += ` <script src="${legacy_scripts[i]}"></script>\n`;
}
} else {
// Otherwise detect if it's a modern browser and select
// variant accordingly
new_script += `\
<script type="module">\n\
window._noVNC_has_module_support = true;\n\
</script>\n\
<script>\n\
window.addEventListener("load", function() {\n\
if (window._noVNC_has_module_support) return;\n\
let legacy_scripts = ${JSON.stringify(legacy_scripts)};\n\
for (let i = 0;i < legacy_scripts.length;i++) {\n\
let script = document.createElement("script");\n\
script.src = legacy_scripts[i];\n\
script.async = false;\n\
document.head.appendChild(script);\n\
}\n\
});\n\
</script>\n`;
// Original, ES6 modules
new_script += ' <script type="module" crossorigin="anonymous" src="app/ui.js"></script>\n';
}
contents = contents.slice(0, start_ind) + `${new_script}\n` + contents.slice(end_ind);
return contents;
})
.then((contents) => {
console.log(`Writing ${out_html_path}`);
return writeFile(out_html_path, contents);
});
}
var make_lib_files = function (import_format, source_maps, with_app_dir, only_legacy) {
if (!import_format) {
throw new Error("you must specify an import format to generate compiled noVNC libraries");
} else if (!SUPPORTED_FORMATS.has(import_format)) {
throw new Error(`unsupported output format "${import_format}" for import/export -- only ${Array.from(SUPPORTED_FORMATS)} are supported`);
}
// NB: we need to make a copy of babel_opts, since babel sets some defaults on it
const babel_opts = () => ({
plugins: [`transform-es2015-modules-${import_format}`],
ast: false,
sourceMaps: source_maps,
});
// No point in duplicate files without the app, so force only converted files
if (!with_app_dir) {
only_legacy = true;
}
var in_path;
if (with_app_dir) {
var out_path_base = paths.out_dir_base;
in_path = paths.main;
} else {
var out_path_base = paths.lib_dir_base;
}
const legacy_path_base = only_legacy ? out_path_base : path.join(out_path_base, 'legacy');
fse.ensureDirSync(out_path_base);
const helpers = require('./use_require_helpers');
const helper = helpers[import_format];
const outFiles = [];
var handleDir = (js_only, vendor_rewrite, in_path_base, filename) => Promise.resolve()
.then(() => {
if (no_copy_files.has(filename)) return;
const out_path = path.join(out_path_base, path.relative(in_path_base, filename));
const legacy_path = path.join(legacy_path_base, path.relative(in_path_base, filename));
if(path.extname(filename) !== '.js') {
if (!js_only) {
console.log(`Writing ${out_path}`);
return copy(filename, out_path);
}
return; // skip non-javascript files
}
return Promise.resolve()
.then(() => {
if (only_legacy && !no_transform_files.has(filename)) {
return;
}
return ensureDir(path.dirname(out_path))
.then(() => {
console.log(`Writing ${out_path}`);
return copy(filename, out_path);
})
})
.then(() => ensureDir(path.dirname(legacy_path)))
.then(() => {
if (no_transform_files.has(filename)) {
return;
}
const opts = babel_opts();
if (helper && helpers.optionsOverride) {
helper.optionsOverride(opts);
}
// Adjust for the fact that we move the core files relative
// to the vendor directory
if (vendor_rewrite) {
opts.plugins.push(["import-redirect",
{"root": legacy_path_base,
"redirect": { "vendor/(.+)": "./vendor/$1"}}]);
}
return babelTransformFile(filename, opts)
.then(res => {
console.log(`Writing ${legacy_path}`);
var {code, map, ast} = res;
if (source_maps === true) {
// append URL for external source map
code += `\n//# sourceMappingURL=${path.basename(legacy_path)}.map\n`;
}
outFiles.push(`${legacy_path}`);
return writeFile(legacy_path, code)
.then(() => {
if (source_maps === true || source_maps === 'both') {
console.log(` and ${legacy_path}.map`);
outFiles.push(`${legacy_path}.map`);
return writeFile(`${legacy_path}.map`, JSON.stringify(map));
}
});
});
});
});
if (with_app_dir && helper && helper.noCopyOverride) {
helper.noCopyOverride(paths, no_copy_files);
}
Promise.resolve()
.then(() => {
let handler = handleDir.bind(null, true, false, in_path || paths.main);
let filter = (filename, stats) => !no_copy_files.has(filename);
return walkDir(paths.vendor, handler, filter);
})
.then(() => {
let handler = handleDir.bind(null, true, !in_path, in_path || paths.core);
let filter = (filename, stats) => !no_copy_files.has(filename);
return walkDir(paths.core, handler, filter);
})
.then(() => {
if (!with_app_dir) return;
let handler = handleDir.bind(null, false, false, in_path);
let filter = (filename, stats) => !no_copy_files.has(filename);
return walkDir(paths.app, handler, filter);
})
.then(() => {
if (!with_app_dir) return;
if (!helper || !helper.appWriter) {
throw new Error(`Unable to generate app for the ${import_format} format!`);
}
const out_app_path = path.join(legacy_path_base, 'app.js');
console.log(`Writing ${out_app_path}`);
return helper.appWriter(out_path_base, legacy_path_base, out_app_path)
.then(extra_scripts => {
let rel_app_path = path.relative(out_path_base, out_app_path);
let legacy_scripts = extra_scripts.concat([rel_app_path]);
transform_html(legacy_scripts, only_legacy);
})
.then(() => {
if (!helper.removeModules) return;
console.log(`Cleaning up temporary files...`);
return Promise.all(outFiles.map(filepath => {
unlink(filepath)
.then(() => {
// Try to clean up any empty directories if this
// was the last file in there
let rmdir_r = dir => {
return rmdir(dir)
.then(() => rmdir_r(path.dirname(dir)))
.catch(() => {
// Assume the error was ENOTEMPTY and ignore it
});
};
return rmdir_r(path.dirname(filepath));
});
}));
});
})
.catch((err) => {
console.error(`Failure converting modules: ${err}`);
process.exit(1);
});
};
if (program.clean) {
console.log(`Removing ${paths.lib_dir_base}`);
fse.removeSync(paths.lib_dir_base);
console.log(`Removing ${paths.out_dir_base}`);
fse.removeSync(paths.out_dir_base);
}
make_lib_files(program.as, program.withSourceMaps, program.withApp, program.onlyLegacy);