design-system-framework
Version:
Create and maintain web component libraries for websites
538 lines (455 loc) • 17.1 kB
JavaScript
///@TODO move dsf to models ?
var fs = require('fs'),
path = require('path'),
_ = require('lodash'),
glob = require('glob'),
async = require('async'),
Handlebars = require('handlebars'),
watchr = require('watchr'),
util = require('./utils'),
configValidator = require('./config-validator'),
ComponentCandidate = require('../models/ComponentCandidate.js'),
Component = require('../models/Component.js'),
PreviewDocument = require('../models/PreviewDocument.js');
var CONFIG; // loaded at dsf.init
var defaultConfig = require('../config.json');
// state is mutable, CONFIG is not
var state = {
initialized: false,
componentsFetched: false,
candidatesAdded: 0,
candidatesResolved: 0,
candidatesYetToLoad: 0, // used to determine when all components have loaded
finishedResolveCandidates: function(){
return this.candidatesAdded > 0 && this.candidatesAdded === this.candidatesResolved;
}
};
var components = [],
componentsById = {},
inspectorPluginsById = {},
cliPluginsById = {},
serverPluginsById = {},
pluginsPathById = {},
resourceHandlersByType = {},
waitingForComponentLoad = [],
onRebuildCallback,
onNewComponentCallback,
onLoadCallback;
function fetchComponents(fromDir, componentIdPrefix, config){
if(!util.path.isDirectory(fromDir)){
return;
}
var componentsDirs = fs.readdirSync(fromDir);
componentsDirs.forEach(function(dirName){
var componentName = dirName;
var componentDir = path.join(fromDir, componentName);
addComponentPathCandidate(componentDir, componentIdPrefix ? componentIdPrefix + '/' + componentName : null, config);
});
}
/**
* Add a condidate component path to be transformed into
* a Component if it resolves
* @param {string} path relative path to component source
* @param {string} componentId optional - componentId to use once resolved
*/
function addComponentPathCandidate(path, componentId, config, onFailCallback){
//API.log('add candidate: '+path);
var candidate = new ComponentCandidate({
dsf: API,
path: path,
id: componentId,
config: config,
onFailCallback: onFailCallback
});
state.candidatesAdded++;
candidate.resolve(candidateResolved);
}
function candidateResolved(candidate){
'use strict';
state.candidatesResolved++;
if(candidate.isComponent){
state.candidatesYetToLoad++;
var componentId = removeAbsPath(candidate.absPath, candidate.config['components-path']);
if(candidate.options.id){
componentId = candidate.options.id;
}
if(componentsById[componentId]){
API.log('Override: ' + componentId);
componentsById[componentId]._onBuildDone = null;// cancel build callback
componentsById[componentId] = null;
components = _.reduce(components, function(res, comp){
if(comp.id !== componentId){
res.push(comp);
}
return res;
}, []);
}
var component = new Component({
path: candidate.path,
dsf: API,
id: componentId,
config: candidate.config
});
if(CONFIG.base && CONFIG.base.css){
component.baseDependencies = CONFIG.base.css.slice(0);
if(CONFIG.base.css.indexOf(component.id)>-1){
component.isBaseCss = true;
}
}
components.push(component);
componentsById[component.id] = component;
component.build(componentLoaded.bind(null, component));
}else{
//API.log('not a component: '+candidate.path);
if(candidate.onFailCallback){
candidate.onFailCallback();
}else{
fetchComponents(candidate.path);
}
}
}
function componentLoaded(component){
API.log(component.id+' loaded');
state.candidatesYetToLoad--;
if(state.candidatesYetToLoad === 0){
if(onLoadCallback){
onLoadCallback();
}
}
waitingForComponentLoad.forEach(function(waitingFor){
if(waitingFor.components[component.id]){
// wait is over
waitingFor.components[component.id] = false;
waitingFor.count--;
if(waitingFor.count === 0){
waitingFor.callback();
}
}
});
if(onNewComponentCallback){
onNewComponentCallback(component);
}
}
function removeAbsPath(absPath, toRemove){
var componentsPathAbs = util.path.absolutePath(toRemove || CONFIG['components-path']);
if(componentsPathAbs[componentsPathAbs.length-1]!=='/'){
componentsPathAbs+='/';
}
return absPath.replace(componentsPathAbs, '');
}
function getComponents(){
if(!state.finishedResolveCandidates()){
return 'still fetching components';
}
return components;
}
function idFromFilePath(path){
// remove left part
path = removeAbsPath(util.path.absolutePath(path));
// search for a component matching the beginning of the path
var matches = _.filter(components, function(component){
if(path.substr(0, component.id.length) === component.id){
return true;
}
return false;
});
if(matches.length !== 1){
throw new Error('idFromFilePath: ' + path + '; could not find a component');
}
return matches[0].id;
}
///@TODO move this to cli.js
function watchFiles(){
'use strict';
watchr.watch({
path: CONFIG['components-path'],
listeners: {
error: function(err){
API.log.error('Watch: an error occured:', err);
},
watching: function(err, watcherInstance, isWatching){
if (err) {
API.log.error("watching the path " + watcherInstance.path + " failed with error", err);
} else {
API.log(API.log.chalk.green('watching ' + watcherInstance.path), isWatching);
}
},
change: function(changeType, f){
//API.log('Watch ', changeType, f);
var component;
try{
var componentId = idFromFilePath(f);
component = API.getComponent(componentId);
}catch(err){}
if(changeType === 'create'){
if(component){
//existing component, rebuild
component.rebuild(function(){
if(onRebuildCallback){
onRebuildCallback(component);
API.log(componentId+' fully rebuilt');
}
});
}else{
// could be a new component
addComponentPathCandidate(f, null, null, function tryUpPath(){
f = f.split('/').slice(0,-1).join('/');
if(f.split('/').length > 1){
addComponentPathCandidate(f, null, null, tryUpPath);
}
});
}
}else if(changeType === 'update'){
if(component){
component.rebuild(function(){
if(onRebuildCallback){
onRebuildCallback(component);
API.log(componentId+' fully rebuilt');
}
});
}
}else if(changeType === 'delete'){
}
}
}
});
}
function loadPlugins(plugins, callback){
'use strict';
async.eachSeries(plugins, function(pluginPath, cb){
var absolutePluginPath = util.path.absolute(pluginPath);
if(pluginPath.split('/')[0] === 'plugins'){
pluginPath = './'+path.relative(process.cwd(), path.join(__dirname, '../'+pluginPath));
absolutePluginPath = util.path.absolute(pluginPath);
}
util.file.readJSON(path.join(absolutePluginPath, 'package.json'), function(err, packageJson){
if(err){
API.log.error('Plugin error: "'+pluginPath+'": package.json not found');
cb();
}else{
var pluginId = packageJson.name;
if(!pluginId){
API.log.error('Plugin error: "'+pluginPath+'": name is required');
cb();
}else{
pluginsPathById[pluginId] = pluginPath; // relative path (from cwd) to plugin dir
///@TODO protect against plugins not calling callback()
///@TODO modify API for plugins (special log, ...)
require(absolutePluginPath)(API, cb);
}
}
});
}, callback);
}
// define API
var API = {
util: util,
log: util.log,
dirname: path.join(__dirname, '..'),
init: function(configFile, callback){
if(arguments.length === 1){
callback = configFile;
configFile = null;
}
API.init = util.func.noop; // API.init can only be called once
configFile = configFile || 'config.json';
util.file.readJSON(util.path.absolute(configFile), function(err, userConfig){
if(err){
API.log.warning('Warning: no user config found\n' + err);
userConfig = {};
}
CONFIG = _.merge({}, defaultConfig, userConfig);
if(!CONFIG.plugins){
CONFIG.plugins = [];
}
// add core plugins
CONFIG.plugins.unshift("plugins/dsf-doc");
CONFIG.plugins.unshift("plugins/dsf-ui");
configValidator(CONFIG, function(err, configStatus){
if(configStatus.canStart){
if(CONFIG.plugins){
loadPlugins(CONFIG.plugins, function(){
state.initialized = true;
callback();
});
}else if(callback){
callback();
}
}else{
API.log.error('Bad configuration: can’t start\n'+configStatus);
}
});
});
},
start: function(){
if(!state.initialized){
throw new Error('dsf.start called before initialization complete');
}
API.start = util.func.noop; // API.start can only be called once
// start fetching components from file system
fetchComponents(CONFIG['components-path']);
// add external components
if(CONFIG['external-components']){
_.each(CONFIG['external-components'], function(external, componentId){
if(external['components-path']){
fetchComponents(external['components-path'], componentId, external);
}else if(external['component-path']){
addComponentPathCandidate(external['component-path'], componentId, external);
}
});
}
// watch files
watchFiles();
},
getConfig: function(){
return CONFIG;
},
getDefaultConfig: function(){///@TODO cleanup. this should not be used, instead addDefaultConfig() or something
return defaultConfig;
},
getComponents: getComponents,
getComponent: function(componentId, callback){
if(typeof callback === 'function'){
// wait for the component to load
API.whenLoaded([componentId], function(){
callback(componentsById[componentId]);
});
}else{
// return directly
var component = componentsById[componentId];
if(!component) throw new Error('DSF: component "'+componentId+'" not found (not ready yet?)');
return component;
}
},
getComponentIdFromPath: function(path){
return idFromFilePath(path);
},
createPreviewDocument: function(){
return new PreviewDocument();
},
getHandlebars: function(){
return Handlebars;
},
whenLoaded: function(componentIds, callback){
var missingCount = 0;
var missing = _.reduce(componentIds, function(memo, componentId){
if(typeof componentsById[componentId] === 'undefined' || !componentsById[componentId].loaded){
memo[componentId] = true;
missingCount++;
}
return memo;
}, {});
if(missingCount > 0){
waitingForComponentLoad.push({
callback: callback,
components: missing,
count: missingCount
});
}else{
// all ready
callback();
}
},
onRebuild: function(callback){
onRebuildCallback = callback;
},
onLoad: function(callback){
onLoadCallback = callback;
},
onNewComponent: function(callback){
onNewComponentCallback = callback;
},
getBaseCss: function(callback){
var css = '',
components;
if(CONFIG.base && CONFIG.base.css){
components = CONFIG.base.css;
API.whenLoaded(components, function(){
components.forEach(function(componentId){
var component = API.getComponent(componentId);
css += component.getCss();
});
callback(css);
});
}else{
callback('');
}
},
getPreprocessor: function(name){
return require('./preprocessors/'+name+'.js');
},
registerInspectorPlugin: function(pluginId, options){
if(!pluginsPathById[pluginId]){
API.log.error('Plugin error: '+pluginId+' tried to call registerInspectorPlugin without registering plugin first.');
return;
}
var plugin = _.clone(options);
plugin.id = pluginId;
// register plugin component
if(plugin['component-path']){
addComponentPathCandidate(path.join(pluginsPathById[pluginId], plugin['component-path']), '_plugin/'+pluginId, _.merge({}, defaultConfig, {doNotInheritConfig:true}, plugin));
}
inspectorPluginsById[pluginId] = plugin;
},
registerCliPlugin: function(pluginId, cliOptions, func){
if(!pluginsPathById[pluginId]){
API.log.error('Plugin error: '+pluginId+' tried to call registerCliPlugin without registering plugin first.');
return;
}
cliPluginsById[pluginId] = {func: func};
cliPluginsById[pluginId].id = pluginId;
cliPluginsById[pluginId]._cliOptions = cliOptions;
},
registerServerPlugin: function(pluginId, method, func){
if(!pluginsPathById[pluginId]){
API.log.error('Plugin error: '+pluginId+' tried to call registerCliPlugin without registering plugin first.');
return;
}
if(!serverPluginsById[pluginId]){
serverPluginsById[pluginId] = {methods:{}};
serverPluginsById[pluginId].id = pluginId;
}
serverPluginsById[pluginId].methods[method] = func;
},
getPlugins: function(){
return {
inspector: Object.keys(inspectorPluginsById).map(function(pluginId){
return inspectorPluginsById[pluginId];
}),
cli: Object.keys(cliPluginsById).map(function(pluginId){
return cliPluginsById[pluginId];
}),
server: Object.keys(serverPluginsById).map(function(pluginId){
return serverPluginsById[pluginId];
})
};
},
registerResourceType: function(type, handler){
resourceHandlersByType[type] = handler;
},
getResourceHandler: function(type){
return resourceHandlersByType[type];
},
getResourceTypes: function(){
return Object.keys(resourceHandlersByType);
},
setUI: function(uiComponentConfig){
_.merge(uiComponentConfig, defaultConfig); ///@TODO better way to apply default config to such components (not user component)
uiComponentConfig.doNotInheritConfig = true;
addComponentPathCandidate(path.join(pluginsPathById['dsf-ui'], uiComponentConfig['component-path']), '_plugin/ui', uiComponentConfig);
}
};
API.registerResourceType('js', function(component, callback){
'use strict';
component.getResourcePaths('js', function(err, files){
if(files[0]){
fs.readFile(files[0], function(err, file){
callback(null, file.toString());
});
}else{
callback(null, '');
}
});
});
// expose API
module.exports = API;