Просмотр исходного кода

new loader system: real line numbers, and more configurable

Adam Shaw 13 лет назад
Родитель
Сommit
a186f5295e
5 измененных файлов с 430 добавлено и 221 удалено
  1. 60 166
      Gruntfile.js
  2. 0 53
      build/deps.js
  3. 312 0
      build/loader.js
  4. 57 0
      files.js
  5. 1 2
      package.json

+ 60 - 166
Gruntfile.js

@@ -1,8 +1,10 @@
 
 var _ = require('underscore');
 
+
 module.exports = function(grunt) {
 
+
 	// Load required NPM tasks.
 	// You must first run `npm install` in the project's root directory to get these dependencies.
 	grunt.loadNpmTasks('grunt-contrib-concat');
@@ -10,33 +12,28 @@ module.exports = function(grunt) {
 	grunt.loadNpmTasks('grunt-contrib-copy');
 	grunt.loadNpmTasks('grunt-contrib-compress');
 	grunt.loadNpmTasks('grunt-contrib-clean');
-	grunt.loadNpmTasks('grunt-contrib-watch'); // Very useful for development. See README.
 
 
+	var fileIndex = require('./files.js'); // lists of source/dependency files
+	var loaderUtils = require('./build/loader.js');
+
 	// read config files, and combine into one "meta" object
 	var packageConfig = grunt.file.readJSON('package.json');
 	var componentConfig = grunt.file.readJSON('component.json');
 	var pluginConfig = grunt.file.readJSON('fullcalendar.jquery.json');
 	var meta = _.extend({}, packageConfig, componentConfig, pluginConfig);
 	
-	
-	var config = { // this will eventually get passed to grunt.initConfig
+	// this will eventually get passed to grunt.initConfig
+	var config = {
 		meta: meta, // do this primarily for templating (<%= %>)
-
-		// initialize multitasks
-		concat: {},
+		concat: {}, // initialize multitasks...
 		uglify: {},
 		copy: {},
 		compress: {},
-		clean: {},
-		watch: {} // we will add watch tasks whenever we do concats, so files get re-concatenated upon save
+		clean: {}
 	};
 
 
-	// files that the demos might need in the distributable
-	var depFiles = require('./build/deps.js');
-
-
 	/* Important Top-Level Tasks
 	----------------------------------------------------------------------------------------------------*/
 
@@ -44,125 +41,51 @@ module.exports = function(grunt) {
 
 	grunt.registerTask('dist', 'Create a distributable ZIP file', [
 		'clean:build',
-		'submodules',
+		'concat',
 		'uglify',
-		'copy:deps',
+		'copy:dependencies',
 		'copy:demos',
 		'copy:misc',
 		'compress'
 	]);
 
-	grunt.registerTask('dev', 'Build necessary files for developing and debugging', 'submodules');
 
-	grunt.registerTask('submodules', 'Build all FullCalendar submodules', [
-		'main',
-		'gcal'
-	]);
-
-
-	/* Main FullCalendar Submodule
+	/* Concatenate Submodules
 	----------------------------------------------------------------------------------------------------*/
 
-	grunt.registerTask('main', 'Build the main FullCalendar submodule', [
-		'concat:mainJs',
-		'concat:mainCss',
-		'concat:mainPrintCss'
-	]);
-
-	// JavaScript
-
-	config.concat.mainJs = {
-		options: {
-			process: true // replace template variables
-		},
-		src: [
-			'src/intro.js',
-			'src/defaults.js',
-			'src/main.js',
-			'src/Calendar.js',
-			'src/Header.js',
-			'src/EventManager.js',
-			'src/date_util.js',
-			'src/util.js',
-			'src/basic/MonthView.js',
-			'src/basic/BasicWeekView.js',
-			'src/basic/BasicDayView.js',
-			'src/basic/BasicView.js',
-			'src/basic/BasicEventRenderer.js',
-			'src/agenda/AgendaWeekView.js',
-			'src/agenda/AgendaDayView.js',
-			'src/agenda/AgendaView.js',
-			'src/agenda/AgendaEventRenderer.js',
-			'src/common/View.js',
-			'src/common/DayEventRenderer.js',
-			'src/common/SelectionManager.js',
-			'src/common/OverlayManager.js',
-			'src/common/CoordinateGrid.js',
-			'src/common/HoverListener.js',
-			'src/common/HorizontalPositionCache.js',
-			'src/outro.js'
-		],
-		dest: 'build/out/fullcalendar/fullcalendar.js'
-	};
-
-	config.watch.mainJs = {
-		files: config.concat.mainJs.src,
-		tasks: 'concat:mainJs'
-	};
-
-	// CSS
-
-	config.concat.mainCss = {
-		options: {
-			process: true // replace template variables
-		},
-		src: [
-			'src/main.css',
-			'src/common/common.css',
-			'src/basic/basic.css',
-			'src/agenda/agenda.css'
-		],
-		dest: 'build/out/fullcalendar/fullcalendar.css'
-	};
-
-	config.watch.mainCss = {
-		files: config.concat.mainCss.src,
-		tasks: 'concat:mainCss'
-	};
-
-	// CSS (for printing)
+	_.each(fileIndex.fullcalendar, function(submodule, name) {
 
-	config.concat.mainPrintCss = {
-		options: {
-			process: true // replace template variables
-		},
-		src: 'src/common/print.css',
-		dest: 'build/out/fullcalendar/fullcalendar.print.css'
-	};
-
-	config.watch.mainPrintCss = {
-		files: config.concat.mainPrintCss.src,
-		tasks: 'concat:mainPrintCss'
-	};
-
-
-	/* Google Calendar Submodule
-	----------------------------------------------------------------------------------------------------*/
+		if (submodule.js) {
+			config.concat[name + '-js'] = {
+				options: {
+					process: true // replace template variables
+				},
+				src: submodule.js,
+				dest: 'build/out/fullcalendar/' + name + '.js'
+			};
+		}
 
-	grunt.registerTask('gcal', 'Build the Google Calendar submodule', 'concat:gcalJs');
+		if (submodule.css) {
+			config.concat[name + '-css'] = {
+				options: {
+					process: true // replace template variables
+				},
+				src: submodule.css,
+				dest: 'build/out/fullcalendar/' + name + '.css'
+			};
+		}
 
-	config.concat.gcalJs = {
-		options: {
-			process: true // replace template variables
-		},
-		src: 'src/gcal/gcal.js',
-		dest: 'build/out/fullcalendar/gcal.js'
-	};
+		if (submodule.printCss) {
+			config.concat[name + '-print-css'] = {
+				options: {
+					process: true // replace template variables
+				},
+				src: submodule.printCss,
+				dest: 'build/out/fullcalendar/' + name + '.print.css'
+			};
+		}
 
-	config.watch.gcalJs = {
-		files: config.concat.gcalJs.src,
-		tasks: 'concat:gcalJs'
-	};
+	});
 
 
 	/* Minify the JavaScript
@@ -173,7 +96,7 @@ module.exports = function(grunt) {
 			preserveComments: 'some' // keep comments starting with /*!
 		},
 		expand: true,
-		src: 'build/out/fullcalendar/fullcalendar.js',
+		src: 'build/out/fullcalendar/*.js',
 		ext: '.min.js'
 	}
 
@@ -181,12 +104,14 @@ module.exports = function(grunt) {
 	/* Copy Dependencies
 	----------------------------------------------------------------------------------------------------*/
 
-	config.copy.deps = {
+	config.copy.dependencies = {
 		expand: true,
 		flatten: true,
-		src: depFiles,
-		dest: 'build/out/jquery/' // all depenencies will go in the jquery/ directory for now
-		                          // (because we only have jquery and jquery-ui)
+		src: [
+			fileIndex['jquery'].js,
+			fileIndex['jquery-ui'].js
+		],
+		dest: 'build/out/jquery/'
 	};
 
 
@@ -195,11 +120,15 @@ module.exports = function(grunt) {
 
 	config.copy.demos = {
 		options: {
-			// while copying demo files over, rewrite <script> and <link> tags for new dependency locations
+			// while copying demo files over, replace loader.js <script> with actual tags
 			processContentExclude: 'demos/*/**', // don't process anything more than 1 level deep (like assets)
 			processContent: function(content) {
-				content = rewriteDemoStylesheetTags(content);
-				content = rewriteDemoScriptTags(content);
+				content = content.replace(
+					/<script[^>]*loader\.js[^>]*?(?:data-modules=['"](.*?)['"])?><\/script>/i, // match loader.js tag and modules param
+					function(wholeMatch, moduleString) {
+						return loaderUtils.buildTags('..', fileIndex, moduleString, 'dist');
+					}
+				);
 				return content;
 			}
 		},
@@ -207,41 +136,6 @@ module.exports = function(grunt) {
 		dest: 'build/out/'
 	};
 
-	function rewriteDemoStylesheetTags(content) {
-		return content.replace(
-			/(<link[^>]*href=['"])(.*?\.css)(['"][^>]*>)/g,
-			function(full, before, href, after) {
-				href = href.replace('../build/out/', '../');
-				return before + href + after;
-			}
-		);
-	}
-
-	function rewriteDemoScriptTags(content) {
-		return content.replace(
-			/(<script[^>]*src=['"])(.*?)(['"][\s\S]*?<\/script>)/g,
-			function(full, before, src, after) {
-				if (src == '../build/deps.js') {
-					return buildDepScriptTags();
-				}
-				else {
-					src = src.replace('../build/out/', '../');
-					src = src.replace('/fullcalendar.', '/fullcalendar.min.'); // use minified version of main JS file
-					return before + src + after;
-				}
-			}
-		);
-	}
-
-	function buildDepScriptTags() {
-		var tags = [];
-		for (var i=0; i<depFiles.length; i++) {
-			var fileName = depFiles[i].replace(/.*\//, ''); // get file's basename
-			tags.push("<script src='../jquery/" + fileName + "'></script>"); // all dependencies are in jquery/ for now
-		}
-		return tags.join("\n");
-	}
-
 
 	/* Copy Misc Files
 	----------------------------------------------------------------------------------------------------*/
@@ -272,11 +166,11 @@ module.exports = function(grunt) {
 
 	grunt.registerTask('component', 'Build the FullCalendar component for the Bower package manager', [
 		'clean:component',
-		'submodules',
+		'concat',
 		'uglify', // we want the minified JS in there
 		'copy:component',
-		'copy:componentReadme',
-		'componentConfig'
+		'copy:component-readme',
+		'component.json'
 	]);
 
 	config.copy.component = {
@@ -286,12 +180,12 @@ module.exports = function(grunt) {
 		dest: 'build/component/',
 	};
 
-	config.copy.componentReadme = {
+	config.copy['component-readme'] = {
 		src: 'build/component-readme.md',
 		dest: 'build/component/readme.md'
 	};
 
-	grunt.registerTask('componentConfig', function() {
+	grunt.registerTask('component.json', function() {
 		grunt.file.write(
 			'build/component/component.json',
 			JSON.stringify(

+ 0 - 53
build/deps.js

@@ -1,53 +0,0 @@
-
-/*
- * This file defines the JS dependencies required to run a barebones FullCalendar example.
- *
- * Additionally, if run from Node (i.e. the build system), this file will serve as a module that
- * exports the dependency file list.
- *
- * Additionally, if run from a browser, this file will write a <script> tag for each dependency.
- */
-
-
-// all files are relative to the project root
-
-var files = [
-	'lib/jquery-1.9.1.min.js',
-	'lib/jquery-ui-1.10.1.custom.min.js'
-];
-
-
-if (typeof module !== 'undefined') {
-
-	//
-	// in a Node module
-	//
-
-	module.exports = files;
-
-}
-else if (typeof window !== 'undefined') {
-
-	//
-	// in a browser
-	//
-
-	var root;
-	var scripts = document.getElementsByTagName('script');
-	var i;
-
-	// determine the current script's directory
-	for (i=0; i<scripts.length; i++) {
-		var match = (scripts[i].getAttribute('src') || '').match(/^(.*)\/build\/deps\.js/);
-		if (match) {
-			root = match[1];
-			break;
-		}
-	}
-
-	// write the dependency script tags
-	for (i=0; i<files.length; i++) {
-		document.write("<script src='" + root + "/" + files[i] + "'></script>\n");
-	}
-
-}

+ 312 - 0
build/loader.js

@@ -0,0 +1,312 @@
+(function() {
+
+
+/*
+ * Modules that will be implicitly included
+ */
+var DEFAULT_MODULES = [ 'jquery', 'jquery-ui', 'fullcalendar'/*the submodule*/ ];
+
+
+/*
+ * Generate HTML script/link tags for loading resources.
+ *
+ * @param {String} root
+ *     Path to the project or distributable's root directory from the HTML file running this script.
+ *
+ * @param {Object} fileIndex
+ *     File location information for each module/submodule (everything in files.js).
+ *
+ * @param {String} moduleString
+ *     A comma-separated string with names of additional modules to load.
+ *
+ * @param {string} [buildType]
+ *     unspecified - use source JS/CSS files
+ *     "concat" - use concatenated JS/CSS files
+ *     "min" - use minified JS files, concatenated CSS files
+ *     "dist" - use minified JS files, concatenated CSS files, and write paths for distributable
+ */
+function buildTags(root, fileIndex, moduleString, buildType) {
+
+	var extraModuleNames = moduleString ? moduleString.split(',') : [];
+	var moduleNames = DEFAULT_MODULES.concat(extraModuleNames);
+
+	var jsPaths = [];
+	var cssPaths = [];
+	var printCssPaths = [];
+
+	var buildRoot;
+	if (buildType == 'dist') {
+		buildRoot = root; // in the distributable, the build root IS the root
+	}
+	else {
+		buildRoot = root + '/build/out';
+	}
+
+	each(moduleNames, function(moduleName) {
+
+		var submodule = fileIndex.fullcalendar[moduleName];
+		var module = submodule || fileIndex[moduleName];
+
+		if (module) {
+
+			var js = arrayify(module.js);
+			var css = arrayify(module.css);
+			var printCss = arrayify(module.printCss);
+
+			if (buildType && submodule) {
+				//
+				// paths for a "built" fullcalendar submodule
+				//
+				if (js.length) {
+					jsPaths.push(buildRoot + '/fullcalendar/' + moduleName + (buildType == 'concat' ? '' : '.min') + '.js');
+				}
+				if (css.length) {
+					cssPaths.push(buildRoot + '/fullcalendar/' + moduleName + '.css');
+				}
+				if (printCss.length) {
+					printCssPaths.push(buildRoot + '/fullcalendar/' + moduleName + '.print.css');
+				}
+			}
+			else if (buildType == 'dist' && !submodule) {
+				//
+				// 3rd-party paths in the distributable (they all go in jquery/ for now)
+				//
+				each(js, function(path) {
+					jsPaths.push(buildRoot + '/jquery/' + basename(path));
+				});
+				each(css, function(path) {
+					cssPaths.push(buildRoot + '/jquery/' + basename(path));
+				});
+				each(printCss, function(path) {
+					printCssPaths.push(buildRoot + '/jquery/' + basename(path));
+				});
+			}
+			else {
+				//
+				// paths for development
+				//
+				each(js, function(path) {
+					if (!/(intro|outro)\.js$/.test(path)) { // don't write syntactically incorrect intro.js/outro.js
+						jsPaths.push(root + '/' + path);
+					}
+				});
+				each(css, function(path) {
+					cssPaths.push(root + '/' + path);
+				});
+				each(printCss, function(path) {
+					printCssPaths.push(root + '/' + path);
+				});
+			}
+		}
+	});
+
+	return [].concat(
+		map(cssPaths, buildCssTag),
+		map(printCssPaths, buildPrintCssTag),
+		map(jsPaths, buildJsTag)
+		)
+		.join('\n');
+}
+
+
+/* When run in a browser...
+====================================================================================================*/
+
+
+/*
+ * Writes script/link tags for resources.
+ * Attributes can be added to loader.js's script tag to change its behavior:
+ *    data-modules="..." (a comma-separated list of additional modules to load)
+ *    data-debug="true"
+ */
+function run() {
+
+	var thisScript = getLastScript();
+	var src = thisScript.getAttribute('src');
+	var cwd = dirname(src);
+	var projectRoot = dirname(cwd);
+	var moduleString = thisScript.getAttribute('data-modules');
+	var debugEnabled = !!thisScript.getAttribute('data-debug');
+	var buildType = getQueryStringVar('build');
+
+	if (debugEnabled) {
+		buildType = buildType || getCookieVar('build'); // fall back to cookie
+		initDebug(buildType);
+	}
+
+	loadScript(projectRoot + '/files.js', function(fileIndex) {
+		document.write(
+			buildTags(projectRoot, fileIndex, moduleString, buildType)
+		);
+	});
+}
+
+
+/*
+ * Installs a build-switching <select> box at top left of screen.
+ */
+function initDebug(currentBuildType) {
+	window.onload = function() {
+		var form = $(
+			"<form type='GET' style='position:absolute;top:5px;left:5px'>" +
+				"<select name='build'>" +
+					"<option value=''>src</option>" +
+					"<option value='concat'>concat</option>" +
+					"<option value='min'>min</option>" +
+				"</select>" +
+			"</form>"
+		);
+
+		var select = form.find('select')
+			.on('change', function() {
+				var val = select.val();
+				if (!val) {
+					select.remove(); // erases from resulting query string
+				}
+				document.cookie = 'build=' + val; // save cookie
+				form.submit();
+			})
+			.val(currentBuildType || '');
+
+		$('body').append(form);
+	};
+}
+
+
+/* HTML-building Utilities
+====================================================================================================*/
+
+
+function buildJsTag(path) {
+	return "<script src='" + path + "'></script>"
+}
+
+
+function buildCssTag(path) {
+	return "<link href='" + path + "' rel='stylesheet' />";
+}
+
+
+function buildPrintCssTag(path) {
+	return "<link href='" + path + "' rel='stylesheet' media='print' />";
+}
+
+
+/* DOM Utilities
+====================================================================================================*/
+
+/*
+ * Loads an external JS file in the global scope, and calls `callback` when done.
+ * If the JS file is a CommonJS module, the module's "exports" will be given to the callback.
+ */
+function loadScript(path, callback) {
+	var funcName = ('_scriptCallback' + Math.random()).replace('.', '');
+	document.write("<script>exports = {}; module = { exports: exports }</script>");
+	document.write("<script src='" + path + "'></script>");
+	document.write("<script>" + funcName + "()</script>");
+	window[funcName] = function() {
+		removeLastScript();
+		removeLastScript();
+		removeLastScript();
+		var exports = module.exports;
+		delete window.module;
+		delete window.exports;
+		delete window[funcName];
+		if (callback) {
+			callback(exports);
+		}
+	};
+}
+
+
+function removeLastScript() {
+	var script = getLastScript();
+	script.parentNode.removeChild(script);
+}
+
+
+function getLastScript() {
+	var scripts = document.getElementsByTagName('script');
+	return scripts[scripts.length - 1];
+}
+
+
+function getQueryStringVar(name) {
+	var qs = extractQueryString(window.location.href);
+	var match = new RegExp(name + '=([^&]*)').exec(qs);
+	if (match) {
+		return match[1];
+	}
+}
+
+
+function getCookieVar(name) {
+	var match = new RegExp(name + '=([^;]*)').exec(document.cookie);
+	if (match) {
+		return match[1];
+	}
+}
+
+
+/* File Path Utilities
+====================================================================================================*/
+
+
+function basename(path) {
+	var match = /.*\/(.*)/.exec(path);
+	return match ? match[1] : path;
+}
+
+
+function dirname(path) {
+	var match = /(.*)\//.exec(path);
+	return match ? match[1] : '.';
+}
+
+
+function extractQueryString(url) {
+	var match = /\?(.*)/.exec(url);
+	return match ? match[1] : '';
+}
+
+
+/* Language Utilities
+====================================================================================================*/
+
+
+function each(a, f) {
+	for (var i=0; i<a.length; i++) {
+		f(a[i], i);
+	}
+}
+
+
+function map(a, f) {
+	var res = [];
+	for (var i=0; i<a.length; i++) {
+		res.push(
+			f(a[i], i)
+		);
+	}
+	return res;
+}
+
+
+function arrayify(a) {
+	return [].concat(a || []);
+}
+
+
+/* RUN!
+====================================================================================================*/
+
+
+if (typeof exports != 'undefined') {
+	exports.buildTags = buildTags; // if we are running in Node, provide the buildTags() utility
+}
+else {
+	run(); // we are running in a web browser
+}
+
+
+})();

+ 57 - 0
files.js

@@ -0,0 +1,57 @@
+
+module.exports = {
+
+	jquery: {
+		js: 'lib/jquery-1.9.1.min.js'
+	},
+
+	'jquery-ui': {
+		js: 'lib/jquery-ui-1.10.1.custom.min.js'
+	},
+
+	fullcalendar: {
+
+		fullcalendar: {
+			js: [
+				'src/intro.js',
+				'src/defaults.js',
+				'src/main.js',
+				'src/Calendar.js',
+				'src/Header.js',
+				'src/EventManager.js',
+				'src/date_util.js',
+				'src/util.js',
+				'src/basic/MonthView.js',
+				'src/basic/BasicWeekView.js',
+				'src/basic/BasicDayView.js',
+				'src/basic/BasicView.js',
+				'src/basic/BasicEventRenderer.js',
+				'src/agenda/AgendaWeekView.js',
+				'src/agenda/AgendaDayView.js',
+				'src/agenda/AgendaView.js',
+				'src/agenda/AgendaEventRenderer.js',
+				'src/common/View.js',
+				'src/common/DayEventRenderer.js',
+				'src/common/SelectionManager.js',
+				'src/common/OverlayManager.js',
+				'src/common/CoordinateGrid.js',
+				'src/common/HoverListener.js',
+				'src/common/HorizontalPositionCache.js',
+				'src/outro.js'
+			],
+			css: [
+				'src/main.css',
+				'src/common/common.css',
+				'src/basic/basic.css',
+				'src/agenda/agenda.css'
+			],
+			printCss: 'src/common/print.css'
+		},
+
+		gcal: {
+			js: 'src/gcal/gcal.js'
+		}
+
+	}
+
+};

+ 1 - 2
package.json

@@ -6,7 +6,6 @@
     "grunt-contrib-uglify": "~0.1.1",
     "grunt-contrib-copy": "~0.4.0",
     "grunt-contrib-compress": "~0.4.0",
-    "grunt-contrib-clean": "~0.4.0",
-    "grunt-contrib-watch": "~0.2.0"
+    "grunt-contrib-clean": "~0.4.0"
   }
 }