UNPKG

8.1 kBJavaScriptView Raw
1"use strict";
2
3exports.compile = compileJS;
4
5const
6 ToloframeworkPermissiveJson = require( "toloframework-permissive-json" ),
7 ModuleAnalyser = require( "./module-analyser" ),
8 CompilerINI = require( "./compiler-ini" ),
9 Transpiler = require( "./transpiler" ),
10 PathUtils = require( "./pathutils" ),
11 Source = require( "./source" ),
12 Fatal = require( "./fatal" ),
13 Path = require( "path" ),
14 Util = require( "./util" ),
15 Tpl = require( "./template" ),
16 Xjs = require( "./boilerplate" );
17
18
19/**
20 * @param {project} project - Current project.
21 * @param {string} path - Source path relative to the `src` folder.
22 * @param {object} options - Build options. `{ debug, transpilation, ... }`.
23 * @return {Source}
24 * Tags:
25 * * __src__: debug content.
26 * * __zip__: release content.
27 * * __dependencies__: array of dependent modules.
28 */
29function compileJS( project, path, options ) {
30 const src = new Source( project, path );
31
32 ensureJavascriptFileExists( project, src );
33 if ( src.isUptodate() ) return src;
34
35 const
36 moduleName = src.name(),
37 watch = [],
38 moduleShortName = Util.removeExtension( Util.removeFirstSubFolder( moduleName ) );
39
40 console.log( `Compiling JS ${moduleShortName.yellow}` );
41
42 const head = compileDEP( project, src, watch );
43 compileINI( project, src );
44 let code = convertIntoModule( src, moduleShortName, head );
45
46 // Export internationalization.
47 if ( path !== 'mod/$.js' ) {
48 code += "module.exports._ = _;\n";
49 }
50 code += "})";
51
52 const transpiled = Transpiler.transpile(
53 code,
54 src.getAbsoluteFilePath(),
55 !( options.debug || options.dev )
56 );
57
58 const
59 info = ModuleAnalyser.extractInfoFromAst( transpiled.ast ),
60 dependencies = info.requires.map( function mapDependencies( itm ) {
61 return `mod/${itm}`;
62 } );
63 console.log( `Dependent modules (${info.requires.length}): `, info.requires.join( ', ' ).grey );
64
65 createMarkdownDoc( src, info );
66 if ( options.transpilation ) {
67 saveTags( src, transpiled.code, transpiled.zip, transpiled.map, dependencies );
68 } else {
69 console.log( "Transpilation: OFF".red );
70 saveTags( src, code, transpiled.zip, null, dependencies );
71 }
72
73 return src;
74}
75
76
77/**
78 * @param {string} src description
79 * @param {string} code description
80 * @param {string} zip description
81 * @param {string} map description
82 * @param {string} dependencies description
83 * @returns {undefined}
84 */
85function saveTags( src, code, zip, map, dependencies ) {
86 src.tag( 'src', code );
87 src.tag( 'zip', zip );
88 src.tag( 'map', map );
89 src.tag( 'dependencies', dependencies );
90 src.save();
91}
92
93/**
94 * @param {project} project - Current project.
95 * @param {Source} src - JS source file.
96 * @returns {undefined}
97 */
98function ensureJavascriptFileExists( project, src ) {
99 const srcXJS = src.clone( "xjs" );
100 if ( !src.exists() ) {
101 if ( !srcXJS.exists() ) {
102 // This file does not exist!
103 Fatal.fire( `Javascript file not found: "${src.name()}"!`, src.name() );
104 } else {
105 // XJS exists but not JS. That's why we create a minimal one.
106 src.write( "// Code behind.\n\"use strict\";\n" );
107 }
108 }
109}
110
111
112/**
113 * DEP file is here to load GLOBAL variable into the module.
114 * @param {Project} project - Current project.
115 * @param {Source} src - Module source file.
116 * @param {array} watch - Array of files to watch for rebuild.
117 * @returns {string} Javascript defining the const variable GLOBAL.
118 */
119function compileDEP( project, src, watch ) {
120 const
121 moduleName = src.name(),
122 depFilename = Util.replaceExtension( moduleName, '.dep' );
123
124 if ( !project.srcOrLibPath( depFilename ) ) return '';
125
126 const depFile = new Source( project, depFilename );
127 let code = '';
128 try {
129 const depJSON = ToloframeworkPermissiveJson.parse( depFile.read() );
130 code = processAttributeVar( project, depJSON, watch, depFile.getAbsoluteFilePath() );
131 } catch ( ex ) {
132 Fatal.fire( `Unable to parse JSON dependency file!\n${ex}`, depFile.getAbsoluteFilePath() );
133 }
134
135 /*
136 * List of files to watch. If one of those files is newer
137 * that the JS file, we have to recompile.
138 */
139 src.tag( 'watch', watch );
140 return code;
141}
142
143
144/**
145 * In the DEP file, we can find the attribute "var".
146 * It will load text file contents into the GLOBAL variable of the module.
147 * @param {Project} project - Current project.
148 * @param {objetc} depJSON - Parsing of the JSON DEP file.
149 * @param {array} watch - Array of files to watch for rebuild.
150 * @param {string} depAbsPath - Absolute path of the DEP file.
151 * @returns {string} Javascript defining the const variable GLOBAL.
152 */
153function processAttributeVar( project, depJSON, watch, depAbsPath ) {
154 if ( typeof depJSON.var === 'undefined' ) return '';
155
156 let
157 head = 'const GLOBAL = {',
158 firstItem = true;
159
160 Object.keys( depJSON.var ).forEach( function forEachVarName( varName ) {
161 const
162 varFilename = depJSON.var[ varName ],
163 folder = Path.dirname( depAbsPath ),
164 srcVar = project.srcOrLibPath( Path.join( folder, varFilename ) ) ||
165 project.srcOrLibPath( `mod/${varFilename}` ) ||
166 project.srcOrLibPath( varFilename );
167
168 if ( !srcVar ) {
169 Fatal.fire(
170 `Unable to find dendency file "${varFilename}" nor "mod/${varFilename}"!`,
171 depAbsPath
172 );
173 }
174 Util.pushUnique( watch, `mod/${varFilename}` );
175 if ( firstItem ) {
176 firstItem = false;
177 } else {
178 head += ',';
179 }
180 const source = new Source( project, srcVar );
181 head += `\n ${JSON.stringify(varName)}: ${JSON.stringify(source.read())}`;
182 } );
183 head += "};\n";
184
185 return head;
186}
187
188
189/**
190 * Internationalization.
191 * @param {Project} project - Current project.
192 * @param {Source} src - Module source file.
193 * @returns {undefined}
194 */
195function compileINI( project, src ) {
196 // Intl.
197 if ( src.name( "js" ) === 'mod/$.js' ) {
198 // Internationalization for all modules except the special one: '$'.
199 src.tag( "intl", "" );
200 } else {
201 const
202 iniName = src.name( "ini" ),
203 iniPath = project.srcOrLibPath( iniName );
204 if ( iniPath ) {
205 src.tag( "intl", CompilerINI.parse( iniPath ) );
206 } else {
207 src.tag( "intl", "var _=function(){return ''};" );
208 }
209 }
210}
211
212
213/**
214 * @param {Source} src - Module source file.
215 * @param {string} moduleShortName - Name of the module without folder and without extension.
216 * @param {string} head - Javascript already present at the top of the module (i.e.: internationalization).
217 * @returns {string} Resulting code.
218 */
219function convertIntoModule( src, moduleShortName, head ) {
220 const options = {
221 name: moduleShortName,
222 code: Xjs.generateCodeFrom( src ),
223 intl: src.tag( 'intl' ),
224 head: `${head} `,
225 foot: ""
226 };
227 return Tpl.file( "module.js", options ).out;
228}
229
230/**
231 * @param {Source} src - Module source.
232 * @param {objetc} info - `{ requires, exports }`.
233 * @param {array} info.publics - `[{ id, type, params, comments }, ...]`.
234 * @returns {undefined}
235 */
236function createMarkdownDoc( src, info ) {
237 const
238 publics = info.exports,
239 prj = src.prj(),
240 name = src.name(),
241 dstPath = prj.docPath( Util.replaceExtension( name, '.md' ) );
242 let output = `# ${name}\n`;
243 publics.forEach( function ( item ) {
244 const
245 params = ( item.params || [] ).join( ', ' ),
246 comments = item.comments;
247 output += `## \`${item.id}(${params})\`\n\n${comments}\n\n`;
248 } );
249
250 if ( info.requires.length > 0 ) {
251 output += "\n----\n\n## Dependencies\n";
252 output += info.requires.map( req => `* [${req}](${req}.md)` ).join( "\n" );
253 }
254 PathUtils.file( dstPath, output );
255}