瀏覽代碼

Merge pull request #977 from amber-smalltalk/include-amber-dev

Include amber dev
Nicolas Petton 10 年之前
父節點
當前提交
3afa18bc4a

+ 17 - 0
.npmignore

@@ -0,0 +1,17 @@
+# Ignoring Mac Finder files
+.DS_Store
+
+# Ignoring test runner
+test_runner.js
+
+# In case amber is also saved in a local Subversion
+.svn
+
+# Ignoring local NPM modules
+node_modules/
+
+# Ignoring local bower modules
+bower_components/
+
+# Exclude the externals
+/external/

+ 1 - 0
bower.json

@@ -6,6 +6,7 @@
     "**/.*",
     "node_modules",
     "bower_components",
+    "/external",
     "test",
     "tests"
   ],

+ 143 - 0
external/amber-dev/lib/Test.js

@@ -0,0 +1,143 @@
+define("amber_core/Test", ["amber_vm/smalltalk", "amber_vm/nil", "amber_vm/_st", "amber_vm/globals", "amber_core/Kernel-Objects"], function(smalltalk,nil,_st, globals){
+smalltalk.addPackage('Test');
+smalltalk.packages["Test"].transport = {"type":"amd","amdNamespace":"amber_core"};
+
+smalltalk.addClass('NodeTestRunner', globals.Object, [], 'Test');
+
+smalltalk.addMethod(
+smalltalk.method({
+selector: "main",
+protocol: 'not yet classified',
+fn: function (){
+var self=this;
+return smalltalk.withContext(function($ctx1) { 
+self._runTestSuite();
+return self}, function($ctx1) {$ctx1.fill(self,"main",{},globals.NodeTestRunner.klass)})},
+args: [],
+source: "main\x0a\x09self runTestSuite",
+messageSends: ["runTestSuite"],
+referencedClasses: []
+}),
+globals.NodeTestRunner.klass);
+
+smalltalk.addMethod(
+smalltalk.method({
+selector: "runTestSuite",
+protocol: 'not yet classified',
+fn: function (){
+var self=this;
+var suite,worker;
+function $OrderedCollection(){return globals.OrderedCollection||(typeof OrderedCollection=="undefined"?nil:OrderedCollection)}
+function $TestCase(){return globals.TestCase||(typeof TestCase=="undefined"?nil:TestCase)}
+function $TestSuiteRunner(){return globals.TestSuiteRunner||(typeof TestSuiteRunner=="undefined"?nil:TestSuiteRunner)}
+function $ResultAnnouncement(){return globals.ResultAnnouncement||(typeof ResultAnnouncement=="undefined"?nil:ResultAnnouncement)}
+return smalltalk.withContext(function($ctx1) { 
+var $2,$1,$3,$9,$8,$12,$11,$10,$7,$6,$15,$14,$13,$5,$4,$17,$16,$19,$18,$26,$25,$24,$23,$22,$28,$27,$21,$20,$30,$29,$32,$31,$39,$38,$37,$36,$35,$34,$33;
+suite=_st($OrderedCollection())._new();
+_st(_st(_st($TestCase())._allSubclasses())._select_((function(each){
+return smalltalk.withContext(function($ctx2) {
+return _st(_st(each)._isAbstract())._not();
+}, function($ctx2) {$ctx2.fillBlock({each:each},$ctx1,1)})})))._do_((function(each){
+return smalltalk.withContext(function($ctx2) {
+return _st(suite)._addAll_(_st(each)._buildSuite());
+}, function($ctx2) {$ctx2.fillBlock({each:each},$ctx1,2)})}));
+worker=_st($TestSuiteRunner())._on_(suite);
+_st(_st(worker)._announcer())._on_do_($ResultAnnouncement(),(function(ann){
+var result;
+return smalltalk.withContext(function($ctx2) {
+result=_st(ann)._result();
+result;
+$2=_st(result)._runs();
+$ctx2.sendIdx["runs"]=1;
+$1=_st($2).__eq(_st(result)._total());
+if(smalltalk.assert($1)){
+$3=console;
+$9=_st(_st(result)._runs())._asString();
+$ctx2.sendIdx["asString"]=1;
+$8=_st($9).__comma(" tests run, ");
+$ctx2.sendIdx[","]=5;
+$12=_st(result)._failures();
+$ctx2.sendIdx["failures"]=1;
+$11=_st($12)._size();
+$ctx2.sendIdx["size"]=1;
+$10=_st($11)._asString();
+$ctx2.sendIdx["asString"]=2;
+$7=_st($8).__comma($10);
+$ctx2.sendIdx[","]=4;
+$6=_st($7).__comma(" failures, ");
+$ctx2.sendIdx[","]=3;
+$15=_st(result)._errors();
+$ctx2.sendIdx["errors"]=1;
+$14=_st($15)._size();
+$13=_st($14)._asString();
+$5=_st($6).__comma($13);
+$ctx2.sendIdx[","]=2;
+$4=_st($5).__comma(" errors.");
+$ctx2.sendIdx[","]=1;
+_st($3)._log_($4);
+$17=_st(result)._failures();
+$ctx2.sendIdx["failures"]=2;
+$16=_st($17)._isEmpty();
+$ctx2.sendIdx["isEmpty"]=1;
+if(! smalltalk.assert($16)){
+$19=_st(result)._failures();
+$ctx2.sendIdx["failures"]=3;
+$18=_st($19)._first();
+$ctx2.sendIdx["first"]=1;
+_st($18)._runCase();
+$ctx2.sendIdx["runCase"]=1;
+$26=_st(result)._failures();
+$ctx2.sendIdx["failures"]=4;
+$25=_st($26)._first();
+$ctx2.sendIdx["first"]=2;
+$24=_st($25)._class();
+$ctx2.sendIdx["class"]=1;
+$23=_st($24)._name();
+$ctx2.sendIdx["name"]=1;
+$22=_st($23).__comma(" >> ");
+$ctx2.sendIdx[","]=8;
+$28=_st(_st(result)._failures())._first();
+$ctx2.sendIdx["first"]=3;
+$27=_st($28)._selector();
+$ctx2.sendIdx["selector"]=1;
+$21=_st($22).__comma($27);
+$ctx2.sendIdx[","]=7;
+$20=_st($21).__comma(" is failing!");
+$ctx2.sendIdx[","]=6;
+self._throw_($20);
+$ctx2.sendIdx["throw:"]=1;
+};
+$30=_st(result)._errors();
+$ctx2.sendIdx["errors"]=2;
+$29=_st($30)._isEmpty();
+if(! smalltalk.assert($29)){
+$32=_st(result)._errors();
+$ctx2.sendIdx["errors"]=3;
+$31=_st($32)._first();
+$ctx2.sendIdx["first"]=4;
+_st($31)._runCase();
+$39=_st(result)._errors();
+$ctx2.sendIdx["errors"]=4;
+$38=_st($39)._first();
+$ctx2.sendIdx["first"]=5;
+$37=_st($38)._class();
+$36=_st($37)._name();
+$35=_st($36).__comma(" >> ");
+$34=_st($35).__comma(_st(_st(_st(result)._errors())._first())._selector());
+$ctx2.sendIdx[","]=10;
+$33=_st($34).__comma(" has errors!");
+$ctx2.sendIdx[","]=9;
+return self._throw_($33);
+};
+};
+}, function($ctx2) {$ctx2.fillBlock({ann:ann,result:result},$ctx1,3)})}));
+_st(worker)._run();
+return self}, function($ctx1) {$ctx1.fill(self,"runTestSuite",{suite:suite,worker:worker},globals.NodeTestRunner.klass)})},
+args: [],
+source: "runTestSuite\x0a\x09| suite worker |\x0a\x0a\x09suite := OrderedCollection new.\x0a    (TestCase allSubclasses select: [ :each | each isAbstract not ])\x0a\x09do: [ :each | suite addAll: each buildSuite ].\x0a\x0a\x09worker := TestSuiteRunner on: suite.\x0a\x09worker announcer on: ResultAnnouncement do:\x0a\x09[ :ann | | result |\x0a    \x09result := ann result.\x0a        result runs = result total ifTrue: [\x0a\x09        console log: result runs asString, ' tests run, ', result failures size asString, ' failures, ', result errors size asString, ' errors.'.\x0a\x0a            result failures isEmpty ifFalse: [\x0a                result failures first runCase.\x0a                \x22the line above should throw, normally, but just in case I leave the line below\x22\x0a                self throw: result failures first class name, ' >> ', result failures first selector, ' is failing!' ].\x0a            result errors isEmpty ifFalse: [\x0a                result errors first runCase.\x0a                \x22the line above should throw, normally, but just in case I leave the line below\x22\x0a                self throw: result errors first class name, ' >> ', result errors first selector, ' has errors!' ].\x0a    ]].\x0a    worker run",
+messageSends: ["new", "do:", "select:", "allSubclasses", "not", "isAbstract", "addAll:", "buildSuite", "on:", "on:do:", "announcer", "result", "ifTrue:", "=", "runs", "total", "log:", ",", "asString", "size", "failures", "errors", "ifFalse:", "isEmpty", "runCase", "first", "throw:", "name", "class", "selector", "run"],
+referencedClasses: ["OrderedCollection", "TestCase", "TestSuiteRunner", "ResultAnnouncement"]
+}),
+globals.NodeTestRunner.klass);
+
+});

