I have a nw.js native application with angular.js inside. My app bundled with webpack and contains native node.js modules. My entry point is index.js file that I organized like this:
var gui = require('nw.gui');
var angular = require('angular');
require('./app.css');
// other modules
var myApp = angular.module('myApp', [
'ngRaven',
'ngMaterial',
'ngMessages'
]).constant(
'fs', require('fs')
)
require('./services')(myApp);
require('./directives')(myApp);
require('./factories')(myApp);
require('./filters')(myApp);
require('./controllers')(myApp);
require('./app.js')(myApp);
My webpack config looks like this:
const path = require('path');
const config = {
entry: [
'./app/index.js'
],
output: {
path: path.resolve(__dirname, 'app'),
filename: 'bundle.js'
},
devtool: "source-map",
target: 'node-webkit',
module:{
// css, html loaders
},
node: {
os: true,
fs: true,
child_process: true,
__dirname: true,
__filename: true
}
};
module.exports = config;
So every dependency include Node.js native modules like fs, path, child_process bundled in one big file bundle.js that i include in html and then package my nw.js app. So my app structure looks like:
my_project:
--app
----controllers
------welcome
--------welcome.js // Page controller
--------welcome.html // Page HTML
------index.js // here I include each page controller
----app.js // My angular app initialization
----index.js // here I include all dependencies
I'm trying to run tests with this structure. I tried karma+jasmine, karma+mocha, tried different configurations, my last one looks like:
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine'],
files: [
'app/bundle.js',
'app/**/*spec.js'
],
exclude: [],
preprocessors: {
'app/bundle.js': ['webpack']
},
reporters: ['progress'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['ChromeHeadlessNoSandbox'],
customLaunchers: {
ChromeHeadlessNoSandbox: {
base: 'ChromeHeadless',
flags: ['--no-sandbox']
}
},
singleRun: true,
webpack: {
// you don't need to specify the entry option because
// karma watches the test entry points
// webpack watches dependencies
// ... remainder of webpack configuration (or import)
},
webpackMiddleware: {
// webpack-dev-middleware configuration
// i.e.
noInfo: true,
// and use stats to turn off verbose output
stats: {
// options i.e.
chunks: false
}
}
});
};
But my tests still not see the angular application.
describe('Welcome page', function() {
beforeEach(angular.mock.module('WelcomePageCtrl'));
});
P.S I don't require exactly karma and jasminne, so any solution will be appreciated. I just want to cover my project with tests
I have gone through something similar myself. I don't think you need the bundle.js for your tests.
Here is how would do it:
I assume your controller/service/etc implementation is as follows:
/* app/controller/welcome.js */
module.exports = function(app) {
return app.controller("MyCtrl", function($scope) {
$scope.name = "lebouf";
});
};
I like my test code to sit right beside the code I'm testing (Welcome.spec.js in the same directory as Welcome.js). That test code would look like so:
/* app/controller/welcome.spec.js */
beforeEach(function() {
var app = angular.module("myApp", []); //module definition
require("./welcome")(app);
});
beforeEach(mockModule("myApp"));
describe("MyCtrl", () => {
var scope = {};
beforeEach(mockInject(function($controller) {
$controller("MyCtrl", {
$scope: scope
});
}));
it("has name in its scope", function() {
expect(scope.name).to.equal("not lebouf"); //the test will fail
});
});
Except, this is an angular controller we're testing and it's not that simple. We need the angular object itself. So lets set it up. I'll explain why it is done how it is done next:
/* test-setup.js */
const jsdom = require("jsdom").jsdom; //v5.6.1
const chai = require("chai");
global.document = jsdom("<html><head></head><body></body></html>", {});
global.window = document.defaultView;
global.window.mocha = true;
global.window.beforeEach = beforeEach;
global.window.afterEach = afterEach;
require("angular/angular");
require("angular-mocks");
global.angular = window.angular;
global.mockInject = angular.mock.inject;
global.mockModule = angular.mock.module;
global.expect = chai.expect;
console.log("ALL SET");
And we'll run the tests as:
node_modules/.bin/mocha ./init.js app/**/*.spec.js
#or preferably as `npm test` by copying the abev statement into package.json
extra info
Here is how init.js is setup as is:
jsdom: f you require("angular/angular") you'll see that it needs a window instance. jsdom can create documents and windows and so on without a web browser!
window.mocha: we need angular-mocks to populate our angular with the necessary utilities. But if you look at the code you'll notice that window.mocha || window.jasmine needs to be true. Thats whywindow.mocha = true`
window.beforeEach, window.afterEach: the same reason as above; because angular-mocks.js demands it.
I set some global variables that I plan to use commonly in my tests: angular, expect, mockInject, mockModule.
Also these may provide some additional information:
https://kasperlewau.github.io/post/angular-without-karma/
https://gist.github.com/rikukissa/dcb422eb3b464cc184ae
Related
I have some repositories (A, B, C and TestRepo) all of them in local, i get karma to load spec files from repos A or B or C with a param (node script).
Now im trying to transpile the files cause some test are using import so i need babel. I configured babel as a preprocessor and it seems like it is working but know i get this error:
Uncaught Error: Module name "builder" has not been loaded yet for context: _. Use require([])
I tried all configs i know but still not working.
My karma.config.js is this:
module.exports = function(config) {
config.set({
basePath: '',
frameworks: ['jasmine', 'requirejs'],
files: [
'test-main.js'
],
exclude: [
],
preprocessors: {
},
reporters: ['progress'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
concurrency: Infinity
});
}
My test-main.js for requirejs:
var allTestFiles = []
var TEST_REGEXP = /(spec|test)\.js$/i
Object.keys(window.__karma__.files).forEach(function (file) {
if (TEST_REGEXP.test(file)) {
var normalizedTestModule = file.replace(/^\/base\/|\.js$/g, '')
allTestFiles.push(normalizedTestModule)
}
})
require.config({
baseUrl: '/base',
deps: allTestFiles,
callback: window.__karma__.start
})
And my node file, where i set karma files, preprocessor and so on base on the repository i got from params is this:
const cfg = require('karma').config;
const stopper = require('karma').stopper;
const runner = require('karma').runner;
const Server = require('karma').Server;
const path = require('path');
const REPOSITORY = process.argv[2] || '';
let karmaConfig = cfg.parseConfig(path.resolve('./karma.conf.js'), { port: 9876 } );
let BASE_PATH;
if (REPOSITORY !== 'all') {
BASE_PATH = `/Users/fernando.delolmo/${REPOSITORY}`;
karmaConfig.files.push({pattern: BASE_PATH + '/test/**/*.spec.js'});
karmaConfig.files.push({pattern: BASE_PATH + '/src/**/*.js', included: true});
karmaConfig.preprocessors[BASE_PATH + '/src/**/*.js'] = ['babel'];
karmaConfig.preprocessors[BASE_PATH + '/test/**/*.spec.js'] = ['babel'];
karmaConfig.exclude.push(BASE_PATH + '/node_modules/**/*.spec.js');
} else {
BASE_PATH = '/Users/fernando.delolmo/';
karmaConfig.files.push({pattern: BASE_PATH + '**/test/**/*.spec.js'});
karmaConfig.files.push({pattern: BASE_PATH + '**/src/**/*.js', included: true});
karmaConfig.preprocessors[BASE_PATH + '**/src/**/*.js'] = ['babel'];
karmaConfig.preprocessors[BASE_PATH + '**/test/**/*.spec.js'] = ['babel'];
karmaConfig.exclude.push(BASE_PATH + '**/node_modules/**/*.spec.js');
}
karmaConfig.basePath = BASE_PATH;
var server = new Server(karmaConfig, function(exitCode) {
process.exit(exitCode);
});
server.start();
server.on('browser_start', function() {
runner.run(karmaConfig, function(exitCode) {
process.exit(exitCode);
});
});
server.on('browser_complete', function() {
stopper.stop(karmaConfig, function(exitCode) {
process.exit(exitCode);
});
});
Its my first time configuring karma, requirejs and babel all together so any help will be great :)
Okay finally i got what's the problem here. Just in case some one get here finding any solution to problems like this.
If you want to run test with ES2015 modules with karma, you will use babel to transpile into CommonJS modules at first, good! but... you will get errors like export is undefined.. and.. why?? Simple, export/import still not supported on the browsers, this means you CAN'T run CommonJS modules on the browser without any module bundler (browersify or webpack).
So, keep this clear:
1st -> Babel only transpile your ES2015 code and test into CommonJS modules
2nd -> Webpack / Browersify get CommonJS modules and bundle them into a bundle.js and then you can run that on the browser with karma.
I was using webpack+aurelia for a project and recently switch to JSPM version 0.16.52. I use jasmine's spyOn for utility or factory functions that are imported into my classes. However tests that have spyOn(object, 'method') have this error:
"Module Exports Cannot Be Changed Externally"
This occurs when jasmine sets the spied method on the module obj[methodName] = spiedMethod'
Here is a very simple example:
test.js
import * as env from '../../src/env';
describe('stackoverflow example', () => {
it('runs the test', () => {
// this gets thrown and swallowed. jasmine does not report an error, but does not increase the test count.
let envSpy = spyOn(env, 'getBaseUrl');
});
});
env.js
export function getBaseUrl(location) {
return location.port ? `http://${location.hostname}:9001/api/`: '/api/';
}
karam.conf.js
'use strict'
const path = require('path')
module.exports = function(config) {
config.set({
// base path that will be used to resolve all patterns
basePath: __dirname,
// frameworks to use
frameworks: ['jspm', 'jasmine'],
// list of files / patterns to load in the browser
files: [],
// list of files to exclude
exclude: [],
jspm: {
// Edit this to your needs
loadFiles: [
'test/unit/setup.js',
'test/unit/**/*.js'
],
serveFiles: [
'src/**/*.*',
'jspm_packages/system-polyfills.js'
],
paths: {
'*': 'src/*',
'test/*': 'test/*',
'github:*': 'jspm_packages/github/*',
'npm:*': 'jspm_packages/npm/*'
}
},
// preprocess matching files before serving them to the browser
preprocessors: {
'test/**/*.js': ['babel'],
'src/**/*.js': ['babel']
},
'babelPreprocessor': {
options: {
sourceMap: 'inline',
presets: [ ['es2015', { loose: true }], 'stage-1'],
plugins: [
'syntax-flow',
'transform-decorators-legacy',
'transform-flow-strip-types',
[ 'istanbul', { 'ignore': 'test/' } ]
]
}
},
// test results reporter to use
reporters: ['mocha'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// start these browsers
browsers: ['Chromium'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: false
});
};
setup,js
import 'babel-polyfill'; // for async/await
import 'aurelia-polyfills';
import {initialize} from 'aurelia-pal-browser';
initialize();
UPDATE
When I pulled down a fresh copy of Aurelia-esnext I did not receive the error. So commented out all my tests in my production app and the first spyOn worked, but as i added more tests I started recieving the error again, and it seems to be the way the module is loaded by systemjs. Below is how the imported module object looks when it is loaded into the test
Fresh Copy does not throw
{
__esModule: true,
getBaseUrl: (),
__proto__: Object
}
Original that throws
{
__esModule: (...)
get __esModule: ()
set __esModule: ()
getBaseUrl: (...)
get getBaseUrl: ()
set getBsaeUrl: ()
_proto__: Object
}
Update
I found the snippet of code in system.js where the difference in import occurs. In my larger original project loader.defined[name]; returned undefined and the fresh copy of aurelia loader.defined[name]; is defined...still have not found a workaround
system.src.js
function getModule(name, loader) {
var exports;
var entry = loader.defined[name];
if (!entry) {
exports = loader.get(name);
if (!exports)
throw new Error('Unable to load dependency ' + name + '.');
}
else {
if (entry.declarative)
ensureEvaluated(name, entry, [], loader);
else if (!entry.evaluated)
linkDynamicModule(entry, loader);
exports = entry.module.exports;
}
if ((!entry || entry.declarative) && exports && exports.__useDefault)
return exports['default'];
return exports;
}
I want to work with closure compiler, so I added grunt-closure-tools to my Grunt config, but my config is erroring with:
Verifying property closureCompiler.loader exists in config...ERROR
Here is the reference material for grunt-closure-tools:
https://www.npmjs.com/package/grunt-closure-tools
or
https://github.com/thanpolas/grunt-closure-tools
Here is my GruntFile.js:
module.exports = function(grunt) {
var path = require('path');
require('load-grunt-config')(grunt, {
//pkg: grunt.file.readJSON('package.json'),
configPath: path.join(process.cwd(), 'grunt'), //path to task.js files, defaults to grunt dir
init: true, //auto grunt.initConfig
data: { //data passed into config. Can use with <%= test %>
pkg: require('./package.json')
},
loadGruntTasks: { //can optionally pass options to load-grunt-tasks. If you set to false, it will disable auto loading tasks.
pattern: ['grunt-contrib-*', 'grunt-jslint', 'grunt-newer', 'imagemin-*','grunt-closure-tools'],
scope: 'devDependencies'
},
postProcess: function(config) {} //can post process config object before it gets passed to grunt
});
//require('load-grunt-tasks')(grunt);
grunt.registerTask("default", ["newer:jslint", "newer:concat", "closureCompiler:loader", "newer:sass"]);
};
I am using load-grunt-config to break up my config into multiple parts. Here is my closure.js file, mostly modeled on grunt-closure-tools github page example:
module.exports = {
options: {
compilerFile: '/usr/local/Cellar/closure-compiler/20141023/libexec/build/compiler.jar',
checkModified: true,
compilerOpts: {
create_source_map: null,
compilation_level: 'ADVANCED_OPTIMIZATIONS',
},
d32: true, // will use 'java -client -d32 -jar compiler.jar'
},
util: {
src: 'includes/javascript/util/util.js',
dest: 'includes/javascript/build/util.min.js'
},
loader: {
src : 'includes/javascript/loaders/loader.js',
dest: 'includes/javascript/build/loader.min.js'
}
};
Any help with this error is appreciated.
After some digging and no small amount of thrashing of teeth, I have managed to getting my config working. My Gruntfile.js:
module.exports = function(grunt) {
var path = require('path');
require('load-grunt-config')(grunt, {
//pkg: grunt.file.readJSON('package.json'),
configPath: path.join(process.cwd(), 'grunt'), //path to task.js files, defaults to grunt dir
init: true, //auto grunt.initConfig
data: { //data passed into config. Can use with <%= test %>
pkg: require('./package.json')
},
loadGruntTasks: { //can optionally pass options to load-grunt-tasks. If you set to false, it will disable auto loading tasks.
pattern: ['grunt-contrib-*', 'grunt-jslint', 'grunt-newer', 'imagemin-*','grunt-closure-tools'],
scope: 'devDependencies'
},
postProcess: function(config) {} //can post process config object before it gets passed to grunt
});
//require('load-grunt-tasks')(grunt);
grunt.registerTask("default", ["newer:jslint", "newer:concat", "closureCompiler:loader", "newer:sass"]);
};
and my closureCompiler.js file (shortened for clarity):
module.exports = {
options: {
compilerFile: '/usr/local/Cellar/closure-compiler/20141023/libexec/build/compiler.jar',
checkModified: true,
compilerOpts: {
// create_source_map: null
},
execOpts: {
maxBuffer: 999999 * 1024
},
TieredCompilation: true // will use 'java -server -XX:+TieredCompilation -jar compiler.jar'
},
loader: {
TEMPcompilerOpts: {
create_source_map: 'includes/javascript/build/loader.min.js.map'
},
src : 'includes/javascript/loaders/loader.js',
dest: 'includes/javascript/build/loader.min.js'
}
};
I had quite a few errors in my config:
I had to rename closure.js to closureCompiler.js so load-grunt-config could find the file. Oops, my bad.
The d32: true switch on the Closure Compiler means use 32-bit JVM, which I do not have on my system. I switched to TieredCompilation: true instead.
create_source_map: null in the options did not work for me. I found the reference in the source code of grunt-closure-tools but didn't spend the time to sort out what was wrong. Instead, I added TEMPcompilerOpts to the target config with the map file I wanted Closure to make.
I hope other benefit from my experience with this solution. From this point, I am going to use the ability of Closure Compiler to create source maps to merge my dev and prod build targets into a single target.
Despite some people having the same issues (like [here][1] or [there][2]), I do not succeed to test my directive in my Angular (1.2.25) application.
Here is my project structure:
myapp
+- src/main/java/resources/META-INF/resources/workflow/directives
| +- directives.js
| +- *.html (all templates)
+- src/test/javascript
+- karma.conf.js
+- spec/directives
+- text-input.spec.js
(yes, not a good structure, but my Angular application is stuck in a Java project)
My karma configuration:
// Karma configuration
module.exports = function (config) {
config.set({
...
// base path, that will be used to resolve files and exclude
basePath: '',
// testing framework to use (jasmine/mocha/qunit/...)
frameworks: ['jasmine'],
// list of files / patterns to load in the browser
files: [
// Third parties dependencies: jQuery, Angular, Angular modules, Angular mocks
'../../main/resources/META-INF/resources/workflow/bower_components/...',
// My directives
'../../main/resources/META-INF/resources/workflow/directives/*.html',
'../../main/resources/META-INF/resources/workflow/directives/*.js',
// My application
'../../main/resources/META-INF/resources/workflow/scripts/*.js',
'../../main/resources/META-INF/resources/workflow/app/**/*.js',
// My Test files
'spec/directives/*.js'
],
// list of files / patterns to exclude
exclude: [],
// web server port
port: 8888,
browsers: [ 'Chrome' ],
// Which plugins to enable
plugins: [
'karma-ng-html2js-preprocessor',
'karma-chrome-launcher',
'karma-jasmine'
],
preprocessors: {
'../../main/resources/META-INF/resources/workflow/directives/*.html': [ 'ng-html2js' ]
},
ngHtml2JsPreprocessor: {
// Not sure what to put here...
},
...
});
};
My test:
describe('directive: text-input', function() {
var element, scope;
beforeEach(module('myApp'));
beforeEach(inject(function($rootScope, $compile) {
scope = $rootScope.$new();
element = '<div my-input-text data-label="Foo" data-model="bar"></div>';
element = $compile(element)(scope);
scope.$digest();
}));
describe('basics tests', function() {
it('should be editable', function () {
expect(element.text()).toBe('Foo');
});
});
});
And the directive itself:
var myDirs = angular.module('my-directives', []);
// Text input
myDirs.directive('myInputText', function () {
return {
replace: true,
templateUrl: 'directives/text-input.html',
scope: {
label: '=',
readOnly: '=',
code: '=',
model: '='
}
};
});
When running the tests (grunt karma), I get that error:
Chrome 31.0.1650 (Windows 7) directive: text-input basics tests should be editable FAILED
Error: Unexpected request: GET directives/text-input.html
No more request expected
I still don't get what I do wrong in my preprocessor. I've tried a lot of different configuration in the ngHtml2JsPreprocessor, but the error is always the same.
I saw in the DEBUG logs that the pre processor is working on my template HTML files:
DEBUG [preprocessor.html2js]: Processing "d:/dev/my-app/src/main/resources/META-INF/resources/workflow/directives/text-input.html".
Thanks.
I finally found a solution.
In my karma.conf.js, I set a module-name, like that:
ngHtml2JsPreprocessor: {
moduleName: 'my-directives'
},
then, in my Jasmine test, I add it:
beforeEach(module('myApp'));
beforeEach(module('my-directives'));
Another solution is to directly set the HTML file as a module without changing the karma.conf.js:
beforeEach(module('directives/text-input.html'));
But not a good solution as I have dozen of directives/*.html...
I am using the npm modules grunt env and load-grunt-config in my project. grunt env handles environment variables for you, while load-grunt-config handles, well, loads the grunt configuration for you. You can put your tasks into other files, then load-grunt-config will bundle them up and have grunt load & consume them for you. You can also make an aliases.js file, with tasks you want to combine together into one task, running one after another. It's similar to the grunt.registerTask task in the original Gruntfile.js. I put all my grunt tasks inside a separate grunt/ folder under the root folder with the main Gruntfile, with no extra subfolders, as suggested by the load-grunt-config README.md on Github. Here is my slimmed-down Gruntfile:
module.exports = function(grunt) {
'use strict';
require('time-grunt')(grunt);
// function & property declarations
grunt.initConfig({
pkg: grunt.file.readJSON('package.json')
});
require('load-grunt-config')(grunt, {
init: true,
loadGruntConfig: {
scope: 'devDependencies',
pattern: ['grunt-*', 'time-grunt']
}
});
};
In theory, setting all these files up the correct way for load-grunt-config to load should be exactly the same as just having a Gruntfile.js. However, I seem to have run into a little snag. It seems the environment variables set under the env task do not get set for the subsequent grunt tasks, but are set by the time node processes its tasks, in this case an express server.
grunt env task:
module.exports = {
// environment variable values for developers
// creating/maintaining site
dev: {
options: {
add: {
NODE_ENV: 'dev',
MONGO_PORT: 27017,
SERVER_PORT: 3000
}
}
}
};
grunt-shell-spawn task:
// shell command tasks
module.exports = {
// starts up MongoDB server/daemon
mongod: {
command: 'mongod --bind_ip konneka.org --port ' + (process.env.MONGO_PORT || 27017) + ' --dbpath C:/MongoDB/data/db --ipv6',
options: {
async: true, // makes this command asynchronous
stdout: false, // does not print to the console
stderr: true, // prints errors to the console
failOnError: true, // fails this task when it encounters errors
execOptions: {
cwd: '.'
}
}
}
};
grunt express task:
module.exports = {
// default options
options: {
hostname: '127.0.0.1', // allow connections from localhost
port: (process.env.SERVER_PORT || 3000), // default port
},
prod: {
options: {
livereload: true, // automatically reload server when express pages change
// serverreload: true, // run forever-running server (do not close when finished)
server: path.resolve(__dirname, '../backend/page.js'), // express server file
bases: 'dist/' // watch files in app folder for changes
}
}
};
aliases.js file (grunt-load-config's way of combining tasks so they run one after the other):
module.exports = {
// starts forever-running server with "production" environment
server: ['env:prod', 'shell:mongod', 'express:prod', 'express-keepalive']
};
part of backend/env/prod.js (environment-specific Express configuration, loaded if NODE_ENV is set to "prod", modeled after MEAN.JS):
'use strict';
module.exports = {
port: process.env.SERVER_PORT || 3001,
dbUrl: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://konneka.org:' + (process.env.MONGO_PORT || 27018) + '/mean'
};
part of backend/env/dev.js (environment-specific Express configuration for dev environment, loaded if the `NODE_ENV variable is not set or is set to "dev"):
module.exports = {
port: process.env.SERVER_PORT || 3000,
dbUrl: 'mongodb://konneka.org:' + (process.env.MONGO_PORT || 27017) + '/mean-dev'
};
part of backend/page.js (my Express configuration page, also modeled after MEAN.JS):
'use strict';
var session = require('express-session');
var mongoStore = require('connect-mongo')(session);
var express = require('express');
var server = express();
...
// create the database object
var monServer = mongoose.connect(environ.dbUrl);
// create a client-server session, using a MongoDB collection/table to store its info
server.use(session({
resave: true,
saveUninitialized: true,
secret: environ.sessionSecret,
store: new mongoStore({
db: monServer.connections[0].db, // specify the database these sessions will be saved into
auto_reconnect: true
})
}));
...
// listen on port related to environment variable
server.listen(process.env.SERVER_PORT || 3000);
module.exports = server;
When I run grunt server, I get:
$ cd /c/repos/konneka/ && grunt server
Running "env:prod" (env) task
Running "shell:mongod" (shell) task
Running "express:prod" (express) task
Running "express-server:prod" (express-server) task
Web server started on port:3000, hostname: 127.0.0.1 [pid: 3996]
Running "express-keepalive" task
Fatal error: failed to connect to [konneka.org:27018]
Execution Time (2014-08-15 18:05:31 UTC)
loading tasks 38.3s █████████████████████████████████ 79%
express-server:prod 8.7s ████████ 18%
express-keepalive 1.2s ██ 2%
Total 48.3s
Now, I can't seem to get the database connected in the first place, but ignore that for now. Notice that the server is started on port 3000, meaning that during execution of the grunt express:prod task, SERVER_PORT is not set so the port gets set to 3000. There are numerous other examples like this, where an environment variable is not set so my app uses the default. However, notice that session tries to connect to the database on port 27018 (and fails), so MONGO_PORT does get set eventually.
If I had just tried the grunt server task, I could chalk it up to load-grunt-config running the tasks in parallel instead of one after the other or some other error, but even when I try the tasks one-by-one, such as running grunt env:prod shell:mongod express-server:prod express-keepalive, I get similar (incorrect) results, so either grunt or grunt env run the tasks in parallel, as well, or something else is going on.
What's going on here? Why are the environment variables not set correctly for later grunt tasks? When are they eventually set, and why then rather than some other time? How can I make them get set for grunt tasks themselves rather than after, assuming there even is a way?
The solution is rather obvious once you figure it out, so let's start at the beginning:
The problem
You're using load-grunt-config to load a set of modules (objects that define tasks) and combine them into one module (object) and pass it along to Grunt. To better understand what load-grunt-config is doing, take a moment to read through the source (it's just three files). So, instead of writing:
// filename: Gruntfile.js
grunt.initConfig({
foo: {
a: {
options: {},
}
},
bar: {
b: {
options: {},
}
}
});
You can write this:
// filename: grunt/foo.js
module.exports = {
a: {
options: {},
}
}
// filename: grunt/bar.js
module.exports = {
b: {
options: {},
}
}
// filename: Gruntfile.js
require('load-grunt-config')(grunt);
Basically, this way you can split up a Grunt configuration into multiple files and have it be more "maintainable". But what you'll need to realize is that these two approaches are semantically equivalent. That is, you can expect them to behave the same way.
Thus, when you write the following*:
(* I've reduced the problem in an attempt to make this answer a bit more general and to reduce noise. I've excluded things like loading the tasks and extraneous option passing, but the error should still be the same. Also note that I've changed the values of the environment variables because the default was the same as what was being set.)
// filename: grunt/env.js
module.exports = {
dev: {
options: {
add: {
// These values are different for demo purposes
NODE_ENV: 'dev',
MONGO_PORT: 'dev_mongo_port',
SERVER_PORT: 'dev_server_port'
}
}
}
};
// filename: grunt/shell.js
module.exports = {
mongod: {
command: 'mongod --port ' + (process.env.MONGO_PORT || 27017)
}
};
// filename: grunt/aliases.js
module.exports = {
server: ['env:prod', 'shell:mongod']
};
// filename: Gruntfile.js
module.exports = function (grunt) {
require('load-grunt-config')(grunt);
};
You can consider the above the same as below:
module.exports = function (grunt) {
grunt.initConfig({
env: {
dev: {
options: {
add: {
NODE_ENV: 'dev',
MONGO_PORT: 'dev_mongo_port',
SERVER_PORT: 'dev_server_port'
}
}
}
},
shell: {
mongod: {
command: 'mongod --port ' + (process.env.MONGO_PORT || 27017)
}
}
});
grunt.registerTask('server', ['env:dev', 'shell:mongod']);
};
Now do you see the problem? What command do you expect shell:mongod to run? The correct answer is:
mongod --port 27017
Where what you want to be executed is:
mongo --port dev_mongo_port
The problem is that when (process.env.MONGO_PORT || 27017) is evaluated the environment variables have not yet been set (i.e. before the env:dev task has been run).
A solution
Well let's look at a working Grunt configuration before splitting it across multiple files:
module.exports = function (grunt) {
grunt.initConfig({
env: {
dev: {
options: {
add: {
NODE_ENV: 'dev',
MONGO_PORT: 'dev_mongo_port',
SERVER_PORT: 'dev_server_port'
}
}
}
},
shell: {
mongod: {
command: 'mongod --port ${MONGO_PORT:-27017}'
}
}
});
grunt.registerTask('server', ['env:dev', 'shell:mongod']);
};
Now when you run shell:mongod, the command will contain ${MONGO_PORT:-27017} and Bash (or just sh) will look for the environment variable you would have set in the task before it (i.e. env:dev).
Okay, that's all well and good for the shell:mongod task, but what about the other tasks, Express for example?
You'll need to move away from environment variables (unless you want to set them up before invoking Grunt. Why? Take this Grunt configuration for example:
module.exports = function (grunt) {
grunt.initConfig({
env: {
dev: {
options: {
add: {
NODE_ENV: 'dev',
MONGO_PORT: 'dev_mongo_port',
SERVER_PORT: 'dev_server_port'
}
}
}
},
express: {
options: {
hostname: '127.0.0.1'
port: (process.env.SERVER_PORT || 3000)
},
prod: {
options: {
livereload: true
server: path.resolve(__dirname, '../backend/page.js'),
bases: 'dist/'
}
}
}
});
grunt.registerTask('server', ['env:dev', 'express:prod']);
};
What port will the express:prod task configuration contain? 3000. What you need is for it to reference the value you've defined in the above task. How you do this is up to you. You could:
Separate the env configuration and reference its values
module.exports = function (grunt) {
grunt.config('env', {
dev: {
options: {
add: {
NODE_ENV: 'dev',
MONGO_PORT: 'dev_mongo_port',
SERVER_PORT: 'dev_server_port'
}
}
}
});
grunt.config('express', {
options: {
hostname: '127.0.0.1'
port: '<%= env.dev.options.add.SERVER_PORT %>'
}
});
grunt.registerTask('server', ['env:dev', 'express:prod']);
};
But you'll notice that the semantics of the env task don't hold up here due to it no longer representing a task's configuration. You could use an object of your own design:
module.exports = function (grunt) {
grunt.config('env', {
dev: {
NODE_ENV: 'dev',
MONGO_PORT: 'dev_mongo_port',
SERVER_PORT: 'dev_server_port'
}
});
grunt.config('express', {
options: {
hostname: '127.0.0.1'
port: '<%= env.dev.SERVER_PORT %>'
}
});
grunt.registerTask('server', ['env:dev', 'express:prod']);
};
Pass grunt an argument to specify what config it should use
Have multiple configuration files (e.g. Gruntfile.js.dev and Gruntfile.js.prod) and rename them as needed
Read a development configuration file (e.g. grunt.file.readJSON('config.development.json')) if it exists and fall back to a production configuration file if it doesn't exist
Some better way not listed here
But all of the above should achieve the same end result.
This seems to be the essence of what you are trying to do, and it works for me. The important part was what I mentioned in my comment -- chaining the environment task before running the other tasks.
Gruntfile.js
module.exports = function(grunt) {
// Do grunt-related things in here
grunt.loadNpmTasks('grunt-env');
grunt.initConfig({
env: {
dev: {
PROD : 'http://production.server'
}
}
});
grunt.registerTask('printEnv', 'prints a message with an env var', function() { console.log('Env var in subsequent grunt task: ' + process.env.PROD) } );
grunt.registerTask('prod', ['env:dev', 'printEnv']);
};
Output of grunt prod
Running "env:dev" (env) task
Running "printEnv" task
Env var in subsequent grunt task: http://production.server
Done, without errors.