Skip to content

Instantly share code, notes, and snippets.

@asizer
Last active March 8, 2016 16:45
Show Gist options
  • Save asizer/1be7cbe69f95320af6e4 to your computer and use it in GitHub Desktop.
Save asizer/1be7cbe69f95320af6e4 to your computer and use it in GitHub Desktop.
Custom grunt task for updating widgets in deployed webappbuilder apps

The problem

When creating apps in WebAppBuilder, an instantiated app's themes and widgets are basically frozen in their state at the time of app creation. If developers continue to update or patch those themes and widgets in their development repo (or in stemapp), the instantiated apps will never be updated.

The solution (mostly)

This gruntfile contains a custom task (written out here in javascript instead of a grunt task package so you can tinker) to update widgets and themes from a development repo to instantiated apps.

The task requires three flag options:

-appIds is a comma-separated string of numeric app ids (corresponding to the apps found in [webappbuilder-root]/server/apps/)

component is the name of the thing to copy over, for instance MyTheme or AwesomeWidget. This name needs to match the folder name where the component lives.

type is your component's type, either theme or widget.

The thing that can't get updated

To create an app config.json, WebAppBuilder mashes up some generated metadata about the app with a theme layout's config.json file. It also includes information about which widgets the app's author has added to the app and where, and how those widgets have been configured. The configuration files live in a separate configs directory.

Because of this complexity, and the unpredictability of modifying layouts and configs of instantiated apps, this task does not copy over a theme layout's config.json. Making these kinds of changes requires either editing the app in Builder. One could also directly (and carefully!) edit both the app's and the theme layout's config, but there's no guarantee the result would then work in Builder at a future date.

The setup

At the top of the gruntfile, there are three environment-specific variables. Change the path to the webappbuilder installation being used, and also change the paths in the repoBase object to point to the location of the development repository's themes and widgets folders.

The examples

grunt pushUpdates -appIds=2 component=FBITheme -type=theme

This updates the FBITheme in instantiated app #2.

grunt pushUpdates -appIds='3, 5, 9' component=CheckboxFilter -type=widget

This updates the CheckboxFilter widget in apps 3, 5, and 9.

All three flags are required.

/* this is a stripped-down version of a gruntfile that had lots of comments
and error checking, just to demonstrate the main workflow here.
this one will probably catch most problematic input, but no guarantees. */
module.exports = function(grunt) {
// TODO: replace these with your own paths
var builderDir = '../path/to/your/builder/root';
var repoBase = {
themes: 'src/themes',
widgets: 'src/widgets'
};
require('load-grunt-tasks')(grunt);
grunt.task.renameTask('sync', 'dynamicsync');
grunt.registerTask('pushUpdates', 'copy updated widget or theme to apps where it\'s been deployed', function() {
var appIds = grunt.option('appIds') + '';
var component = grunt.option('component');
var type = grunt.option('type');
appIds = appIds.indexOf(',') > 0 ? appIds.split(',') : [appIds];
appIds = appIds.map(function(str) {
return str.trim();
});
var foundComponentPaths = [];
appIds.forEach(function(id) {
var componentPath = builderDir + '/server/apps/' + id + '/' + type + 's/' + component;
if (grunt.file.exists(componentPath)) {
foundComponentPaths.push(componentPath);
}
});
if (!foundComponentPaths.length) {
return;
}
var files = foundComponentPaths.map(function(cPath) {
var file = {
cwd: repoBase[type + 's'] + '/' + component,
src: ['**'],
dest: cPath
};
if (type === 'theme') {
file.src.push('!layouts/*/config.json');
}
return file;
});
grunt.initConfig({
dynamicsync: {
updates: {
files: files,
verbose: true
}
}
});
grunt.task.run('dynamicsync');
});
};
module.exports = function(grunt) {
// TODO: replace this with your own path
var builderDir = '../path/to/your/builder/root';
// allows for your widgets and themes folders to not be siblings of your gruntfile
// or of each other.
// TODO: replace these paths with your own paths.
var repoBase = {
themes: 'src/themes',
widgets: 'src/widgets'
};
// Load grunt tasks automatically
require('load-grunt-tasks')(grunt);
// make sure you add `grunt sync` to package.json
grunt.task.renameTask('sync', 'dynamicsync');
// if you aren't using grunt-sync elsewhere, you can get rid of the next line
grunt.task.loadNpmTasks('grunt-sync');
grunt.initConfig({
// regular grunt task setup here...
});
// regular registered tasks here... (get rid of this if you don't have any)
grunt.registerTask('default', ['copy', 'sync', 'watch']);
// push component changes!
grunt.registerTask('pushUpdates', 'copy updated widget or theme to apps where it\'s been deployed', function() {
var appIds = grunt.option('appIds') + ''; // force to string
var component = grunt.option('component');
var type = grunt.option('type');
// check that options exist.
if (!appIds || !component || !type || (type !== 'theme' && type !== 'widget')) {
grunt.log.writeln('This task requires three options -- '['red']);
grunt.log.writeln(' appIds as a comma-separated string: -appIds=\'2, 3, 7\''['red']);
grunt.log.writeln(' component as a string: -component=\'MyComponent\''['red']);
grunt.log.writeln(' type as a string: -type=[\'theme\'|\'widget\']'['red']);
return;
}
if (!repoBase[type + 's']) {
grunt.log.writeln('Missing repoBase path'['red']);
return;
}
// process app id's.
appIds = appIds.indexOf(',') > 0 ? appIds.split(',') : [appIds];
appIds = appIds.map(function(str) {
return str.trim();
});
// find paths within the specified apps to the widget folder
var foundComponentPaths = [];
appIds.forEach(function(id) {
if (isNaN(parseInt(id, 10))) {
grunt.log.writeln('Ignoring non-numeric app id '['yellow'] + id['yellow']);
return;
}
// construct the path where the component should live for this app
var componentPath = builderDir + '/server/apps/' + id + '/' + type + 's/' + component;
// check to see if the component is even deployed to this app
if (grunt.file.exists(componentPath)) {
// and if it is found, capture the app path
foundComponentPaths.push(componentPath);
}
});
if (!foundComponentPaths.length) {
var message = 'No apps with component ' + component + ' found in apps ' + appIds;
grunt.log.writeln(message['yellow']);
return;
}
// construct config for sync event
var files = foundComponentPaths.map(function(cPath) {
var file = {
cwd: repoBase[type + 's'] + '/' + component,
src: ['**'],
dest: cPath
};
if (type === 'theme') {
file.src.push('!layouts/*/config.json');
}
return file;
});
grunt.initConfig({
dynamicsync: {
updates: {
files: files,
verbose: true
}
}
});
// actually run sync event
grunt.task.run('dynamicsync');
});
};
{
"name": "push-webappbuilder-updates",
"version": "0.1.0",
"description": "Grunt file for pushing webappbuilder updates of custom themes and widgets to already-created apps. Note this does not push changes of themes/*/layouts/config.json",
"author": {
"name": "Esri Professional Services"
},
"devDependencies": {
"grunt": "^0.4.5",
"grunt-sync": "^0.4.1",
"load-grunt-tasks": "^3.2.0"
},
"dependencies": {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment