2020-09-20 14:16:44 +02:00
#!/usr/bin/env node
const path = require ( 'path' ) ;
const program = require ( 'commander' ) ;
const fs = require ( 'fs' ) ;
const fse = require ( 'fs-extra' ) ;
2019-11-11 15:46:12 +01:00
const babel = require ( '@babel/core' ) ;
2020-09-20 14:16:44 +02:00
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 . app , 'images' , 'icons' , 'Makefile' ) ,
] ) ;
2019-11-11 13:36:30 +01:00
const only _legacy _scripts = new Set ( [
path . join ( paths . vendor , 'promise.js' ) ,
] ) ;
2020-09-20 14:16:44 +02:00
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 promise _wrap ( ) {
const args = Array . prototype . slice . call ( arguments ) ;
return new Promise ( ( resolve , reject ) => {
original . apply ( this , 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.
function walkDir ( base _path , cb , filter ) {
return readdir ( base _path )
. then ( ( files ) => {
const paths = files . map ( filename => path . join ( base _path , filename ) ) ;
return Promise . all ( paths . map ( filepath => 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 ) ;
} ) ) ) ;
} ) ;
}
function transform _html ( legacy _scripts , only _legacy ) {
// write out the modified vnc.html file that works with the bundle
const src _html _path = path . resolve ( _ _dirname , '..' , 'vnc.html' ) ;
const out _html _path = path . resolve ( paths . out _dir _base , 'vnc.html' ) ;
return readFile ( src _html _path )
. then ( ( contents _raw ) => {
let contents = contents _raw . toString ( ) ;
const start _marker = '<!-- begin scripts -->\n' ;
const end _marker = '<!-- end scripts -->' ;
const start _ind = contents . indexOf ( start _marker ) + start _marker . length ;
const end _ind = contents . indexOf ( end _marker , start _ind ) ;
let 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 \
< / s c r i p t > \ 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 \
< / s c r i p t > \ 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 ) ;
} ) ;
}
function make _lib _files ( 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 = ( ) => ( {
2019-11-11 15:46:12 +01:00
plugins : [ ] ,
presets : [
[ '@babel/preset-env' ,
{ targets : 'ie >= 11' ,
modules : import _format } ]
] ,
2020-09-20 14:16:44 +02:00
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 ;
}
let in _path ;
let out _path _base ;
if ( with _app _dir ) {
out _path _base = paths . out _dir _base ;
in _path = paths . main ;
} else {
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 = [ ] ;
2019-11-11 13:36:30 +01:00
const legacyFiles = [ ] ;
2020-09-20 14:16:44 +02:00
const handleDir = ( js _only , vendor _rewrite , in _path _base , filename ) => Promise . resolve ( )
. then ( ( ) => {
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
}
2019-11-11 13:33:47 +01:00
if ( no _transform _files . has ( filename ) ) {
return ensureDir ( path . dirname ( out _path ) )
. then ( ( ) => {
console . log ( ` Writing ${ out _path } ` ) ;
return copy ( filename , out _path ) ;
} ) ;
}
2019-11-11 13:36:30 +01:00
if ( only _legacy _scripts . has ( filename ) ) {
legacyFiles . push ( legacy _path ) ;
return ensureDir ( path . dirname ( legacy _path ) )
. then ( ( ) => {
console . log ( ` Writing ${ legacy _path } ` ) ;
return copy ( filename , legacy _path ) ;
} ) ;
}
2020-09-20 14:16:44 +02:00
return Promise . resolve ( )
. then ( ( ) => {
2019-11-11 13:33:47 +01:00
if ( only _legacy ) {
2020-09-20 14:16:44 +02:00
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 ( ( ) => {
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 } ` ) ;
const { map } = res ;
let { code } = 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 ) ) ;
}
} ) ;
} ) ;
} ) ;
} ) ;
Promise . resolve ( )
. then ( ( ) => {
const handler = handleDir . bind ( null , true , false , in _path || paths . main ) ;
const filter = ( filename , stats ) => ! no _copy _files . has ( filename ) ;
return walkDir ( paths . vendor , handler , filter ) ;
} )
. then ( ( ) => {
const handler = handleDir . bind ( null , true , ! in _path , in _path || paths . core ) ;
const filter = ( filename , stats ) => ! no _copy _files . has ( filename ) ;
return walkDir ( paths . core , handler , filter ) ;
} )
. then ( ( ) => {
if ( ! with _app _dir ) return ;
const handler = handleDir . bind ( null , false , false , in _path ) ;
const 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 ) => {
2019-11-12 10:24:40 +01:00
let legacy _scripts = [ ] ;
2019-11-11 13:36:30 +01:00
legacyFiles . forEach ( ( file ) => {
let rel _file _path = path . relative ( out _path _base , file ) ;
legacy _scripts . push ( rel _file _path ) ;
} ) ;
2019-11-12 10:24:40 +01:00
legacy _scripts = legacy _scripts . concat ( extra _scripts ) ;
2019-11-11 13:36:30 +01:00
let rel _app _path = path . relative ( out _path _base , out _app _path ) ;
legacy _scripts . push ( rel _app _path ) ;
2020-09-20 14:16:44 +02:00
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
const rmdir _r = dir =>
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 ) ;