224 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			224 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| "use_strict";
 | |
| 
 | |
| const loaderUtils = require('loader-utils');
 | |
| const fs = require('fs');
 | |
| const path = require('path');
 | |
| const exec = require('child_process').exec;
 | |
| const tmp = require('tmp');
 | |
| const hash = require('hash-sum');
 | |
| const split = require('haxe-modular/tool/bin/split');
 | |
| 
 | |
| const cache = Object.create(null);
 | |
| 
 | |
| module.exports = function(hxmlContent) {
 | |
|     const context = this;
 | |
|     const options = loaderUtils.getOptions(context) || {};
 | |
|     context.cacheable && context.cacheable();
 | |
|     const cb = context.async();
 | |
| 
 | |
|     const request = context.resourcePath;
 | |
|     if (!request) {
 | |
|         // Loader was called without specifying a hxml file
 | |
|         // Expecting a require of the form '!haxe-loader?hxmlName/moduleName!'
 | |
|         fromCache(context, context.query, cb);
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     const ns = path.basename(request).replace('.hxml', '');
 | |
|     const jsTempFile = makeJSTempFile(ns);
 | |
|     const { jsOutputFile, classpath, args } = prepare(options, context, ns, hxmlContent, jsTempFile);
 | |
| 
 | |
|     registerDepencencies(context, classpath);
 | |
| 
 | |
|     // Execute the Haxe build.
 | |
|     console.log('haxe', args.join(' '));
 | |
|     exec(`haxe ${args.join(' ')}`, (err, stdout, stderr) => {
 | |
|         if (err) {
 | |
|             return cb(err);
 | |
|         }
 | |
| 
 | |
|         // If the hxml file outputs something other than client JS, we should not include it in the bundle.
 | |
|         // We're only passing it through webpack so that we get `watch` and the like to work.
 | |
|         if (!jsOutputFile) {
 | |
|             // We allow the user to configure a timeout so the server has a chance to restart before webpack triggers a page refresh.
 | |
|             var delay = options.delayForNonJsBuilds || 0;
 | |
|             setTimeout(() => {
 | |
|                 // We will include a random string in the output so that the dev server notices a difference and triggers a page refresh.
 | |
|                 cb(null, "// " + Math.random())
 | |
|             }, delay);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         // Read the resulting JS file and return the main module
 | |
|         const processed = processOutput(ns, jsTempFile, jsOutputFile);
 | |
|         if (processed) {
 | |
|             updateCache(context, ns, processed, classpath);
 | |
|         }
 | |
|         returnModule(context, ns, 'Main', cb);
 | |
|     });
 | |
| };
 | |
| 
 | |
| function updateCache(context, ns, { contentHash, results }, classpath) {
 | |
|     cache[ns] = { contentHash, results, classpath };
 | |
| }
 | |
| 
 | |
| function processOutput(ns, jsTempFile, jsOutputFile) {
 | |
|     const content = fs.readFileSync(jsTempFile.path);
 | |
|     // Check whether the output has changed since last build
 | |
|     const contentHash = hash(content);
 | |
|     if (cache[ns] && cache[ns].hash === contentHash)
 | |
|         return null;
 | |
| 
 | |
|     // Split output
 | |
|     const modules = findImports(content);
 | |
|     const debug = fs.existsSync(`${jsTempFile.path}.map`);
 | |
|     const results = split.run(jsTempFile.path, jsOutputFile, modules, debug, true)
 | |
|         .filter(entry => entry && entry.source);
 | |
| 
 | |
|     // Inject .hx sources in map file
 | |
|     results.forEach(entry => {
 | |
|         if (entry.map) {
 | |
|             const map = entry.map.content = JSON.parse(entry.map.content);
 | |
|             map.sourcesContent = map.sources.map(path => {
 | |
|                 try {
 | |
|                     if (path.startsWith('file:///')) path = path.substr(8);
 | |
|                     return fs.readFileSync(path).toString();
 | |
|                 } catch (_) {
 | |
|                     return '';
 | |
|                 }
 | |
|             });
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     // Delete temp files
 | |
|     jsTempFile.cleanup();
 | |
| 
 | |
|     return { contentHash, results };
 | |
| }
 | |
| 
 | |
| function returnModule(context, ns, name, cb) {
 | |
|     const { results, classpath } = cache[ns];
 | |
|     if (!results.length) {
 | |
|         throw new Error(`${ns}.hxml did not emit any modules`);
 | |
|     }
 | |
| 
 | |
|     const entry = results.find(entry => entry.name === name);
 | |
|     if (!entry) {
 | |
|         throw new Error(`${ns}.hxml did not emit a module called '${name}'`);
 | |
|     }
 | |
| 
 | |
|     cb(null, entry.source.content, entry.map ? entry.map.content : null);
 | |
| }
 | |
| 
 | |
| function fromCache(context, query, cb) {
 | |
|     // To locate a split module we expect a query of the form '?hxmlName/moduleName'
 | |
|     const options = /\?([^/]+)\/(.*)/.exec(query);
 | |
|     if (!options) {
 | |
|         throw new Error(`Invalid query: ${query}`);
 | |
|     }
 | |
|     const ns = options[1];
 | |
|     const name = options[2];
 | |
| 
 | |
|     const cached = cache[ns];
 | |
|     if (!cached) {
 | |
|         throw new Error(`${ns}.hxml is not a known entry point`);
 | |
|     }
 | |
| 
 | |
|     registerDepencencies(context, cached.classpath);
 | |
| 
 | |
|     if (!cached.results.length) {
 | |
|         throw new Error(`${ns}.hxml did not emit any modules`);
 | |
|     }
 | |
|     returnModule(context, ns, name, cb);
 | |
| }
 | |
| 
 | |
| function findImports(content) {
 | |
|     // Webpack.load() emits a call to System.import with a query to haxe-loader
 | |
|     const reImports = /System.import\("!haxe-loader\?([^!]+)/g;
 | |
|     const results = [];
 | |
| 
 | |
|     let match = reImports.exec(content);
 | |
|     while (match) {
 | |
|         // Module reference  is of the form 'hxmlName/moduleName'
 | |
|         const name = match[1].substr(match[1].indexOf('/') + 1);
 | |
|         results.push(name);
 | |
|         match = reImports.exec(content);
 | |
|     }
 | |
|     return results;
 | |
| }
 | |
| 
 | |
| function makeJSTempFile() {
 | |
|     const path = tmp.tmpNameSync({ postfix: '.js' });
 | |
|     const nop = () => {};
 | |
|     const cleanup = () => {
 | |
|         fs.unlink(path, nop);
 | |
|         fs.unlink(`${path}.map`, nop);
 | |
|     };
 | |
|     return { path, cleanup };
 | |
| }
 | |
| 
 | |
| function registerDepencencies(context, classpath) {
 | |
|     // Listen for any changes in the classpath
 | |
|     classpath.forEach(path => context.addContextDependency(path));
 | |
| }
 | |
| 
 | |
| function prepare(options, context, ns, hxmlContent, jsTempFile) {
 | |
|     const args = [];
 | |
|     const classpath = [];
 | |
|     let jsOutputFile = null;
 | |
|     let mainClass = 'Main';
 | |
|     let isNodeJs = false;
 | |
| 
 | |
|     // Add args that are specific to hxml-loader
 | |
|     if (options.debug) {
 | |
|         args.push('-debug');
 | |
|     }
 | |
|     args.push('-D', `webpack_namespace=${ns}`);
 | |
| 
 | |
|     // Process all of the args in the hxml file.
 | |
|     for (let line of hxmlContent.split('\n')) {
 | |
|         line = line.trim();
 | |
|         if (line === '' || line.substr(0, 1) === '#') {
 | |
|             continue;
 | |
|         }
 | |
| 
 | |
|         let space = line.indexOf(' ');
 | |
|         let name = space > -1 ? line.substr(0, space) : line;
 | |
|         args.push(name);
 | |
| 
 | |
|         if (name === '--next') {
 | |
|             var err = `${context.resourcePath} included a "--next" line, hxml-loader only supports a single build per hxml file.`;
 | |
|             throw new Error(err);
 | |
|         }
 | |
| 
 | |
|         if (space > -1) {
 | |
|             let value = line.substr(space + 1).trim();
 | |
| 
 | |
|             if (name === '-js' && !isNodeJs) {
 | |
|                 jsOutputFile = value;
 | |
|                 args.push(jsTempFile.path);
 | |
|                 continue;
 | |
|             }
 | |
| 
 | |
|             if (name === '-cp') {
 | |
|                 classpath.push(path.resolve(value));
 | |
|             }
 | |
| 
 | |
|             if (name === '-lib' && value == 'hxnodejs') {
 | |
|                 isNodeJs = true;
 | |
|                 if (jsOutputFile) {
 | |
|                     // If a JS output file was already set to use a webpack temp file, go back and undo that.
 | |
|                     args = args.map(arg => (arg === jsTempFile.path) ? value : arg);
 | |
|                     jsOutputFile = null;
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             args.push(value);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     if (options.extra) args.push(options.extra);
 | |
| 
 | |
|     return { jsOutputFile, classpath, args };
 | |
| }
 |