+ 37 - 0
external/amber-dev/lib/Test.st

@@ -0,0 +1,37 @@
+Smalltalk createPackage: 'Test'!
+Object subclass: #NodeTestRunner
+	instanceVariableNames: ''
+	package: 'Test'!
+
+!NodeTestRunner class methodsFor: 'not yet classified'!
+
+main
+	self runTestSuite
+!
+
+runTestSuite
+	| suite worker |
+
+	suite := OrderedCollection new.
+    (TestCase allSubclasses select: [ :each | each isAbstract not ])
+	do: [ :each | suite addAll: each buildSuite ].
+
+	worker := TestSuiteRunner on: suite.
+	worker announcer on: ResultAnnouncement do:
+	[ :ann | | result |
+    	result := ann result.
+        result runs = result total ifTrue: [
+	        console log: result runs asString, ' tests run, ', result failures size asString, ' failures, ', result errors size asString, ' errors.'.
+
+            result failures isEmpty ifFalse: [
+                result failures first runCase.
+                "the line above should throw, normally, but just in case I leave the line below"
+                self throw: result failures first class name, ' >> ', result failures first selector, ' is failing!!' ].
+            result errors isEmpty ifFalse: [
+                result errors first runCase.
+                "the line above should throw, normally, but just in case I leave the line below"
+                self throw: result errors first class name, ' >> ', result errors first selector, ' has errors!!' ].
+    ]].
+    worker run
+! !
+

