UNPKG

toloframework

Version:

Javascript/HTML/CSS compiler for Firefox OS or nodewebkit apps using modules in the nodejs style.

256 lines (222 loc) 8.1 kB
"use strict"; exports.compile = compileJS; const ToloframeworkPermissiveJson = require( "toloframework-permissive-json" ), ModuleAnalyser = require( "./module-analyser" ), CompilerINI = require( "./compiler-ini" ), Transpiler = require( "./transpiler" ), PathUtils = require( "./pathutils" ), Source = require( "./source" ), Fatal = require( "./fatal" ), Path = require( "path" ), Util = require( "./util" ), Tpl = require( "./template" ), Xjs = require( "./boilerplate" ); /** * @param {project} project - Current project. * @param {string} path - Source path relative to the `src` folder. * @param {object} options - Build options. `{ debug, transpilation, ... }`. * @return {Source} * Tags: * * __src__: debug content. * * __zip__: release content. * * __dependencies__: array of dependent modules. */ function compileJS( project, path, options ) { const src = new Source( project, path ); ensureJavascriptFileExists( project, src ); if ( src.isUptodate() ) return src; const moduleName = src.name(), watch = [], moduleShortName = Util.removeExtension( Util.removeFirstSubFolder( moduleName ) ); console.log( `Compiling JS ${moduleShortName.yellow}` ); const head = compileDEP( project, src, watch ); compileINI( project, src ); let code = convertIntoModule( src, moduleShortName, head ); // Export internationalization. if ( path !== 'mod/$.js' ) { code += "module.exports._ = _;\n"; } code += "})"; const transpiled = Transpiler.transpile( code, src.getAbsoluteFilePath(), !( options.debug || options.dev ) ); const info = ModuleAnalyser.extractInfoFromAst( transpiled.ast ), dependencies = info.requires.map( function mapDependencies( itm ) { return `mod/${itm}`; } ); console.log( `Dependent modules (${info.requires.length}): `, info.requires.join( ', ' ).grey ); createMarkdownDoc( src, info ); if ( options.transpilation ) { saveTags( src, transpiled.code, transpiled.zip, transpiled.map, dependencies ); } else { console.log( "Transpilation: OFF".red ); saveTags( src, code, transpiled.zip, null, dependencies ); } return src; } /** * @param {string} src description * @param {string} code description * @param {string} zip description * @param {string} map description * @param {string} dependencies description * @returns {undefined} */ function saveTags( src, code, zip, map, dependencies ) { src.tag( 'src', code ); src.tag( 'zip', zip ); src.tag( 'map', map ); src.tag( 'dependencies', dependencies ); src.save(); } /** * @param {project} project - Current project. * @param {Source} src - JS source file. * @returns {undefined} */ function ensureJavascriptFileExists( project, src ) { const srcXJS = src.clone( "xjs" ); if ( !src.exists() ) { if ( !srcXJS.exists() ) { // This file does not exist! Fatal.fire( `Javascript file not found: "${src.name()}"!`, src.name() ); } else { // XJS exists but not JS. That's why we create a minimal one. src.write( "// Code behind.\n\"use strict\";\n" ); } } } /** * DEP file is here to load GLOBAL variable into the module. * @param {Project} project - Current project. * @param {Source} src - Module source file. * @param {array} watch - Array of files to watch for rebuild. * @returns {string} Javascript defining the const variable GLOBAL. */ function compileDEP( project, src, watch ) { const moduleName = src.name(), depFilename = Util.replaceExtension( moduleName, '.dep' ); if ( !project.srcOrLibPath( depFilename ) ) return ''; const depFile = new Source( project, depFilename ); let code = ''; try { const depJSON = ToloframeworkPermissiveJson.parse( depFile.read() ); code = processAttributeVar( project, depJSON, watch, depFile.getAbsoluteFilePath() ); } catch ( ex ) { Fatal.fire( `Unable to parse JSON dependency file!\n${ex}`, depFile.getAbsoluteFilePath() ); } /* * List of files to watch. If one of those files is newer * that the JS file, we have to recompile. */ src.tag( 'watch', watch ); return code; } /** * In the DEP file, we can find the attribute "var". * It will load text file contents into the GLOBAL variable of the module. * @param {Project} project - Current project. * @param {objetc} depJSON - Parsing of the JSON DEP file. * @param {array} watch - Array of files to watch for rebuild. * @param {string} depAbsPath - Absolute path of the DEP file. * @returns {string} Javascript defining the const variable GLOBAL. */ function processAttributeVar( project, depJSON, watch, depAbsPath ) { if ( typeof depJSON.var === 'undefined' ) return ''; let head = 'const GLOBAL = {', firstItem = true; Object.keys( depJSON.var ).forEach( function forEachVarName( varName ) { const varFilename = depJSON.var[ varName ], folder = Path.dirname( depAbsPath ), srcVar = project.srcOrLibPath( Path.join( folder, varFilename ) ) || project.srcOrLibPath( `mod/${varFilename}` ) || project.srcOrLibPath( varFilename ); if ( !srcVar ) { Fatal.fire( `Unable to find dendency file "${varFilename}" nor "mod/${varFilename}"!`, depAbsPath ); } Util.pushUnique( watch, `mod/${varFilename}` ); if ( firstItem ) { firstItem = false; } else { head += ','; } const source = new Source( project, srcVar ); head += `\n ${JSON.stringify(varName)}: ${JSON.stringify(source.read())}`; } ); head += "};\n"; return head; } /** * Internationalization. * @param {Project} project - Current project. * @param {Source} src - Module source file. * @returns {undefined} */ function compileINI( project, src ) { // Intl. if ( src.name( "js" ) === 'mod/$.js' ) { // Internationalization for all modules except the special one: '$'. src.tag( "intl", "" ); } else { const iniName = src.name( "ini" ), iniPath = project.srcOrLibPath( iniName ); if ( iniPath ) { src.tag( "intl", CompilerINI.parse( iniPath ) ); } else { src.tag( "intl", "var _=function(){return ''};" ); } } } /** * @param {Source} src - Module source file. * @param {string} moduleShortName - Name of the module without folder and without extension. * @param {string} head - Javascript already present at the top of the module (i.e.: internationalization). * @returns {string} Resulting code. */ function convertIntoModule( src, moduleShortName, head ) { const options = { name: moduleShortName, code: Xjs.generateCodeFrom( src ), intl: src.tag( 'intl' ), head: `${head} `, foot: "" }; return Tpl.file( "module.js", options ).out; } /** * @param {Source} src - Module source. * @param {objetc} info - `{ requires, exports }`. * @param {array} info.publics - `[{ id, type, params, comments }, ...]`. * @returns {undefined} */ function createMarkdownDoc( src, info ) { const publics = info.exports, prj = src.prj(), name = src.name(), dstPath = prj.docPath( Util.replaceExtension( name, '.md' ) ); let output = `# ${name}\n`; publics.forEach( function ( item ) { const params = ( item.params || [] ).join( ', ' ), comments = item.comments; output += `## \`${item.id}(${params})\`\n\n${comments}\n\n`; } ); if ( info.requires.length > 0 ) { output += "\n----\n\n## Dependencies\n"; output += info.requires.map( req => `* [${req}](${req}.md)` ).join( "\n" ); } PathUtils.file( dstPath, output ); }