+ 587 - 0
external/amber-dev/lib/amberc.js

@@ -0,0 +1,587 @@
+/**
+ * This is a "compiler" for Amber code.
+ * Put the following code into compiler.js:
+ *     var amberc = require('amberc');
+ *     var compiler = new amberc.Compiler('path/to/amber');
+ *     var options = amberc.createDefaults();
+ *     // edit options entries
+ *     compiler.main(options);
+ */
+
+/**
+ * Helper for concatenating Amber generated AMD modules.
+ * The produced output can be exported and run as an independent program.
+ *
+ * var concatenator = createConcatenator();
+ * concatenator.start(); // write the required AMD define header
+ * concatenator.add(module1);
+ * concatenator.addId(module1_ID);
+ * //...
+ * concatenator.finish("//some last code");
+ * var concatenation = concatenator.toString();
+ * // The variable concatenation contains the concatenated result
+ * // which can either be stored in a file or interpreted with eval().
+ */
+function createConcatenator () {
+	return {
+		elements: [],
+		ids: [],
+		add: function () {
+			this.elements.push.apply(this.elements, arguments);
+		},
+		addId: function () {
+			this.ids.push.apply(this.ids, arguments);
+		},
+		forEach: function () {
+			this.elements.forEach.apply(this.elements, arguments);
+		},
+		start: function () {
+			this.add(
+				'var define = (' + require('amdefine') + ')(null, function (id) { throw new Error("Dependency not found: " +  id); }), requirejs = define.require;',
+				'define("amber_vm/browser-compatibility", [], {});'
+			);
+		},
+		finish: function (realWork) {
+			this.add(
+				'define("amber_vm/_init", ["amber_vm/smalltalk", "amber_vm/globals", "' + this.ids.join('","') + '"], function (vm, globals) {',
+				'vm.initialize();',
+				realWork,
+				'});',
+				'requirejs("amber_vm/_init");'
+			);
+		},
+		toString: function () {
+			return this.elements.join('\n');
+		}
+	};
+}
+
+
+var path = require('path'),
+	fs = require('fs'),
+	Promise = require('es6-promise').Promise;
+
+/**
+ * AmberCompiler constructor function.
+ * amber_dir: points to the location of an amber installation
+ */
+function AmberCompiler(amber_dir) {
+	if (undefined === amber_dir || !fs.existsSync(amber_dir)) {
+		throw new Error('amber_dir needs to be a valid directory');
+	}
+
+	this.amber_dir = amber_dir;
+	this.kernel_libraries = ['boot', 'smalltalk', 'globals', 'nil', '_st', 'Kernel-Objects', 'Kernel-Classes', 'Kernel-Methods',
+							'Kernel-Collections', 'Kernel-Infrastructure', 'Kernel-Exceptions', 'Kernel-Transcript',
+							'Kernel-Announcements'];
+	this.compiler_libraries = this.kernel_libraries.concat(['parser', 'Kernel-ImportExport', 'Compiler-Exceptions',
+							'Compiler-Core', 'Compiler-AST', 'Compiler-Exceptions', 'Compiler-IR', 'Compiler-Inlining', 'Compiler-Semantic']);
+}
+
+
+/**
+ * Default values.
+ */
+var createDefaultConfiguration = function() {
+	return {
+		'load': [],
+		'main': undefined,
+		'mainfile': undefined,
+		'stFiles': [],
+		'jsFiles': [],
+		'jsGlobals': [],
+		'amd_namespace': 'amber_core',
+		'suffix': '',
+		'loadsuffix': '',
+		'suffix_used': '',
+		'libraries': [],
+		'jsLibraryDirs': [],
+		'compile': [],
+		'compiled': [],
+		'program': undefined,
+		'output_dir': undefined,
+		'verbose': false
+	};
+};
+
+
+/**
+ * Main function for executing the compiler.
+ * If check_configuration_ok() returns successfully
+ * the configuration is used to trigger the following compilation steps.
+ */
+AmberCompiler.prototype.main = function(configuration, finished_callback) {
+	console.time('Compile Time');
+
+	if (configuration.amd_namespace.length === 0) {
+		configuration.amd_namespace = 'amber_core';
+	}
+
+	if (undefined !== configuration.jsLibraryDirs) {
+		configuration.jsLibraryDirs.push(path.join(this.amber_dir, 'src'));
+		configuration.jsLibraryDirs.push(path.join(this.amber_dir, 'support'));
+	}
+
+	console.ambercLog = console.log;
+	if (false === configuration.verbose) {
+		console.log = function() {};
+	}
+
+	// the evaluated compiler will be stored in this variable (see create_compiler)
+	configuration.vm = {};
+	configuration.globals = {};
+	configuration.kernel_libraries = this.kernel_libraries;
+	configuration.compiler_libraries = this.compiler_libraries;
+	configuration.amber_dir = this.amber_dir;
+
+	check_configuration(configuration)
+	.then(collect_st_files)
+	.then(collect_js_files)
+	.then(resolve_kernel)
+	.then(create_compiler)
+	.then(compile)
+	.then(category_export)
+	.then(verify)
+	.then(compose_js_files)
+	.then(function () {
+		console.timeEnd('Compile Time');
+	}, function(error) {
+		console.error(error);
+	})
+	.then(function () {
+		console.log = console.ambercLog;
+		finished_callback && finished_callback();
+	});
+};
+
+
+/**
+ * Check if the passed in configuration object has sufficient/nonconflicting values.
+ * Returns a Promise which resolves into the configuration object.
+ */
+function check_configuration(configuration) {
+	return new Promise(function(resolve, reject) {
+		if (undefined === configuration) {
+			reject(Error('AmberCompiler.check_configuration_ok(): missing configuration object'));
+		}
+
+		if (0 === configuration.jsFiles.length && 0 === configuration.stFiles.length) {
+			reject(Error('AmberCompiler.check_configuration_ok(): no files to compile/link specified in configuration object'));
+		}
+
+		resolve(configuration);
+	});
+};
+
+
+/**
+ * Check if the file given as parameter exists in any of the following directories:
+ *  1. current local directory
+ *  2. configuration.jsLibraryDirs
+ *  3. $AMBER/src/
+ *  3. $AMBER/support/
+ *
+ * @param filename name of a file without '.js' prefix
+ * @param configuration the main amberc configuration object
+ */
+function resolve_js(filename, configuration) {
+	var baseName = path.basename(filename, '.js');
+	var jsFile = baseName + configuration.loadsuffix + '.js';
+	return resolve_file(jsFile, configuration.jsLibraryDirs);
+};
+
+
+/**
+ * Check if the file given as parameter exists in any of the following directories:
+ *  1. current local directory
+ *  2. $AMBER/
+ *
+ * @param filename name of a .st file
+ * @param configuration the main amberc configuration object
+ */
+function resolve_st(filename, configuration) {
+	return resolve_file(filename, [configuration.amber_dir]);
+};
+
+
+/**
+ * Resolve the location of a file given as parameter filename.
+ * First check if the file exists at given location,
+ * then check in each of the directories specified in parameter searchDirectories.
+ */
+function resolve_file(filename, searchDirectories) {
+	return new Promise(function(resolve, reject) {
+		console.log('Resolving: ' + filename);
+		fs.exists(filename, function(exists) {
+			if (exists) {
+				resolve(filename);
+			} else {
+				var alternativeFile = '';
+				// check for filename in any of the given searchDirectories
+				var found = searchDirectories.some(function(directory) {
+					alternativeFile = path.join(directory, filename);
+					return fs.existsSync(alternativeFile);
+				});
+				if (found) {
+					resolve(alternativeFile);
+				} else {
+					reject(Error('File not found: ' + alternativeFile));
+				}
+			}
+		});
+	});
+};
+
+
+/**
+ * Resolve st files given by stFiles and add them to configuration.compile.
+ * Returns a Promise which resolves into the configuration object.
+ */
+function collect_st_files(configuration) {
+	return Promise.all(
+		configuration.stFiles.map(function(stFile) {
+			return resolve_st(stFile, configuration);
+		})
+	)
+	.then(function(data) {
+		configuration.compile = configuration.compile.concat(data);
+		return configuration;
+	});
+}
+
+
+/**
+ * Resolve js files given by jsFiles and add them to configuration.libraries.
+ * Returns a Promise which resolves into the configuration object.
+ */
+function collect_js_files(configuration) {
+	return Promise.all(
+		configuration.jsFiles.map(function(file) {
+			return resolve_js(file, configuration);
+		})
+	)
+	.then(function(data) {
+		configuration.libraries = configuration.libraries.concat(data);
+		return configuration;
+	});
+}
+
+
+/**
+ * Resolve .js files needed by kernel.
+ * Returns a Promise which resolves into the configuration object.
+ */
+function resolve_kernel(configuration) {
+	var kernel_files = configuration.kernel_libraries.concat(configuration.load);
+	return Promise.all(
+		kernel_files.map(function(file) {
+			return resolve_js(file, configuration);
+		})
+	)
+	.then(function(data) {
+		// boot.js and Kernel files need to be used first
+		// otherwise the global objects 'vm' and 'globals' are undefined
+		configuration.libraries = data.concat(configuration.libraries);
+		return configuration;
+	});
+}
+
+
+/**
+ * Resolve .js files needed by compiler, read and eval() them.
+ * The finished Compiler gets stored in configuration.{vm,globals}.
+ * Returns a Promise object which resolves into the configuration object.
+ */
+function create_compiler(configuration) {
+	var compiler_files = configuration.compiler_libraries;
+	var include_files = configuration.load;
+	var builder;
+	return Promise.all(
+		compiler_files.map(function(file) {
+			return resolve_js(file, configuration);
+		})
+	)
+	.then(function(compilerFilesArray) {
+		return Promise.all(
+			compilerFilesArray.map(function(file) {
+				return new Promise(function(resolve, reject) {
+					console.log('Loading file: ' + file);
+					fs.readFile(file, function(err, data) {
+						if (err)
+							reject(err);
+						else
+							resolve(data);
+					});
+				});
+			})
+		)
+	})
+	.then(function(files) {
+		builder = createConcatenator();
+		builder.add('(function() {');
+		builder.start();
+
+		files.forEach(function(data) {
+			// data is an array where index 0 is the error code and index 1 contains the data
+			builder.add(data);
+			// matches and returns the "module_id" string in the AMD definition: define("module_id", ...)
+			var match = ('' + data).match(/^define\("([^"]*)"/);
+			if (match) {
+				builder.addId(match[1]);
+			}
+		});
+	})
+	.then(function () { return Promise.all(
+		include_files.map(function(file) {
+			return resolve_js(file, configuration);
+		})
+	); })
+	.then(function(includeFilesArray) {
+		return Promise.all(
+			includeFilesArray.map(function(file) {
+				return new Promise(function(resolve, reject) {
+					console.log('Loading library file: ' + file);
+					fs.readFile(file, function(err, data) {
+						if (err)
+							reject(err);
+						else
+							resolve(data);
+					});
+				});
+			})
+		)
+	})
+	.then(function(files) {
+		var loadIds = [];
+		files.forEach(function(data) {
+			// data is an array where index 0 is the error code and index 1 contains the data
+			builder.add(data);
+			// matches and returns the "module_id" string in the AMD definition: define("module_id", ...)
+			var match = ('' + data).match(/^define\("([^"]*)"/);
+			if (match) {
+				loadIds.push(match[1]);
+			}
+		});
+		// store the generated smalltalk env in configuration.{vm,globals}
+		builder.finish('configuration.vm = vm; configuration.globals = globals;');
+		loadIds.forEach(function (id) {
+			builder.add('requirejs("' + id + '");');
+		});
+		builder.add('})();');
+
+		eval(builder.toString());
+
+		console.log('Compiler loaded');
+
+		configuration.globals.ErrorHandler._register_(configuration.globals.RethrowErrorHandler._new());
+
+		if(0 !== configuration.jsGlobals.length) {
+			var jsGlobalVariables = configuration.vm.globalJsVariables;
+			jsGlobalVariables.push.apply(jsGlobalVariables, configuration.jsGlobals);
+		}
+
+		return configuration;
+	});
+}
+
+
+/**
+ * Compile all given .st files by importing them.
+ * Returns a Promise object that resolves into the configuration object.
+ */
+function compile(configuration) {
+	// return function which does the actual work
+	// and use the compile function to reference the configuration object
+	return Promise.all(
+		configuration.compile.map(function(stFile) {
+			return new Promise(function(resolve, reject) {
+				if (/\.st/.test(stFile)) {
+					console.ambercLog('Reading: ' + stFile);
+					fs.readFile(stFile, 'utf8', function(err, data) {
+						if (!err)
+							resolve(data);
+						else
+							reject(Error('Could not read: ' + stFile));
+					});
+				}
+			});
+		})
+	)
+	.then(function(fileContents) {
+		console.log('Compiling collected .st files');
+		// import/compile content of .st files
+		return Promise.all(
+			fileContents.map(function(code) {
+				return new Promise(function(resolve, reject) {
+					var importer = configuration.globals.Importer._new();
+					try {
+						importer._import_(code._stream());
+						resolve(true);
+					} catch (ex) {
+						reject(Error("Compiler error in section:\n" +
+							importer._lastSection() + "\n\n" +
+							"while processing chunk:\n" +
+							importer._lastChunk() + "\n\n" +
+							(ex._messageText && ex._messageText() || ex.message || ex))
+						);
+					}
+				});
+			})
+		);
+	})
+	.then(function () {
+		return configuration;
+	});
+}
+
+
+/**
+ * Export compiled categories to JavaScript files.
+ * Returns a Promise() that resolves into the configuration object.
+ */
+function category_export(configuration) {
+	return Promise.all(
+		configuration.compile.map(function(stFile) {
+			return new Promise(function(resolve, reject) {
+				var category = path.basename(stFile, '.st');
+				var jsFilePath = configuration.output_dir;
+				if (undefined === jsFilePath) {
+					jsFilePath = path.dirname(stFile);
+				}
+				var jsFile = category + configuration.suffix_used + '.js';
+				jsFile = path.join(jsFilePath, jsFile);
+				configuration.compiled.push(jsFile);
+				var smalltalkGlobals = configuration.globals;
+				var packageObject = smalltalkGlobals.Package._named_(category);
+				packageObject._transport()._namespace_(configuration.amd_namespace);
+				fs.writeFile(jsFile, smalltalkGlobals.String._streamContents_(function (stream) {
+					smalltalkGlobals.AmdExporter._new()._exportPackage_on_(packageObject, stream);
+				}), function(err) {
+					if (err)
+						reject(err);
+					else
+						resolve(true);
+				});
+			});
+		})
+	)
+	.then(function() {
+		return configuration;
+	});
+}
+
+
+/**
+ * Verify if all .st files have been compiled.
+ * Returns a Promise() that resolves into the configuration object.
+ */
+function verify(configuration) {
+	console.log('Verifying if all .st files were compiled');
+	return Promise.all(
+		configuration.compiled.map(function(file) {
+			return new Promise(function(resolve, reject) {
+				fs.exists(file, function(exists) {
+					if (exists)
+						resolve(true);
+					else
+						reject(Error('Compilation failed of: ' + file));
+				});
+			});
+		})
+	)
+	.then(function() {
+		return configuration;
+	});
+}
+
+
+/**
+ * Synchronous function.
+ * Concatenates compiled JavaScript files into one file in the correct order.
+ * The name of the produced file is given by configuration.program.
+ * Returns a Promise which resolves into the configuration object.
+ */
+function compose_js_files(configuration) {
+	return new Promise(function(resolve, reject) {
+		var programFile = configuration.program;
+		if (undefined === programFile) {
+			resolve(configuration);
+			return;
+		}
+		if (undefined !== configuration.output_dir) {
+			programFile = path.join(configuration.output_dir, programFile);
+		}
+
+		var program_files = [];
+		if (0 !== configuration.libraries.length) {
+			console.log('Collecting libraries: ' + configuration.libraries);
+			program_files.push.apply(program_files, configuration.libraries);
+		}
+
+		if (0 !== configuration.compiled.length) {
+			var compiledFiles = configuration.compiled.slice(0);
+
+			console.log('Collecting compiled files: ' + compiledFiles);
+			program_files.push.apply(program_files, compiledFiles);
+		}
+
+		console.ambercLog('Writing program file: %s.js', programFile);
+
+		var fileStream = fs.createWriteStream(programFile + configuration.suffix_used + '.js');
+		fileStream.on('error', function(error) {
+			fileStream.end();
+			console.ambercLog(error);
+			reject(error);
+		});
+
+		fileStream.on('close', function(){
+			resolve(configuration);
+		});
+
+		var builder = createConcatenator();
+		builder.add('#!/usr/bin/env node');
+		builder.start();
+
+		program_files.forEach(function(file) {
+			if(fs.existsSync(file)) {
+				console.log('Adding : ' + file);
+				var buffer = fs.readFileSync(file);
+				// matches and returns the "module_id" string in the AMD define: define("module_id", ...)
+				var match = buffer.toString().match(/^define\("([^"]*)"/);
+				if (match /*&& match[1].slice(0,9) !== "amber_vm/"*/) {
+					builder.addId(match[1]);
+				}
+				builder.add(buffer);
+			} else {
+				fileStream.end();
+				reject(Error('Can not find file ' + file));
+			}
+		});
+
+		var mainFunctionOrFile = '';
+
+		if (undefined !== configuration.main) {
+			console.log('Adding call to: %s>>main', configuration.main);
+			mainFunctionOrFile += 'globals.' + configuration.main + '._main();';
+		}
+
+		if (undefined !== configuration.mainfile && fs.existsSync(configuration.mainfile)) {
+			console.log('Adding main file: ' + configuration.mainfile);
+			mainFunctionOrFile += '\nvar smalltalk = vm; // backward compatibility\n' + fs.readFileSync(configuration.mainfile);
+		}
+
+		builder.finish(mainFunctionOrFile);
+
+		console.log('Writing...');
+		builder.forEach(function (element) {
+			fileStream.write(element);
+			fileStream.write('\n');
+		});
+		console.log('Done.');
+		fileStream.end();
+	});
+}
+
+
+module.exports.Compiler = AmberCompiler;
+module.exports.createDefaultConfiguration = createDefaultConfiguration;

+ 21 - 0
external/amber-dev/package.json

@@ -0,0 +1,21 @@
+{
+  "name": "amber-dev",
+  "version": "0.0.3",
+  "description": "Development goodies for Amber Smalltalk",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git://github.com/amber-smalltalk/amber.git"
+  },
+  "author": "",
+  "license": "MIT",
+  "bugs": {
+    "url": "https://github.com/amber-smalltalk/amber/issues?labels=devkit"
+  },
+  "dependencies": {
+    "amdefine": "0.0.8",
+    "es6-promise": "~0.1.1"
+  }
+}

+ 137 - 0
external/amber-dev/tasks/grunt-amberc.js

@@ -0,0 +1,137 @@
+module.exports = function(grunt) {
+
+  var path = require('path');
+  var fs = require('fs');
+  var amberc = require('../lib/amberc.js');
+
+  /**
+     A full example entry for a Gruntfile.js is available below.
+     Please note that the verbose level is either specified globally
+     or on a target specific level.
+     However, it can additionally be triggered on the commandline by
+     adding the '-v' or '--verbose' flag.
+
+     Example Gruntfile.js entry:
+
+     amberc: {
+       options: {
+         amber_dir: process.cwd(),                // REQUIRED
+         library_dirs: ['dir1', '/usr/local/js'], // optional
+         verbose: true                            // optional
+       },
+       helloWorld: {
+         // this 'options' object is optional as well as all parameters inside it
+         // they can be used to override the global 'options'
+         options: {
+           library_dirs: ['dir1', '/usr/local/js'], // optional
+           verbose: true
+         },
+         src: ['projects/HelloWorld/src/HelloWorld.st'], // REQUIRED
+         output_dir: 'projects/HelloWorld/src',  // optional
+         libraries: 'Web',                       // optional
+         jsGlobals: ['global1', 'global2'],     // optional
+         main_class: 'HelloWorld',              // optional
+         output_name: 'helloWorld',             // optional
+         amd_namespace: 'MyNamespace',          // optional (default: 'amber')
+         main_file: 'myMain.js',                // optional
+         output_suffix: 'mySuffix',             // optional
+         library_suffix: '-0.9'                 // optional
+       },
+     },
+
+   */
+  grunt.registerMultiTask('amberc', 'Compile Smalltalk files with the amberc compiler', function() {
+    // mark task as async task
+    var done = this.async();
+
+    var options = this.options({
+      amber_dir: undefined,
+      library_dirs: [],
+      verbose: grunt.option('verbose') || false
+    });
+    this.data.verbose = options.verbose;
+    this.data.library_dirs = options.library_dirs;
+
+    // mark required properties
+    this.requiresConfig('amberc.options.amber_dir');
+    // raise error on missing source files
+    if (this.filesSrc.length === 0) {
+        grunt.fail.fatal('No source files to compile or link.');
+    }
+
+    // create and initialize amberc
+    var compiler = new amberc.Compiler(grunt.config('amberc.options.amber_dir'));
+
+    // generate the amberc configuration out of the given target properties
+    var configuration = generateCompilerConfiguration(this.data, this.filesSrc);
+
+    // run the compiler and call the async callback once finished
+    var self = this;
+    compiler.main(configuration, function(){
+      // signal that task has finished
+      done();
+    });
+  });
+
+
+  function generateCompilerConfiguration(data, sourceFiles) {
+    var configuration = amberc.createDefaultConfiguration();
+    var parameters = [];
+
+    var libraries = data.libraries;
+    if (undefined !== libraries) {
+      configuration.load = libraries;
+    }
+    var library_dirs = data.library_dirs;
+    if (undefined !== library_dirs) {
+      configuration.jsLibraryDirs = library_dirs;
+    }
+    var mainClass = data.main_class;
+    if (undefined !== mainClass) {
+      configuration.main = mainClass;
+    }
+    var mainFile = data.main_file;
+    if (undefined !== mainFile) {
+      configuration.mainfile = mainFile;
+    }
+    var outputSuffix = data.output_suffix;
+    if (undefined !== outputSuffix) {
+      configuration.suffix = outputSuffix;
+      configuration.suffix_used = outputSuffix;
+    }
+    var librarySuffix = data.library_suffix;
+    if (undefined !== librarySuffix) {
+      configuration.loadsuffix = librarySuffix;
+      configuration.suffix_used = librarySuffix;
+    }
+    if (undefined !== sourceFiles) {
+      sourceFiles.forEach(function(currentItem){
+        var fileSuffix = path.extname(currentItem);
+        switch (fileSuffix) {
+          case '.st':
+            configuration.stFiles.push(currentItem);
+          break;
+          case '.js':
+            configuration.jsFiles.push(currentItem);
+          break;
+        }
+      });
+    }
+    var outputName = data.output_name;
+    if (undefined !== outputName) {
+      configuration.program = outputName;
+    }
+    var amdNamespace = data.amd_namespace;
+    if (undefined !== amdNamespace) {
+      configuration.amd_namespace = amdNamespace;
+    }
+    if (undefined !== data.output_dir) {
+      configuration.output_dir = data.output_dir;
+    }
+    if (undefined !== data.jsGlobals) {
+      configuration.jsGlobals.push.apply(configuration.jsGlobals, data.jsGlobals);
+    }
+    configuration.verbose = data.verbose;
+    return configuration;
+  }
+};