Provides the view and the pack functions with a
+list of entries for an asset type relative to the client directory.
+The default implementation is used.
globalModules {boolean} - set true to load client side modules using their full path relative to client/code. If for example your app is my the entry module can be accessed with require('/my/entry').
-
-ss.client.set({ globalModules: true });
-
Note, that paths for excluding should be relative to client/code/ and that file client/code/app/entry.js could not be excluded in any cases.
If you need to exclude from automatically packaging certain file, just specify the file's relative path:
diff --git a/docs/partials/tutorials/client_side_development.html b/docs/partials/tutorials/client_side_development.html
new file mode 100644
index 00000000..dce4704d
--- /dev/null
+++ b/docs/partials/tutorials/client_side_development.html
@@ -0,0 +1,16 @@
+
+
+
+
+
Client-Side Development
+
Each view is served with a separate bundle of assets. A HTML, JS and CSS file makes up the view.
+
Each entry is served separately to the browser injected in the HTML on the fly.
+
+
A relative path is given in the URL, relative to the "/client" directory.
+
The client is given in the URL to determine the bundler.
+
The asset type is specified in the URL to determine the bundler.
+
A timestamp is given in the URL to break any caching.
+
+
The URL pattern is http:///_serveDev/<type>/<relative path>?ts=<id>.
+
Bundlers generally work within the client directory to reduce the amount of files to watch.
+
diff --git a/docs/partials/tutorials/client_side_xbundler.html b/docs/partials/tutorials/client_side_xbundler.html
new file mode 100644
index 00000000..4daf96d1
--- /dev/null
+++ b/docs/partials/tutorials/client_side_xbundler.html
@@ -0,0 +1,69 @@
+
+
+
+
+
Client-Side Bundler
+
Each view is served with a separate bundle of assets. A HTML, JS and CSS file makes up the view.
+The default bundler will create the bundle based on the client definition as described in Client-Side Code and Client-Side Templates.
+
You can implement your own bundler and use it for a client definition. The bundlers are named and referenced by name.
+
Be aware the API is experimental. The current bundler is based on an early Browserify implementation, so it lacks some
+features. The objective is to be able to move to bundling based on newer ones such as WebPack or JSPM. It should be possible
+to implement a bundler that completely changes how the client is implemented. Hence there will be additional responsibilities
+for bundlers in the future.
+
Custom Bundler
+
You can define a custom bundler by implementing a bundler function that will be called for each client it is used on.
The define method of the bundler will be called to complete ss.client.define.
+
Bundler Define define(client,config,..)
+
The define method will be called with a client object containing id, name,
+If you pass additional arguments to define they will be passed to bundler.define. This may be dropped in the future.
+
Bundler Load load()
+
The load method is called as the first step to load the client views. This is done as a bulk action as part of starting
+the server.
+
Bundler asset methods
+
For each of the asset types supported individual files can be served during development.
+A callback function is passed, and must be called with the text contents.
+
Bundler pack methods
+
Files are saved in the assets directory for production use. The HTML file is the same as the one used during development,
+so the asset.html method will be called. For JS and CSS the pack methods are called to do additional minification and
+other optimisations.
+
Bundler shorthand
+
A lot of functionality is built-in. When you write your own bundler you shouldn't have to do it all over again. So most
+of the existing behaviour can be called through ss.bundler.
+
ss.bundler.sourcePaths(ss,paths,options)
+
This returns a revised paths object. Paths should contain entries code, css, tmpl. They will be forced into an array.
+If a path starts with "./", it is considered relative to "/client". Otherwise it is considered relative to "/client/code",
+"/client/css", "/client/templates". These directory options can be set using ss.client.set.
diff --git a/docs/partials/tutorials/url_scheme.html b/docs/partials/tutorials/url_scheme.html
new file mode 100644
index 00000000..ba29c523
--- /dev/null
+++ b/docs/partials/tutorials/url_scheme.html
@@ -0,0 +1,22 @@
+
+
+
+
+
URL Scheme
+
The common URL for a view is its name at the root level, but you can choose whatever you will calling serveClient(..).
+
Assets
+
The contents of the client assets directory will be served under /assets.
+
When views are packed for production they are saved under the client assets directory. This will change in the future
+to make relative URLs work the same in development and production.
+
Middleware
+
At development time middleware is added to serve HTML, JS and CSS on the fly.
+
Serving CSS
+
CSS files are served under /assets//123.css in production. When served ad hoc in development, and on-demand in
+production all CSS must be served on the same level and ideally in an equivalent URL.
+
JS Module Paths
+
On Demand Loading
+
The current on-demand fetching of JS is handled by middleware. It should be possible to do it using static files.
+
In production it would make sense to support a path like /assets/require/...
+
We will have to consider whether all client code is considered completely open, or only partially. Should all client
+modules be exported in minified form, or only those in a whitelist.
+
diff --git a/lib/client/asset.js b/lib/client/asset.js
deleted file mode 100644
index 5f2e7d45..00000000
--- a/lib/client/asset.js
+++ /dev/null
@@ -1,137 +0,0 @@
-// Client Asset File
-// -----------------
-// An asset is a Code (JS or CoffeeScript), CSS or HTML file
-'use strict';
-
-var formatKb, formatters, fs, jsp, log, minifyJSFile, pathlib, pro, uglifyjs, wrap, wrapCode,
- __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) {return i; }} return -1; };
-
-fs = require('fs');
-
-pathlib = require('path');
-
-uglifyjs = require('uglify-js');
-
-formatters = require('./formatters');
-
-log = require('../utils/log');
-
-wrap = require('./wrap');
-
-jsp = uglifyjs.parser;
-
-pro = uglifyjs.uglify;
-
-// Load, compile and minify the following assets
-
-module.exports = function(ss, options) {
- var loadFile;
- loadFile = function(dir, fileName, type, options, cb) {
- var extension, formatter, path;
- dir = pathlib.join(ss.root, dir);
- path = pathlib.join(dir, fileName);
- extension = pathlib.extname(path);
- extension = extension && extension.substring(1); // argh!
- formatter = ss.client.formatters[extension];
- if (path.substr(0, dir.length) !== dir) {
- throw new Error('Invalid path. Request for ' + path + ' must not live outside ' + dir);
- }
- if (!formatter) {
- throw new Error('Unsupported file extension \'.' + extension + '\' when we were expecting some type of ' + (type.toUpperCase()) + ' file. Please provide a formatter for ' + (path.substring(ss.root.length)) + ' or move it to /client/static');
- }
- if (formatter.assetType !== type) {
- throw new Error('Unable to render \'' + fileName + '\' as this appears to be a ' + (formatter.assetType.toUpperCase()) + ' file. Expecting some type of ' + (type.toUpperCase()) + ' file in ' + (dir.substr(ss.root.length)) + ' instead');
- }
- return formatter.compile(path.replace(/\\/g, '/'), options, cb);
- };
- return {
-
- // Public
-
- js: function(path, opts, cb) {
- return loadFile(options.dirs.code, path, 'js', opts, function(output) {
- output = wrapCode(output, path, opts.pathPrefix, options);
- if (opts.compress && path.indexOf('.min') === -1) {
- output = minifyJSFile(output, path);
- }
- return cb(output);
- });
- },
- worker: function(path, opts, cb) {
- return loadFile(options.dirs.workers, path, 'js', opts, function(output) {
- if (opts.compress) {
- output = minifyJSFile(output, path);
- }
- return cb(output);
- });
- },
- css: function(path, opts, cb) {
- return loadFile(options.dirs.css, path, 'css', opts, cb);
- },
- html: function(path, opts, cb) {
- return loadFile(options.dirs.views, path, 'html', opts, cb);
- }
- };
-};
-
-// PRIVATE
-
-formatKb = function(size) {
- return '' + (Math.round((size / 1024) * 1000) / 1000) + ' KB';
-};
-
-minifyJSFile = function(originalCode, fileName) {
- var ast, minifiedCode;
- ast = jsp.parse(originalCode);
- ast = pro.ast_mangle(ast);
- ast = pro.ast_squeeze(ast);
- minifiedCode = pro.gen_code(ast);
- log.info((' Minified ' + fileName + ' from ' + (formatKb(originalCode.length)) + ' to ' + (formatKb(minifiedCode.length))).grey);
- return minifiedCode;
-};
-
-// Before client-side code is sent to the browser any file which is NOT a library (e.g. /client/code/libs)
-// is wrapped in a module wrapper (to keep vars local and allow you to require() one file in another).
-// The 'system' directory is a special case - any module placed in this dir will not have a leading slash
-wrapCode = function(code, path, pathPrefix, options) {
- var modPath, pathAry, sp, i;
- pathAry = path.split('/');
-
- // Don't touch the code if it's in a 'libs' directory
- if (__indexOf.call(pathAry, 'libs') >= 0) {
- return code;
- }
-
- if (__indexOf.call(pathAry, 'entry.js') === -1 && options && options.browserifyExcludePaths) {
- for(i in options.browserifyExcludePaths) {
- if (options.browserifyExcludePaths.hasOwnProperty(i)) {
- if ( path.split( options.browserifyExcludePaths[i] )[0] === '' ) {
- return code;
- }
- }
- }
- }
-
- // Don't add a leading slash if this is a 'system' module
- if (__indexOf.call(pathAry, 'system') >= 0) {
- modPath = pathAry[pathAry.length - 1];
- return wrap.module(modPath, code);
- } else {
-
- // Otherwise treat as a regular module
- modPath = options.globalModules? pathAry.join("/") : pathAry.slice(1).join('/');
-
- // Work out namespace for module
- if (pathPrefix) {
-
- // Ignore any filenames in the path
- if (pathPrefix.indexOf('.') > 0) {
- sp = pathPrefix.split('/');
- sp.pop();
- pathPrefix = sp.join('/');
- }
- modPath = path.substr(pathPrefix.length + 1);
- }
- return wrap.module('/' + modPath, code);
- }
-};
diff --git a/lib/client/bundler/default.js b/lib/client/bundler/default.js
new file mode 100644
index 00000000..79d31d86
--- /dev/null
+++ b/lib/client/bundler/default.js
@@ -0,0 +1,225 @@
+// Default bundler implementation
+'use strict';
+
+var systemAssets = require('../system').assets;
+
+function includeFlags(overrides) {
+ var includes = {
+ css: true,
+ html: true,
+ system: true,
+ initCode: true
+ };
+ if (overrides) {
+ for(var n in overrides) { includes[n] = overrides[n]; }
+ }
+ return includes;
+}
+
+/**
+ * @typedef { name:string, path:string, dir:string, content:string, options:string, type:string } AssetEntry
+ */
+
+/**
+ * @ngdoc service
+ * @name bundler.default:default
+ * @function
+ *
+ * @description
+ * The default bundler of HTML, CSS & JS
+ *
+ * @type {{define: define, load: load, toMinifiedCSS: toMinifiedCSS, toMinifiedJS: toMinifiedJS, asset: {entries: entries, loader: assetLoader, systemModule: systemModule, js: assetJS, worker: assetWorker, start: assetStart, css: assetCSS, html: assetHTML}}}
+ */
+module.exports = function(ss,client,options){
+
+ var bundler = {
+ define: define,
+ load: load,
+ toMinifiedCSS: toMinifiedCSS,
+ toMinifiedJS: toMinifiedJS,
+ asset: {
+ entries: entries,
+
+ loader: assetLoader,
+ systemModule: systemModule,
+ js: assetJS,
+ worker: assetWorker,
+ start: assetStart,
+ css: assetCSS,
+ html: assetHTML
+ }
+ };
+
+ function define(paths) {
+
+ if (typeof paths.view !== 'string') {
+ throw new Error('You may only define one HTML view per single-page client. Please pass a filename as a string, not an Array');
+ }
+ if (paths.view.indexOf('.') === -1) {
+ throw new Error('The \'' + paths.view + '\' view must have a valid HTML extension (such as .html or .jade)');
+ }
+
+ // Define new client object
+ client.paths = ss.bundler.sourcePaths(paths);
+ client.includes = includeFlags(paths.includes);
+
+ return ss.bundler.destsFor(client);
+ }
+
+ /**
+ *
+ * @returns {{a: string, b: string}}
+ */
+ function load() {
+ return {
+ a:'a',
+ b:'b'
+ }
+ }
+
+ /**
+ * @ngdoc method
+ * @name bundler.default:default#entries
+ * @methodOf bundler.default:default
+ * @function
+ * @description
+ * Provides the view and the pack functions with a
+ * list of entries for an asset type relative to the client directory.
+ * The default implementation is used.
+ *
+ * @param {String} assetType js/css
+ * @param {Object} systemAssets Collection of libs, modules, initCode
+ * @returns {[AssetEntry]} List of output entries
+ */
+ function entries(assetType,systemAssets) {
+ return ss.bundler.entries(client, assetType, systemAssets);
+ }
+
+ /**
+ * @ngdoc method
+ * @name bundler.default:default#assetLoader
+ * @methodOf bundler.default:default
+ * @function
+ * @description
+ * Return entry for the JS loader depending on the includes.system client config.
+ *
+ * @returns {AssetEntry} Loader resource
+ */
+ function assetLoader() {
+ return client.includes.system? ss.bundler.systemLibs() : null;
+ }
+
+ /**
+ * @ngdoc method
+ * @name bundler.default:default#systemModule
+ * @methodOf bundler.default:default
+ * @function
+ * @description
+ * Return the resource for a registered system module by the given name. It uses
+ * the default wrapCode for module registration with require.
+ *
+ * @param {String} name Logical Module Name
+ * @returns {AssetEntry} Module resource
+ */
+ function systemModule(name) {
+ switch(name) {
+ case "eventemitter2":
+ case "socketstream":
+ default:
+ if (client.includes.system) {
+ return ss.bundler.systemModule(name)
+ }
+ }
+ }
+
+ /**
+ *
+ * @param path
+ * @param opts
+ * @param cb
+ * @returns {*}
+ */
+ function assetJS(path, opts, cb) {
+ return ss.bundler.loadFile(path, 'js', opts, function(output) {
+ //TODO with options compress saved to avoid double compression
+ output = ss.bundler.wrapCode(output, path, opts.pathPrefix);
+ if (opts.compress && path.indexOf('.min') === -1) {
+ output = ss.bundler.minifyJSFile(output, path);
+ }
+ return cb(output);
+ });
+ }
+
+ /**
+ *
+ * @param path
+ * @param opts
+ * @param cb
+ * @returns {*}
+ */
+ function assetWorker(path, opts, cb) {
+ return ss.bundler.loadFile(path, 'js', opts, function(output) {
+ if (opts.compress) {
+ output = ss.bundler.minifyJSFile(output, path);
+ }
+ return cb(output);
+ });
+ }
+
+ /**
+ * @ngdoc method
+ * @name bundler.default:default#assetStart
+ * @methodOf bundler.default:default
+ * @function
+ * @description
+ * Return the resource for starting the view. It is code for immediate execution at the end of the page.
+ *
+ * @returns {AssetEntry} Start Script resource
+ */
+ function assetStart() {
+ return client.includes.initCode? ss.bundler.startCode(client) : null;
+ }
+
+ /**
+ *
+ * @param path
+ * @param opts
+ * @param cb
+ * @returns {*}
+ */
+ function assetCSS(path, opts, cb) {
+ return ss.bundler.loadFile(path, 'css', opts, cb);
+ }
+
+ /**
+ *
+ * @param path
+ * @param opts
+ * @param cb
+ * @returns {*}
+ */
+ function assetHTML(path, opts, cb) {
+ return ss.bundler.loadFile(path, 'html', opts, cb);
+ }
+
+ /**
+ *
+ * @param files
+ * @returns {*}
+ */
+ function toMinifiedCSS(files) {
+ return ss.bundler.minifyCSS(files);
+ }
+
+ /**
+ *
+ * @param files
+ * @returns {*}
+ */
+ function toMinifiedJS(files) {
+ return ss.bundler.minifyJS(files);
+ }
+
+ return bundler;
+};
+
diff --git a/lib/client/bundler/index.js b/lib/client/bundler/index.js
new file mode 100644
index 00000000..eef2e495
--- /dev/null
+++ b/lib/client/bundler/index.js
@@ -0,0 +1,507 @@
+// Client-Side Bundler of assets in development and production
+'use strict';
+
+var fs = require('fs'),
+ path = require('path'),
+ log = require('../../utils/log'),
+ cleanCSS = require('clean-css'),
+ system = require('../system'),
+ view = require('../view'),
+ magicPath = require('../magic_path'),
+ uglifyjs = require('uglify-js'),
+ jsp = uglifyjs.parser,
+ pro = uglifyjs.uglify;
+
+/**
+ * @typedef { name:string, path:string, dir:string, content:string, options:string, type:string } AssetEntry
+ */
+
+/**
+ * Bundler by client name
+ * @type {{}}
+ */
+var bundlers = {},
+ bundlerById = {};
+
+function getBundler(client){
+ if (typeof client === "string") { return bundlers[client]; }
+
+ if (client.bundler) { return client.bundler; }
+
+ if (client.ts) {
+ if (bundlerById[client.ts]) {
+ return bundlerById[client.ts];
+ }
+ }
+ if (typeof client.client === "string") {
+ return bundlers[client.client];
+ }
+ if (typeof client.name === "string") {
+ return bundlers[client.name];
+ }
+
+ throw new Error('Unknown client '+(client.name || client.client || client.ts) );
+}
+
+/**
+ * @ngdoc service
+ * @name bundler
+ * @function
+ * @description
+ * Bundlers included.
+ */
+
+/**
+ * @ngdoc service
+ * @name ss.bundler:bundler
+ * @function
+ *
+ * @description
+ * Client bundling API
+ * -----------
+ * Client bundling API for implementing a custom bundler.
+ */
+module.exports = function(ss,options) {
+
+
+ return {
+
+ /**
+ * @ngdoc method
+ * @name ss.bundler:bundler#define
+ * @methodOf ss.bundler:bundler
+ * @function
+ * [Internal] Define the bundler for a client (do not call directly)
+ * @param {string} client object to store the definition in
+ * @param {object} args arguments passed to define
+ */
+ define: function defineBundler(client,args) {
+
+ var name = args[0],
+ pathsOrFunc = args[1],
+ bundler;
+
+ if (typeof pathsOrFunc === "function") {
+ bundler = bundlers[name] = pathsOrFunc(ss,options);
+ bundler.dests = bundler.define(client, args[2], args[3], args[4], args[5]);
+ } else {
+ bundler = bundlers[name] = require('./default')(ss,client,options);
+ bundler.dests = bundler.define(args[1]);
+ }
+ bundlerById[client.id] = bundler;
+ },
+
+ /**
+ * @ngdoc method
+ * @name ss.bundler:bundler#get
+ * @methodOf ss.bundler:bundler
+ * @function
+ * @description
+ * Determine the bundler for a client
+ * @param {object|string} client Query params with client=name or an actual client object
+ */
+ get: getBundler,
+
+ load: function() {
+ for(var n in bundlers) {
+ if (bundlers[n].load) {
+ bundlers[n].load();
+ }
+ }
+ },
+
+ unload: function() {
+ for(var n in bundlers) {
+ if (bundlers[n].unload) {
+ bundlers[n].unload();
+ bundlers[n].unload = null;
+ }
+ }
+ },
+
+ forget: function() {
+ bundlerById = {};
+ bundlers = {};
+ },
+
+ pack: function pack(client) {
+ client.pack = true;
+
+ // the concrete bundler for the client
+ var bundler = getBundler(client);
+
+ /* PACKER */
+
+ log(('Pre-packing and minifying the \'' + client.name + '\' client...').yellow);
+
+ // Prepare folder
+ mkdir(bundler.dests.containerDir);
+ mkdir(bundler.dests.dir);
+ if (!(options.packedAssets && options.packedAssets.keepOldFiles)) {
+ deleteOldFiles(bundler.dests.dir);
+ }
+
+ // Output CSS
+ ss.bundler.packAssetSet('css', client, bundler.toMinifiedCSS);
+
+ // Output JS
+ ss.bundler.packAssetSet('js', client, bundler.toMinifiedJS);
+
+ // Output HTML view
+ return view(ss, client, options, function(html) {
+ fs.writeFileSync(bundler.dests.paths.html, html);
+ return log.info('✓'.green, 'Created and cached HTML file ' + bundler.dests.relPaths.html);
+ });
+ },
+
+
+ // API for implementing bundlers
+
+ loadFile: function loadFile(fileName, type, opts, cb) {
+ var dir = path.join(ss.root, options.dirs.client);
+ var p = path.join(dir, fileName);
+ var extension = path.extname(p);
+ extension = extension && extension.substring(1); // argh!
+ var formatter = ss.client.formatters[extension];
+ if (p.substr(0, dir.length) !== dir) {
+ throw new Error('Invalid path. Request for ' + p + ' must not live outside ' + dir);
+ }
+ if (!formatter) {
+ throw new Error('Unsupported file extension \'.' + extension + '\' when we were expecting some type of ' + (type.toUpperCase()) + ' file. Please provide a formatter for ' + (p.substring(ss.root.length)) + ' or move it to /client/static');
+ }
+ if (formatter.assetType !== type) {
+ throw new Error('Unable to render \'' + fileName + '\' as this appears to be a ' + (formatter.assetType.toUpperCase()) + ' file. Expecting some type of ' + (type.toUpperCase()) + ' file in ' + (dir.substr(ss.root.length)) + ' instead');
+ }
+ return formatter.compile(p.replace(/\\/g, '/'), opts, cb);
+ },
+
+ minifyCSS: function minifyCSS(files) {
+ var original = files.join('\n');
+ var minified = cleanCSS().minify(original);
+ log.info((' Minified CSS from ' + (formatKb(original.length)) + ' to ' + (formatKb(minified.length))).grey);
+ return minified;
+ },
+
+ minifyJS: function minifyJS_(files) {
+ var min = files.map(function(js) {
+ return js.options.minified ? js.content : minifyJS(js.content);
+ });
+ return min.join('\n');
+ },
+
+ minifyJSFile: function minifyJSFile(originalCode, fileName) {
+ var ast = jsp.parse(originalCode);
+ ast = pro.ast_mangle(ast);
+ ast = pro.ast_squeeze(ast);
+ var minifiedCode = pro.gen_code(ast);
+ log.info((' Minified ' + fileName + ' from ' + (formatKb(originalCode.length)) + ' to ' + (formatKb(minifiedCode.length))).grey);
+ return minifiedCode;
+ },
+
+ // input is decorated and returned
+ sourcePaths: function(paths) {
+
+ function entries(from, dirType) {
+ if (from == null) {
+ return [];
+ }
+ var list = (from instanceof Array)? from : [from];
+
+ return list.map(function(value) {
+ var relClient = './' + path.relative(options.dirs.client, options.dirs[dirType]);
+ return value.substring(0,2) === './'? value : path.join(relClient, value);
+ });
+ }
+
+ paths.css = entries(paths.css, 'css');
+ paths.code = entries(paths.code, 'code');
+ paths.tmpl = entries(paths.tmpl || paths.templates, 'templates');
+
+ var relClient = './' + path.relative(options.dirs.client, options.dirs['views']);
+ paths.view = paths.view.substring(0,2) === './'? paths.view : path.join(relClient, paths.view);
+
+ return paths;
+ },
+
+ /**
+ * @ngdoc method
+ * @name ss.bundler:bundler#destsFor
+ * @methodOf ss.bundler:bundler
+ * @function
+ * @description
+ * The define client method of all bundlers must return the file locations for the client.
+ *
+ * return ss.bundler.destsFor(client);
+ *
+ * To offer a very different way to define the entry-points for assets the bundler can tweak
+ * the paths or replace them.
+ * @param {object} client Object describing the client.
+ * @returns {object} Destinations paths, relPaths, dir, containerDir
+ */
+ destsFor: function(client) {
+ var containerDir = path.join(ss.root, options.dirs.assets);
+ var clientDir = path.join(containerDir, client.name);
+
+ return {
+
+ //TODO perhaps mixin the abs versions by SS
+ paths: {
+ html: path.join(clientDir, client.id + '.html'),
+ js: path.join(clientDir, client.id + '.js'),
+ css: path.join(clientDir, client.id + '.css')
+ },
+ relPaths: {
+ html: path.join(options.dirs.assets, client.name, client.id + '.html'),
+ js: path.join(options.dirs.assets, client.name, client.id + '.js'),
+ css: path.join(options.dirs.assets, client.name, client.id + '.css')
+ },
+ dir: clientDir,
+ containerDir: containerDir
+ };
+ },
+
+ /**
+ * @ngdoc method
+ * @name ss.bundler:bundler#systemLibs
+ * @methodOf ss.bundler:bundler
+ * @function
+ * @description
+ * A single entry for all system libraries.
+ *
+ * @returns {AssetEntry} Entry
+ */
+ systemLibs: function() {
+ var names = [];
+ return {
+ type: 'loader',
+ names: names,
+ content: system.assets.libs.map(function(lib) { names.push(lib.name); return lib.content; }).join('\n')
+ };
+ },
+
+ /**
+ * @ngdoc method
+ * @name ss.bundler:bundler#systemModule
+ * @methodOf ss.bundler:bundler
+ * @function
+ * @description
+ * Describe a system module.
+ *
+ * @param {String} name Name of the system module to return in a descriptor
+ * @param {boolean} wrap Shall the content be wrapped in `require.define`. Default is true.
+ * @returns {AssetEntry} Entry
+ */
+ systemModule: function(name,wrap) {
+ name = name.replace(/\.js$/,'');
+ var mod = system.assets.modules[name];
+ if (mod) {
+ var code = wrap===false? mod.content: ss.bundler.wrapModule(name, mod.content);
+ return {
+ file: mod.name,
+ name: mod.name,
+ path: mod.path,
+ dir: mod.dir,
+ content: code,
+ options: mod.options,
+ type: mod.type
+ };
+ }
+ },
+
+ /**
+ * Default start/init codes to load the client view.
+ *
+ * Called in default bundler startCode.
+ *
+ * @param client Client Object
+ * @returns {{content: *, options: {}}} Single Entry for inclusion in entries()
+ */
+ startCode: function(client) {
+ var startCode = system.assets.startCode.map(function(ic) { return ic.content; }).join('\n'),
+ entryInit = options.defaultEntryInit,
+ realInit = client.entryInitPath? 'require("' + client.entryInitPath + '");' : null;
+
+ if (typeof options.entryModuleName === 'string' || options.entryModuleName === null) {
+ realInit = options.entryModuleName? 'require("/'+options.entryModuleName+'");' : '';
+ }
+
+ if (realInit !== null) {
+ startCode = startCode.replace(entryInit, realInit);
+ }
+ return { content:startCode, options: {}, type: 'start' };
+ },
+
+ packAssetSet: function packAssetSet(assetType, client, postProcess) {
+ var bundler = getBundler(client),
+ filePaths = bundler.asset.entries(assetType,system.assets);
+
+ function writeFile(fileContents) {
+ var fileName = bundler.dests.paths[assetType];
+ fs.writeFileSync(fileName, postProcess(fileContents));
+ return log.info('✓'.green, 'Packed', filePaths.length, 'files into', bundler.dests.relPaths[assetType]);
+ }
+
+ function processFiles(fileContents, i) {
+ if (!fileContents) {
+ fileContents = [];
+ }
+ if (!i) {
+ i = 0;
+ }
+ if (filePaths.length === 0) {
+ return writeFile([]);
+ }
+
+ var _ref = filePaths[i], path = _ref.importedBy, file = _ref.file;
+ return bundler.asset[assetType](file, {
+ pathPrefix: path,
+ compress: true
+ }, function(output) {
+ fileContents.push({content:output,options:{}});
+ if (filePaths[++i]) {
+ return processFiles(fileContents, i);
+ } else {
+ return writeFile(fileContents);
+ }
+ });
+ }
+
+ return processFiles();
+ },
+
+ /**
+ * Make a list of asset entries for JS/CSS bundle.
+ *
+ * @param client
+ * @param assetType
+ * @returns {Array}
+ */
+ entries: function entries(client, assetType) {
+
+ var _entries = [],
+ bundler = getBundler(client),
+ pathType;
+ switch(assetType) {
+ case 'css': pathType = 'css'; break;
+ case 'js': pathType = 'code'; break;
+ case 'worker': pathType = 'code'; break;
+ }
+ if (pathType === 'code') {
+ // Libs
+ var libs = [bundler.asset.loader()];
+
+ // Modules
+ var mods = [],
+ _ref = system.assets.modules;
+ for (var name in _ref) {
+ if (_ref.hasOwnProperty(name)) {
+ mods.push( bundler.asset.systemModule(name) );
+ }
+ }
+ _entries = _entries.concat(libs).concat(mods);
+ }
+ client.paths[pathType].forEach(function(from) {
+ return magicPath.files(path.join(ss.root, options.dirs.client), from).forEach(function(file) {
+ return _entries.push({file:file,importedBy:from});
+ });
+ });
+ if (pathType === 'code') {
+ _entries.push(bundler.asset.start());
+ }
+
+ // entries with blank ones stripped out
+ return _entries.filter(function(entry) {
+ return !!entry;
+ });
+ },
+
+ formatKb: formatKb,
+
+ htmlTag : {
+ css: function(path) {
+ return '';
+ },
+ js: function(path) {
+ return '';
+ }
+ },
+
+ wrapModule: function(modPath, code) {
+ return 'require.define("' + modPath + '", function (require, module, exports, __dirname, __filename){\n' + code + '\n});';
+ },
+
+ // Before client-side code is sent to the browser any file which is NOT a library (e.g. /client/code/libs)
+ // is wrapped in a module wrapper (to keep vars local and allow you to require() one file in another).
+ // The 'system' directory is a special case - any module placed in this dir will not have a leading slash
+ wrapCode: function wrapCode(code, path, pathPrefix) {
+ var pathAry = path.split('/');
+
+ // Don't touch the code if it's in a 'libs' directory
+ if (pathAry.indexOf('libs') >= 0) {
+ return code;
+ }
+
+ if (pathAry.indexOf('entry.js') === -1 && options && options.browserifyExcludePaths) {
+ //TODO is this an array? should be revised
+ for(var p in options.browserifyExcludePaths) {
+ if (options.browserifyExcludePaths.hasOwnProperty(p)) {
+ if ( path.split( options.browserifyExcludePaths[p] )[0] === '' ) {
+ return code;
+ }
+ }
+ }
+ }
+
+ // Don't add a leading slash if this is a 'system' module
+ if (pathAry.indexOf('system') >= 0) {
+ return ss.bundler.wrapModule(pathAry[pathAry.length - 1], code);
+ } else {
+
+ // Otherwise treat as a regular module
+ var modPath = /*options.globalModules? pathAry.join("/") :*/ pathAry.slice(1).join('/');
+
+ // Work out namespace for module
+ if (pathPrefix) {
+
+ // Ignore any filenames in the path
+ if (pathPrefix.indexOf('.') > 0) {
+ var sp = pathPrefix.split('/');
+ sp.pop();
+ pathPrefix = sp.join('/');
+ }
+ modPath = path.substr(pathPrefix.length + 1);
+ }
+ return ss.bundler.wrapModule('/' + modPath, code);
+ }
+ }
+
+ };
+};
+
+function deleteOldFiles(clientDir) {
+ var filesDeleted = fs.readdirSync(clientDir).map(function(fileName) {
+ return fs.unlinkSync(path.join(clientDir, fileName));
+ });
+ return filesDeleted.length > 1 && log('✓'.green, '' + filesDeleted.length + ' previous packaged files deleted');
+}
+
+function mkdir(dir) {
+ if (!fs.existsSync(dir)) {
+ return fs.mkdirSync(dir);
+ }
+}
+
+function formatKb(size) {
+ return '' + (Math.round((size / 1024) * 1000) / 1000) + ' KB';
+}
+
+// Private
+function minifyJS(originalCode) {
+ var ast, jsp, pro;
+ jsp = uglifyjs.parser;
+ pro = uglifyjs.uglify;
+ ast = jsp.parse(originalCode);
+ ast = pro.ast_mangle(ast);
+ ast = pro.ast_squeeze(ast);
+ return pro.gen_code(ast) + ';';
+}
diff --git a/lib/client/bundler/webpack.js b/lib/client/bundler/webpack.js
new file mode 100644
index 00000000..8c82a64f
--- /dev/null
+++ b/lib/client/bundler/webpack.js
@@ -0,0 +1,192 @@
+// Webpack bundler implementation
+'use strict';
+
+//var fs = require('fs'),
+// path = require('path'),
+// log = require('../../utils/log');
+
+/**
+ * @typedef { name:string, path:string, dir:string, content:string, options:string, type:string } AssetEntry
+ */
+
+/**
+ * @ngdoc service
+ * @name bundler.webpack:webpack
+ * @function
+ *
+ * @description
+ * The webpack bundler of HTML, CSS & JS
+ *
+ * This is just for demonstration purposes and to validate the custom bundler concept. It can be improved.
+ */
+module.exports = function(webpack) {
+ return function(ss,options){
+ var bundler = {
+ define: define,
+ load: load,
+ toMinifiedCSS: toMinifiedCSS,
+ toMinifiedJS: toMinifiedJS,
+ asset: {
+ entries: entries,
+
+ html: assetHTML,
+ loader: assetLoader,
+ systemModule: systemModule,
+ js: assetJS,
+ worker: assetWorker,
+ start: assetStart,
+ css: assetCSS
+ }
+ };
+
+ /**
+ *
+ * @param client
+ * @param paths
+ * @returns {*}
+ */
+ function define(client, paths) {
+
+ if (typeof paths.view !== 'string') {
+ throw new Error('You may only define one HTML view per single-page client. Please pass a filename as a string, not an Array');
+ }
+ if (paths.view.indexOf('.') === -1) {
+ throw new Error('The \'' + paths.view + '\' view must have a valid HTML extension (such as .html or .jade)');
+ }
+
+ bundler.client = client;
+
+ // Define new client object
+ client.paths = ss.bundler.sourcePaths(paths);
+
+ return ss.bundler.destsFor(client);
+ }
+
+ function load() {
+
+ }
+
+ /**
+ * @ngdoc method
+ * @name bundler.webpack:default#entries
+ * @methodOf bundler.webpack:webpack
+ * @function
+ * @description
+ * Provides the view and the pack functions with a
+ * list of entries for an asset type relative to the client directory.
+ *
+ * @param {String} assetType js/css
+ * @param {Object} systemAssets Collection of libs, modules, initCode
+ * @returns {[AssetEntry]} List of output entries
+ */
+ function entries(assetType,systemAssets) {
+ return ss.bundler.entries(bundler.client, assetType);
+ }
+
+ /**
+ *
+ * @param path
+ * @param opts
+ * @param cb
+ * @returns {*}
+ */
+ function assetCSS(path, opts, cb) {
+ return ss.bundler.loadFile(path, 'css', opts, cb);
+ }
+
+ /**
+ *
+ * @param path
+ * @param opts
+ * @param cb
+ * @returns {*}
+ */
+ function assetHTML(path, opts, cb) {
+ return ss.bundler.loadFile(path, 'html', opts, cb);
+ }
+
+ /**
+ *
+ * @param cb
+ */
+ function assetLoader() {
+ return { type: 'loader', names: [], content: ';/* loader */' };
+ }
+
+ /**
+ *
+ * @param name
+ * @param content
+ * @param options
+ * @returns {boolean}
+ */
+ function systemModule(name) {
+ switch(name) {
+ case "eventemitter2":
+ case "socketstream":
+ default:
+ //if (client.includes.system) {
+ return ss.bundler.systemModule(name)
+ //}
+ }
+ }
+
+
+ /**
+ *
+ * @param path
+ * @param opts
+ * @param cb
+ */
+ function assetJS(path, opts, cb) {
+ webpack({}, function() {
+ cb('//');
+ });
+
+ }
+
+ /**
+ *
+ * @param cb
+ * @returns {*}
+ */
+ function assetStart() {
+ var output = ss.bundler.startCode(bundler.client);
+ return output;
+ }
+
+ /**
+ *
+ * @param path
+ * @param opts
+ * @param cb
+ */
+ function assetWorker(path, opts, cb) {
+ webpack({}, function() {
+ cb('//');
+ });
+ }
+
+ /**
+ *
+ * @param files
+ * @returns {*}
+ */
+ function toMinifiedCSS(files) {
+ return ss.bundler.minifyCSS(files);
+ }
+
+ /**
+ *
+ * @param files
+ * @returns {string}
+ */
+ function toMinifiedJS() {
+ return '// minified JS for '+bundler.client.name;
+ }
+
+ return bundler;
+ };
+
+};
+
diff --git a/lib/client/http.js b/lib/client/http.js
index b8e1e25e..49ec4fa1 100644
--- a/lib/client/http.js
+++ b/lib/client/http.js
@@ -30,9 +30,9 @@ module.exports = function(ss, clients, options) {
// Append the 'serveClient' method to the HTTP Response object
res.serveClient = function(name) {
- var client, fileName, self, sendHTML;
- self = this;
- sendHTML = function(html, code) {
+ var self = this;
+
+ function sendHTML(html, code) {
if (!code) {
code = 200;
}
@@ -45,9 +45,10 @@ module.exports = function(ss, clients, options) {
self.setHeader('Content-Length', Buffer.byteLength(html));
self.setHeader('Content-Type', 'text/html; charset=UTF-8');
self.end(html);
- };
+ }
+
try {
- client = typeof name === 'string' && clients[name];
+ var client = typeof name === 'string' && clients[name];
if (!client) {
throw new Error('Unable to find single-page client: ' + name);
}
@@ -57,7 +58,7 @@ module.exports = function(ss, clients, options) {
// Return from in-memory cache if possible
if (!cache[name]) {
- fileName = pathlib.join(ss.root, options.dirs.assets, client.name, client.id + '.html');
+ var fileName = pathlib.join(ss.root, options.dirs.assets, client.name, client.id + '.html');
cache[name] = fs.readFileSync(fileName, 'utf8');
}
diff --git a/lib/client/index.js b/lib/client/index.js
index baf68d24..eaf55193 100644
--- a/lib/client/index.js
+++ b/lib/client/index.js
@@ -10,6 +10,7 @@ require('colors');
var fs = require('fs'),
path = require('path'),
+ shortid = require('shortid'),
log = require('../utils/log'),
systemAssets = require('./system');
@@ -20,7 +21,9 @@ var packAssets = process.env['SS_PACK'];
var options = {
packedAssets: packAssets || false,
liveReload: ['code', 'css', 'static', 'templates', 'views'],
+ defaultEntryInit: 'require("/entry");',
dirs: {
+ client: '/client',
code: '/client/code',
css: '/client/css',
static: '/client/static',
@@ -34,25 +37,21 @@ var options = {
// Store each client as an object
var clients = {};
-function includeFlags(overrides) {
- var includes = {
- css: true,
- html: true,
- system: true,
- initCode: true
- }, n;
- if (overrides) {
- for (n in overrides) {
- if (overrides.hasOwnProperty(n)) {
- includes[n] = overrides[n];
- }
- }
- }
- return includes;
-}
-
+/**
+ * @ngdoc service
+ * @name ss.client:client
+ * @function
+ *
+ * @description
+ * Client serving, bundling, development, building.
+ * -----------
+ * One or more clients are defined and will be served in production as a single HTML, CSS, and JS file.
+ */
module.exports = function(ss, router) {
+ // make bundler methods available for default and other implementations
+ ss.bundler = require('./bundler/index')(ss,options);
+
// Require sub modules
var templateEngine = require('./template_engine')(ss),
formatters = require('./formatters')(ss),
@@ -66,6 +65,8 @@ module.exports = function(ss, router) {
// Very basic check to see if we can find pre-packed assets
// TODO: Improve to test for complete set
+ //TODO: Update for new id scheme
+ //TODO: move to bundler
function determineLatestId(client) {
var files, id, latestId;
try {
@@ -147,39 +148,28 @@ module.exports = function(ss, router) {
if (clients[name]) {
throw new Error('Client name \'' + name + '\' has already been defined');
}
- if (typeof paths.view !== 'string') {
- throw new Error('You may only define one HTML view per single-page client. Please pass a filename as a string, not an Array');
- }
- if (paths.view.indexOf('.') === -1) {
- throw new Error('The \'' + paths.view + '\' view must have a valid HTML extension (such as .html or .jade)');
- }
+ // if a function is used construct a bundler with it otherwise use default bundler
+ var client = clients[name] = { name: name };
+ client.id = shortid.generate();
+ client.paths = {};
+ client.includes = {
+ css: true,
+ html: true,
+ system: true,
+ initCode: true
+ };
- // Alias 'templates' to 'tmpl'
- if (paths.templates) {
- paths.tmpl = paths.templates;
- }
+ //TODO reconsider relative paths of all these
+ client.entryInitPath = './code/' + client.name + '/entry';
- // Force each into an array
- ['css', 'code', 'tmpl'].forEach(function(assetType) {
- if (!(paths[assetType] instanceof Array)) {
- paths[assetType] = [paths[assetType]];
- return paths[assetType];
- }
- });
-
- // Define new client object
- clients[name] = {
- id: Number(Date.now()),
- name: name,
- paths: paths,
- includes: includeFlags(paths.includes)
- };
- return clients[name];
+ ss.bundler.define(client,arguments);
+
+ return client;
},
// Listen and serve incoming asset requests
load: function() {
- var client, id, name, pack, entryInit;
+ ss.bundler.load();
// Cache instances of code formatters and template engines here
// This may change in the future as I don't like hanging system objects
@@ -189,13 +179,7 @@ module.exports = function(ss, router) {
ss.client.templateEngines = templateEngine.load();
// Code to execute once everything is loaded
- entryInit = 'require("/entry");';
- if (typeof options.entryModuleName === 'string' || options.entryModuleName === null) {
- entryInit = options.entryModuleName? 'require("/'+options.entryModuleName+'");' : '';
- }
- if (entryInit) {
- systemAssets.send('code', 'init', entryInit);
- }
+ systemAssets.send('code', 'init', options.defaultEntryInit);
if (options.packedAssets) {
@@ -203,10 +187,10 @@ module.exports = function(ss, router) {
// If unsuccessful, assets will be re-packed automatically
if (!packAssets) {
log.info('i'.green, 'Attempting to find pre-packed assets... (force repack with SS_PACK=1)'.grey);
- for (name in clients) {
+ for (var name in clients) {
if (clients.hasOwnProperty(name)) {
- client = clients[name];
- id = options.packedAssets.id || determineLatestId(client);
+ var client = clients[name],
+ id = options.packedAssets.id || determineLatestId(client);
if (id) {
client.id = id;
log.info('✓'.green, ('Serving client \'' + client.name + '\' using pre-packed assets (ID ' + client.id + ')').grey);
@@ -220,11 +204,9 @@ module.exports = function(ss, router) {
// Pack Assets
if (packAssets) {
- pack = require('./pack');
- for (name in clients) {
+ for (var name in clients) {
if (clients.hasOwnProperty(name)) {
- client = clients[name];
- pack(ss, client, options);
+ ss.bundler.pack(clients[name]);
}
}
}
@@ -237,6 +219,15 @@ module.exports = function(ss, router) {
}
// Listen out for requests to async load new assets
return require('./serve/ondemand')(ss, router, options);
+ },
+
+ unload: function() {
+
+ ss.bundler.unload();
+ },
+
+ forget: function() {
+ clients = {};
}
};
};
diff --git a/lib/client/live_reload.js b/lib/client/live_reload.js
index f57fcdb9..f0b64998 100644
--- a/lib/client/live_reload.js
+++ b/lib/client/live_reload.js
@@ -3,8 +3,7 @@
// Detects changes in client files and sends an event to connected browsers instructing them to refresh the page
'use strict';
-var chokidar, consoleMessage, cssExtensions, lastRun, pathlib, log,
- __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) {return i;}} return -1; };
+var chokidar, consoleMessage, cssExtensions, lastRun, pathlib, log;
require('colors');
@@ -60,7 +59,7 @@ module.exports = function(ss, options) {
//
onChange = function (path) {
var action, _ref;
- action = (_ref = pathlib.extname(path), __indexOf.call(cssExtensions, _ref) >= 0) ? 'updateCSS' : 'reload';
+ action = (_ref = pathlib.extname(path), cssExtensions.indexOf(_ref) >= 0) ? 'updateCSS' : 'reload';
if ((Date.now() - lastRun[action]) > 1000) { // Reload browser max once per second
log.info('✎'.green, consoleMessage[action].grey);
ss.publish.all('__ss:' + action);
diff --git a/lib/client/magic_path.js b/lib/client/magic_path.js
index 8e156374..2d86d11d 100644
--- a/lib/client/magic_path.js
+++ b/lib/client/magic_path.js
@@ -7,6 +7,7 @@
require('colors');
var fileUtils = require('../utils/file'),
+ pathlib = require('path'),
log = require('../utils/log');
exports.files = function(prefix, paths) {
@@ -23,11 +24,11 @@ exports.files = function(prefix, paths) {
var dir, sp, tree;
sp = path.split('/');
if (sp[sp.length - 1].indexOf('.') > 0) {
- return files.push(path);
+ return files.push(path); // explicit (seems like a very weak test)
} else {
dir = prefix;
if (path !== '*') {
- dir += '/' + path;
+ dir = pathlib.join(dir, path);
}
tree = fileUtils.readDirSync(dir);
if (tree) {
diff --git a/lib/client/pack.js b/lib/client/pack.js
deleted file mode 100644
index 1d793a4c..00000000
--- a/lib/client/pack.js
+++ /dev/null
@@ -1,133 +0,0 @@
-// Asset Packer
-// ------------
-// Packs all CSS, JS and HTML assets declared in the ss.client.define() call to be sent upon initial connection
-// Other code modules can still be served asynchronously later on
-'use strict';
-
-require('colors');
-
-var fs = require('fs'),
- pathlib = require('path'),
- cleanCSS = require('clean-css'),
- magicPath = require('./magic_path'),
- system = require('./system'),
- view = require('./view'),
- log = require('../utils/log');
-
-module.exports = function(ss, client, options) {
- var asset, clientDir, containerDir, packAssetSet;
- asset = require('./asset')(ss, options);
- client.pack = true;
- containerDir = pathlib.join(ss.root, options.dirs.assets);
- clientDir = pathlib.join(containerDir, client.name);
- packAssetSet = function(assetType, paths, dir, postProcess) {
- var filePaths, prefix, processFiles, writeFile;
- writeFile = function(fileContents) {
- var fileName;
- fileName = clientDir + '/' + client.id + '.' + assetType;
- fs.writeFileSync(fileName, postProcess(fileContents));
- return log.info('✓'.green, 'Packed ' + filePaths.length + ' files into ' + fileName.substr(ss.root.length));
- };
- processFiles = function(fileContents, i) {
- var file, path, _ref;
- if (!fileContents) {
- fileContents = [];
- }
- if (!i) {
- i = 0;
- }
- _ref = filePaths[i];
- path = _ref.path;
- file = _ref.file;
- return asset[assetType](file, {
- pathPrefix: path,
- compress: true
- }, function(output) {
- fileContents.push(output);
- if (filePaths[++i]) {
- return processFiles(fileContents, i);
- } else {
- return writeFile(fileContents);
- }
- });
- };
-
- // Expand any dirs into real files
- if (paths && paths.length > 0) {
- filePaths = [];
- prefix = pathlib.join(ss.root, dir);
- paths.forEach(function(path) {
- return magicPath.files(prefix, path).forEach(function(file) {
- return filePaths.push({
- path: path,
- file: file
- });
- });
- });
- return processFiles();
- }
- };
-
- /* PACKER */
-
- log(('Pre-packing and minifying the \'' + client.name + '\' client...').yellow);
-
- // Prepare folder
- mkdir(containerDir);
- mkdir(clientDir);
- if (!(options.packedAssets && options.packedAssets.keepOldFiles)) {
- deleteOldFiles(clientDir);
- }
-
- // Output CSS
- packAssetSet('css', client.paths.css, options.dirs.css, function(files) {
- var minified, original;
- original = files.join('\n');
- minified = cleanCSS().minify(original);
- log.info((' Minified CSS from ' + (formatKb(original.length)) + ' to ' + (formatKb(minified.length))).grey);
- return minified;
- });
-
- // Output JS
- packAssetSet('js', client.paths.code, options.dirs.code, function(files) {
- var parts = [];
- if (client.includes.system) {
- parts.push( system.serve.js({ compress:true }) );
- }
- parts = parts.concat(files);
- if (client.includes.initCode) {
- parts.push( system.serve.initCode() );
- }
-
- return parts.join(';');
- });
-
- // Output HTML view
- return view(ss, client, options, function(html) {
- var fileName;
- fileName = pathlib.join(clientDir, client.id + '.html');
- fs.writeFileSync(fileName, html);
- return log.info('✓'.green, 'Created and cached HTML file ' + fileName.substr(ss.root.length));
- });
-};
-
-// PRIVATE
-
-function formatKb(size) {
- return '' + (Math.round((size / 1024) * 1000) / 1000) + ' KB';
-}
-
-function mkdir(dir) {
- if (!fs.existsSync(dir)) {
- return fs.mkdirSync(dir);
- }
-}
-
-function deleteOldFiles(clientDir) {
- var filesDeleted, numFilesDeleted;
- numFilesDeleted = 0;
- filesDeleted = fs.readdirSync(clientDir).map(function(fileName) {
- return fs.unlinkSync(pathlib.join(clientDir, fileName));
- });
- return filesDeleted.length > 1 && log('✓'.green, '' + filesDeleted.length + ' previous packaged files deleted');
-}
diff --git a/lib/client/serve/dev.js b/lib/client/serve/dev.js
index d00d0876..6ca319c8 100644
--- a/lib/client/serve/dev.js
+++ b/lib/client/serve/dev.js
@@ -11,39 +11,66 @@ var url = require('url'),
// Expose asset server as the public API
//
module.exports = function (ss, router, options) {
- var asset;
- asset = require('../asset')(ss, options);
- // JAVASCRIPT
+ // JAVASCRIPT
// Serve system libraries and modules
router.on('/_serveDev/system?*', function(request, response) {
- return utils.serve.js(system.serve.js(), response);
+ var thisUrl = url.parse(request.url),
+ params = qs.parse(thisUrl.query),
+ moduleName = utils.parseUrl(request.url);
+
+ // no module name (probably ts=..)
+ if (moduleName.indexOf('=') >= 0) {
+ var loader = ss.bundler.get(params).asset.loader() || {},
+ namesComment = '/* ' + loader.names.join(',') + ' */';
+ utils.serve.js(namesComment+'\n'+loader.content || '', response);
+ }
+
+ // module
+ else {
+ var module = ss.bundler.get(params).asset.systemModule(moduleName) || {};
+ utils.serve.js(module.content || '', response);
+ }
});
+ //TODO bundler calculates entries. view builds according to entries. formatter is predetermined
+
// Listen for requests for application client code
router.on('/_serveDev/code?*', function(request, response) {
- var params, path, thisUrl;
- thisUrl = url.parse(request.url);
- params = qs.parse(thisUrl.query);
- path = utils.parseUrl(request.url);
- return asset.js(path, {
+ var thisUrl = url.parse(request.url),
+ params = qs.parse(thisUrl.query),
+ path = utils.parseUrl(request.url);
+
+ return ss.bundler.get(params).asset.js(path, {
+ //TODO formatter: params.formatter,
+ client: params.client,
+ clientId: params.ts,
pathPrefix: params.pathPrefix
}, function(output) {
return utils.serve.js(output, response);
});
});
router.on('/_serveDev/start?*', function(request, response) {
- return utils.serve.js(system.serve.initCode(), response);
+ var thisUrl = url.parse(request.url),
+ params = qs.parse(thisUrl.query);
+
+ var start = ss.bundler.get(params).asset.start() || {};
+ return utils.serve.js(start.content || '', response);
});
// CSS
// Listen for requests for CSS files
return router.on('/_serveDev/css?*', function(request, response) {
- var path;
+ var params, path, thisUrl;
+ thisUrl = url.parse(request.url);
+ params = qs.parse(thisUrl.query);
path = utils.parseUrl(request.url);
- return asset.css(path, {}, function(output) {
+ return ss.bundler.get(params).asset.css(path, {
+ client: params.client,
+ clientId: params.ts
+ }, function(output) {
return utils.serve.css(output, response);
});
});
diff --git a/lib/client/serve/ondemand.js b/lib/client/serve/ondemand.js
index 1a09de97..0ada457e 100644
--- a/lib/client/serve/ondemand.js
+++ b/lib/client/serve/ondemand.js
@@ -4,29 +4,26 @@
// ----------------------
// Serves assets to browsers on demand, caching responses in production mode
-var magicPath, pathlib, queryCache, utils, log;
-
require('colors');
-pathlib = require('path');
-
-magicPath = require('../magic_path');
-
-log = require('../../utils/log');
-
-utils = require('./utils');
+var url = require('url'),
+ qs = require('querystring'),
+ pathlib = require('path'),
+ magicPath = require('../magic_path'),
+ log = require('../../utils/log'),
+ utils = require('./utils');
// When packing assets, cache responses to each query in RAM to avoid
// having to re-compile and minify assets. TODO: Add limits/purging
-queryCache = {};
+var queryCache = {};
module.exports = function(ss, router, options) {
- var asset, code, serve, worker;
- asset = require('../asset')(ss, options);
- serve = function(processor) {
+
+ var bundler = require('../bundler/index')(ss,options);
+
+ function serve(processor) {
return function(request, response) {
- var path;
- path = utils.parseUrl(request.url);
+ var path = utils.parseUrl(request.url);
if (options.packAssets && queryCache[path]) {
return utils.serve.js(queryCache[path], response);
} else {
@@ -36,19 +33,23 @@ module.exports = function(ss, router, options) {
});
}
};
- };
+ }
// Async Code Loading
- code = function(request, response, path, cb) {
- var dir, files, output;
- output = [];
- dir = pathlib.join(ss.root, options.dirs.code);
- files = magicPath.files(dir, [path]);
+ function code(request, response, path, cb) {
+ var output = [],
+ thisUrl = url.parse(request.url),
+ params = qs.parse(thisUrl.query),
+ dir = pathlib.join(ss.root, options.dirs.client),
+ files = magicPath.files(dir, [path]);
+
return files.forEach(function(file) {
var description;
try {
- return asset.js(file, {
- pathPrefix: options.globalModules? null : path,
+ return bundler.get(params).asset.js(file, {
+ client: params.client,
+ clientId: params.ts,
+ //pathPrefix: options.globalModules? null : path,
compress: options.packAssets
}, function(js) {
output.push(js);
@@ -61,16 +62,20 @@ module.exports = function(ss, router, options) {
return log.error(('! Unable to load ' + file + ' on demand:').red, description);
}
});
- };
+ }
// Web Workers
- worker = function(request, response, path, cb) {
- return asset.worker(path, {
+ function worker(request, response, path, cb) {
+ var thisUrl = url.parse(request.url),
+ params = qs.parse(thisUrl.query);
+
+ return bundler.get(params).asset.worker(path, {
compress: options.packAssets
}, cb);
- };
+ }
// Bind to routes
router.on('/_serve/code?*', serve(code));
return router.on('/_serve/worker?*', serve(worker));
};
+
diff --git a/lib/client/system/index.js b/lib/client/system/index.js
index e7ba376b..fa82128b 100644
--- a/lib/client/system/index.js
+++ b/lib/client/system/index.js
@@ -4,24 +4,17 @@
// -------------
// Loads system libraries and modules for the client. Also exposes an internal API
// which other modules can use to send system assets to the client
-var assets, fs, fsUtils, minifyJS, pathlib, send, uglifyjs, wrap;
-fs = require('fs');
-
-pathlib = require('path');
-
-uglifyjs = require('uglify-js');
-
-wrap = require('../wrap');
-
-fsUtils = require('../../utils/file');
+var fs = require('fs'),
+ pathlib = require('path'),
+ uglifyjs = require('uglify-js'),
+ fsUtils = require('../../utils/file');
// Allow internal modules to deliver assets to the browser
-assets = {
- shims: [],
+var assets = exports.assets = {
libs: [],
modules: {},
- initCode: []
+ startCode: []
};
function pushUniqueAsset(listName,asset) {
@@ -34,24 +27,34 @@ function pushUniqueAsset(listName,asset) {
return list.push(asset);
}
-// API to add new System Library or Module
-exports.send = send = function (type, name, content, options) {
+/**
+ * @ngdoc function
+ * @name ss.client:client#send
+ * @methodOf ss.client:client
+ * @parma {'code','lib','module'} type - `code`, `lib`, `module`.
+ * @param {string} name - Module name for require.
+ * @param {string} content - The JS code
+ * @param {Object} options - Allows you to specify `compress` and `coffee` format flags.
+ * @description
+ * Allow other libs to send assets to the client. add new System Library or Module
+ */
+
+var send = exports.send = function (type, name, content, options) {
if (options === null || options === undefined) {
options = {};
}
+
switch (type) {
+ case 'start':
case 'code':
- return assets.initCode.push(content);
- case 'shim':
- return pushUniqueAsset('shims',{
- name: name,
- content: content,
- options: options
- });
+ return assets.startCode.push({content:content,options:options, type:'start'});
case 'lib':
case 'library':
return pushUniqueAsset('libs',{
name: name,
+ type: type,
+ dir: pathlib.join(__dirname,'libs'),
+ path: pathlib.join(__dirname,'libs',name + '.js'),
content: content,
options: options
});
@@ -60,7 +63,12 @@ exports.send = send = function (type, name, content, options) {
if (assets.modules[name]) {
throw new Error('System module name \'' + name + '\' already exists');
} else {
+ name = name.replace(/\.js$/,'');
assets.modules[name] = {
+ name: name,
+ type: type,
+ dir: pathlib.join(__dirname,'modules'),
+ path: pathlib.join(__dirname,'modules',name + '.js'),
content: content,
options: options
};
@@ -70,32 +78,15 @@ exports.send = send = function (type, name, content, options) {
};
exports.unload = function() {
- assets.shims = [];
assets.libs = [];
assets.modules = {};
- assets.initCode = [];
+ assets.startCode = [];
};
// Load all system libs and modules
exports.load = function() {
var modDir;
- // System shims for backwards compatibility with all browsers.
- // Load order is not important
- modDir = pathlib.join(__dirname, '/shims');
- fsUtils.readDirSync(modDir).files.forEach(function(fileName) {
- var code, extension, modName, sp, preMinified;
- code = fs.readFileSync(fileName, 'utf8');
- sp = fileName.split('.');
- extension = sp[sp.length - 1];
- preMinified = fileName.indexOf('.min') >= 0;
- modName = fileName.substr(modDir.length + 1);
- return send('shim', modName, code, {
- minified: preMinified,
- coffee: extension === 'coffee'
- });
- });
-
// System Libs. Including browserify client code
// Load order is not important
modDir = pathlib.join(__dirname, '/libs');
@@ -118,59 +109,9 @@ exports.load = function() {
code = fs.readFileSync(fileName, 'utf8');
sp = fileName.split('.');
extension = sp[sp.length - 1];
- modName = fileName.substr(modDir.length + 1);
+ modName = fileName.substr(modDir.length + 1).replace('.js','').replace('.min.js','');
return send('mod', modName, code, {
coffee: extension === 'coffee'
});
});
};
-
-// Serve system assets
-exports.serve = {
- js: function (options) {
- var code, mod, name, output, _ref;
- if (options === null || options === undefined) {
- options = {};
- }
-
- // Shims
- output = assets.shims.map(function(code) {
- return options.compress && !code.options.minified && minifyJS(code.content) || code.content;
- });
-
- // Libs
- output = output.concat(assets.libs.map(function(code) {
- return options.compress && !code.options.minified && minifyJS(code.content) || code.content;
- }));
-
- // Modules
- _ref = assets.modules;
- for (name in _ref) {
- if (_ref.hasOwnProperty(name)) {
-
- mod = _ref[name];
- code = wrap.module(name, mod.content);
- if (options.compress && !mod.options.minified) {
- code = minifyJS(code);
- }
- output.push(code);
-
- }
- }
- return output.join('\n');
- },
- initCode: function() {
- return assets.initCode.join(' ');
- }
-};
-
-// Private
-minifyJS = function(originalCode) {
- var ast, jsp, pro;
- jsp = uglifyjs.parser;
- pro = uglifyjs.uglify;
- ast = jsp.parse(originalCode);
- ast = pro.ast_mangle(ast);
- ast = pro.ast_squeeze(ast);
- return pro.gen_code(ast) + ';';
-};
diff --git a/lib/client/system/shims/json.min.js b/lib/client/system/shims/json.min.js
deleted file mode 100644
index 73be3910..00000000
--- a/lib/client/system/shims/json.min.js
+++ /dev/null
@@ -1,18 +0,0 @@
-if(!this.JSON){JSON=function(){function f(n){return n<10?'0'+n:n;}
-Date.prototype.toJSON=function(){return this.getUTCFullYear()+'-'+
-f(this.getUTCMonth()+1)+'-'+
-f(this.getUTCDate())+'T'+
-f(this.getUTCHours())+':'+
-f(this.getUTCMinutes())+':'+
-f(this.getUTCSeconds())+'Z';};var m={'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'};function stringify(value,whitelist){var a,i,k,l,r=/["\\\x00-\x1f\x7f-\x9f]/g,v;switch(typeof value){case'string':return r.test(value)?'"'+value.replace(r,function(a){var c=m[a];if(c){return c;}
-c=a.charCodeAt();return'\\u00'+Math.floor(c/16).toString(16)+
-(c%16).toString(16);})+'"':'"'+value+'"';case'number':return isFinite(value)?String(value):'null';case'boolean':case'null':return String(value);case'object':if(!value){return'null';}
-if(typeof value.toJSON==='function'){return stringify(value.toJSON());}
-a=[];if(typeof value.length==='number'&&!(value.propertyIsEnumerable('length'))){l=value.length;for(i=0;i';
- },
- js: function(path) {
- return '';
- }
-};
\ No newline at end of file
diff --git a/lib/socketstream.js b/lib/socketstream.js
index 82356e18..750a7ff9 100644
--- a/lib/socketstream.js
+++ b/lib/socketstream.js
@@ -32,16 +32,50 @@ var session = exports.session = require('./session');
// logging
var log = require('./utils/log');
-// Create an internal API object which is passed to sub-modules and can be used within your app
+/**
+ * @ngdoc overview
+ * @name ss
+ * @description
+ * Internal API object which is passed to sub-modules and can be used within your app
+ *
+ * To access it without it being passed `var ss = require('socketstream').api;`
+ *
+ * @type {{version: *, root: *, env: string, log: (*|exports), session: exports, add: Function}}
+ */
var api = exports.api = {
+ /**
+ * @ngdoc property
+ * @name ss.version
+ * @returns {number} major.minor
+ */
version: version,
+ /**
+ * @ngdoc property
+ * @name ss.root
+ * @description
+ * By default the project root is the current working directory
+ * @returns {string} Project root
+ */
root: root,
+ /**
+ * @ngdoc property
+ * @name ss.env
+ * @returns {string} Execution environment type. To change set environment variable `NODE_ENV` or `SS_ENV`. 'development' by default.
+ */
env: env,
+
log: log,
session: session,
- // Call ss.api.add('name_of_api', value_or_function) from your app to safely extend the 'ss' internal API object passed through to your /server code
+ /**
+ * @ngdoc function
+ * @name ss.add
+ * @param {string} name - Key in the `ss` API.
+ * @param {function|number|boolean|string} fn - value or function
+ * @description
+ * Call from your app to safely extend the 'ss' internal API object passed through to your /server code
+ */
add: function(name, fn) {
if (api[name]) {
throw new Error('Unable to register internal API extension \'' + name + '\' as this name has already been taken');
@@ -52,8 +86,16 @@ var api = exports.api = {
}
};
-// Create internal Events bus
-// Note: only used by the ss-console module for now. This idea will be expended upon in SocketStream 0.4
+/**
+ * @ngdoc service
+ * @name events
+ * @description
+ * Internal Event bus.
+ *
+ * Note: only used by the ss-console module for now. This idea will be expended upon in SocketStream 0.4
+ *
+ * 'server:start' is emitted when the server starts. If in production the assets will be saved before the event.
+ */
var events = exports.events = new EventEmitter2();
// Publish Events
@@ -77,7 +119,13 @@ var ws = exports.ws = require('./websocket/index')(api, responders);
// Only one instance of the server can be started at once
var serverInstance = null;
-// Public API
+/**
+ * @ngdoc function
+ * @name start
+ * @param {HTTPServer} server Instance of the server from the http module
+ * @description
+ * Starts the development or production server
+ */
function start(httpServer) {
// Load SocketStream server instance
diff --git a/lib/utils/log.js b/lib/utils/log.js
index fabe53c4..a0065568 100644
--- a/lib/utils/log.js
+++ b/lib/utils/log.js
@@ -1,7 +1,7 @@
'use strict';
/**
* @ngdoc service
- * @name utils.log:log
+ * @name ss.log:log
* @function
*
* @description
@@ -11,8 +11,8 @@
/**
* @ngdoc service
- * @name utils.log#debug
- * @methodOf utils.log:log
+ * @name ss.log#debug
+ * @methodOf ss.log:log
* @function
*
* @description
@@ -25,14 +25,14 @@
*
* @example
* ```
- * ss.api.log.debug("Something fairly trivial happened");
+ * ss.log.debug("Something fairly trivial happened");
* ```
*/
/**
* @ngdoc service
- * @name utils.log#info
- * @methodOf utils.log:log
+ * @name ss.log#info
+ * @methodOf ss.log:log
* @function
*
* @description
@@ -41,14 +41,14 @@
*
* @example
* ```
- * ss.api.log.info("Just keeping you informed");
+ * ss.log.info("Just keeping you informed");
* ```
*/
/**
* @ngdoc service
- * @name utils.log#warn
- * @methodOf utils.log:log
+ * @name ss.log#warn
+ * @methodOf ss.log:log
* @function
*
* @description
@@ -57,19 +57,19 @@
* ```
* var ss = require('socketstream'),
* winston = require('winston');
- * ss.api.log.warn = winston.warn;
+ * ss.log.warn = winston.warn;
* ```
*
* @example
* ```
- * ss.api.log.warn("Something unexpected happened!");
+ * ss.log.warn("Something unexpected happened!");
* ```
*/
/**
* @ngdoc service
- * @name utils.log#error
- * @methodOf utils.log:log
+ * @name ss.log#error
+ * @methodOf ss.log:log
* @function
*
* @description
@@ -78,7 +78,7 @@
*
* @example
* ```
- * ss.api.log.error("Time to wakeup the sysadmin");
+ * ss.log.error("Time to wakeup the sysadmin");
* ```
*/
module.exports = (function() {
diff --git a/lib/websocket/transports/engineio/index.js b/lib/websocket/transports/engineio/index.js
index 6b76c29d..cfb3501a 100644
--- a/lib/websocket/transports/engineio/index.js
+++ b/lib/websocket/transports/engineio/index.js
@@ -38,6 +38,9 @@ module.exports = function(ss, messageEmitter, httpServer, config){
// Tell the SocketStream client to use this transport, passing any client-side config along to the wrapper
ss.client.send('code', 'transport', "require('socketstream').assignTransport(" + JSON.stringify(config.client) + ");");
+ // don't set up server for CLI and test
+ if (httpServer == null) return;
+
// Create a new Engine.IO server and bind to /ws
ws = engine.attach(httpServer, config.server);
// ws.installHandlers(httpServer, {prefix: '/ws'});
diff --git a/package.json b/package.json
index 5e9b8d6f..40c5b067 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,7 @@
"redis": "= 0.12.1",
"semver": "= 4.2.0",
"send": "0.11.0",
+ "shortid": "^2.1.3",
"uglify-js": "= 1.3.3",
"uid2": "0.0.3",
"utils-merge": "1.0.0"
diff --git a/src/docs/tutorials/en/client_side_code.ngdoc b/src/docs/tutorials/en/client_side_code.ngdoc
index 811abf02..40b7773e 100644
--- a/src/docs/tutorials/en/client_side_code.ngdoc
+++ b/src/docs/tutorials/en/client_side_code.ngdoc
@@ -70,10 +70,6 @@ code for the entry module. If null or a blank string is given no entry module is
ss.client.set({ entryModuleName: null });
-- **globalModules {boolean} ** - set true to load client side modules using their full path relative to `client/code`. If for example your app is `my` the entry module can be accessed with `require('/my/entry')`.
-
-ss.client.set({ globalModules: true });
-
**Note**, that paths for excluding should be relative to `client/code/` and that file `client/code/app/entry.js` could not be excluded in any cases.
diff --git a/src/docs/tutorials/en/client_side_development.ngdoc b/src/docs/tutorials/en/client_side_development.ngdoc
new file mode 100644
index 00000000..69ea5bac
--- /dev/null
+++ b/src/docs/tutorials/en/client_side_development.ngdoc
@@ -0,0 +1,20 @@
+@ngdoc overview
+@name Client-Side Development
+
+@description
+# Client-Side Development
+
+Each view is served with a separate bundle of assets. A HTML, JS and CSS file makes up the view.
+
+Each entry is served separately to the browser injected in the HTML on the fly.
+
+* A relative path is given in the URL, relative to the "/client" directory.
+* The client is given in the URL to determine the bundler.
+* The asset type is specified in the URL to determine the bundler.
+* A timestamp is given in the URL to break any caching.
+
+The URL pattern is `http:///_serveDev//?ts=`.
+
+Bundlers generally work within the client directory to reduce the amount of files to watch.
+
+
diff --git a/src/docs/tutorials/en/client_side_xbundler.ngdoc b/src/docs/tutorials/en/client_side_xbundler.ngdoc
new file mode 100644
index 00000000..78336444
--- /dev/null
+++ b/src/docs/tutorials/en/client_side_xbundler.ngdoc
@@ -0,0 +1,91 @@
+@ngdoc overview
+@name Client-Side Bundler
+
+@description
+# Client-Side Bundler
+
+Each view is served with a separate bundle of assets. A HTML, JS and CSS file makes up the view.
+The default bundler will create the bundle based on the client definition as described in Client-Side Code and Client-Side Templates.
+
+You can implement your own bundler and use it for a client definition. The bundlers are named and referenced by name.
+
+Be aware the *API is experimental*. The current bundler is based on an early Browserify implementation, so it lacks some
+features. The objective is to be able to move to bundling based on newer ones such as WebPack or JSPM. It should be possible
+to implement a bundler that completely changes how the client is implemented. Hence there will be additional responsibilities
+for bundlers in the future.
+
+### Custom Bundler
+
+You can define a custom bundler by implementing a bundler function that will be called for each client it is used on.
+
+ function webpackBundler(ss, options) {
+ var bundler = {};
+ bundler.define = function(client,config,next_arg...) {
+ // ...
+ return ss.bundler.destsFor(ss,client,options);
+ };
+ bundler.load = function() {};
+ bundler.asset = {
+ html: function(path, opts, cb) { cb(output) },
+ css: function(path, opts, cb) { cb(output) },
+ js: function(path, opts, cb) { cb(output) },
+ worker: function(path, opts, cb) { cb(output) }
+ };
+ bundler.pack = {
+ css: function(cb) { cb(output); },
+ js: function(cb) { cb(output); }
+ };
+
+ return bundler;
+ }
+
+You can use a custom bundler by for a client view by specifying it in the definition.
+
+ ss.client.define('discuss', webpackBundler, {
+ view: './views/discuss.jade',
+ css: './css/discuss.scss',
+ code: './code/discuss',
+ tmpl: './templates/discuss'
+ });
+
+The define method of the bundler will be called to complete `ss.client.define`.
+
+### Bundler Define `define(client,config,..)`
+
+The define method will be called with a client object containing `id`, `name`,
+If you pass additional arguments to define they will be passed to `bundler.define`. This may be dropped in the future.
+
+### Bundler Load `load()`
+
+The load method is called as the first step to load the client views. This is done as a bulk action as part of starting
+the server.
+
+### Bundler asset methods
+
+For each of the asset types supported individual files can be served during development.
+A callback function is passed, and must be called with the text contents.
+
+
+### Bundler pack methods
+
+Files are saved in the assets directory for production use. The HTML file is the same as the one used during development,
+so the `asset.html` method will be called. For JS and CSS the pack methods are called to do additional minification and
+other optimisations.
+
+### Bundler shorthand
+
+A lot of functionality is built-in. When you write your own bundler you shouldn't have to do it all over again. So most
+of the existing behaviour can be called through `ss.bundler`.
+
+##### `ss.bundler.sourcePaths(ss,paths,options)`
+
+This returns a revised paths object. Paths should contain entries `code`, `css`, `tmpl`. They will be forced into an array.
+If a path starts with "./", it is considered relative to "/client". Otherwise it is considered relative to "/client/code",
+"/client/css", "/client/templates". These directory options can be set using `ss.client.set`.
+
+ ss.client.set({
+ dirs: {
+ client: '/client',
+ code: '/client/code'
+ }
+ });
diff --git a/src/docs/tutorials/en/url_scheme.ngdoc b/src/docs/tutorials/en/url_scheme.ngdoc
new file mode 100644
index 00000000..1618772f
--- /dev/null
+++ b/src/docs/tutorials/en/url_scheme.ngdoc
@@ -0,0 +1,37 @@
+@ngdoc overview
+@name URL Scheme
+
+@description
+# URL Scheme
+
+The common URL for a view is its name at the root level, but you can choose whatever you will calling `serveClient(..)`.
+
+## Assets
+
+The contents of the client assets directory will be served under `/assets`.
+
+
+When views are packed for production they are saved under the client assets directory. This will change in the future
+to make relative URLs work the same in development and production.
+
+## Middleware
+
+At development time middleware is added to serve HTML, JS and CSS on the fly.
+
+
+## Serving CSS
+
+CSS files are served under /assets//123.css in production. When served ad hoc in development, and on-demand in
+production all CSS must be served on the same level and ideally in an equivalent URL.
+
+## JS Module Paths
+
+
+## On Demand Loading
+
+The current on-demand fetching of JS is handled by middleware. It should be possible to do it using static files.
+
+In production it would make sense to support a path like `/assets/require/..`.
+
+We will have to consider whether all client code is considered completely open, or only partially. Should all client
+modules be exported in minified form, or only those in a whitelist.
\ No newline at end of file
diff --git a/test/fixtures/project/client/abc/abc.html b/test/fixtures/project/client/abc/abc.html
new file mode 100644
index 00000000..4e35a57b
--- /dev/null
+++ b/test/fixtures/project/client/abc/abc.html
@@ -0,0 +1,4 @@
+
+ABC
+
ABC
+
\ No newline at end of file
diff --git a/test/fixtures/project/client/abc/index.js b/test/fixtures/project/client/abc/index.js
new file mode 100644
index 00000000..09d4352e
--- /dev/null
+++ b/test/fixtures/project/client/abc/index.js
@@ -0,0 +1 @@
+// test
diff --git a/test/fixtures/project/client/abc/style.css b/test/fixtures/project/client/abc/style.css
new file mode 100644
index 00000000..c84ecefc
--- /dev/null
+++ b/test/fixtures/project/client/abc/style.css
@@ -0,0 +1 @@
+/* */
\ No newline at end of file
diff --git a/test/fixtures/project/client/static/assets/info.txt b/test/fixtures/project/client/static/assets/info.txt
new file mode 100644
index 00000000..4b2944cc
--- /dev/null
+++ b/test/fixtures/project/client/static/assets/info.txt
@@ -0,0 +1 @@
+saving assets here during tests
diff --git a/test/unit/client/bundler/default.test.js b/test/unit/client/bundler/default.test.js
new file mode 100644
index 00000000..6314ab82
--- /dev/null
+++ b/test/unit/client/bundler/default.test.js
@@ -0,0 +1,128 @@
+'use strict';
+
+var path = require('path'),
+ should = require('should'),
+ ss = require( '../../../../lib/socketstream'),
+ options = ss.client.options;
+
+describe('default bundler', function () {
+
+ var origDefaultEntryInit = options.defaultEntryInit;
+
+ describe('define', function() {
+
+ it('should support default css/code/view/tmpl locations');
+
+ it('should support relative css/code/view/tmpl locations');
+
+ it('should set up client and bundler', function() {
+
+ //TODO set project root function
+ ss.root = ss.api.root = path.join(__dirname, '../../../fixtures/project');
+
+ var client = ss.client.define('abc', {
+ css: './abc/style.css',
+ code: './abc/index.js',
+ view: './abc/abc.html'
+ });
+
+ client.id.should.be.type('string');
+
+ client.paths.should.be.type('object');
+ client.paths.css.should.be.eql(['./abc/style.css']);
+ client.paths.code.should.be.eql(['./abc/index.js']);
+ client.paths.view.should.be.eql('./abc/abc.html');
+ client.paths.tmpl.should.be.eql([]);
+
+ client.includes.should.be.type('object');
+ client.includes.css.should.be.equal(true);
+ client.includes.html.should.be.equal(true);
+ client.includes.system.should.be.equal(true);
+ client.includes.initCode.should.be.equal(true);
+ client.entryInitPath.should.be.equal('./code/abc/entry');
+
+ var bundler = ss.api.bundler.get('abc');
+
+ bundler.dests.paths.html.should.be.equal( path.join(ss.root,'client','static', 'assets', 'abc', client.id + '.html') );
+ bundler.dests.paths.css.should.be.equal( path.join(ss.root,'client','static', 'assets', 'abc', client.id + '.css') );
+ bundler.dests.paths.js.should.be.equal( path.join(ss.root,'client','static', 'assets', 'abc', client.id + '.js') );
+
+ bundler.dests.relPaths.html.should.be.equal( path.join('/client','static', 'assets', 'abc', client.id + '.html') );
+ bundler.dests.relPaths.css.should.be.equal( path.join('/client','static', 'assets', 'abc', client.id + '.css') );
+ bundler.dests.relPaths.js.should.be.equal( path.join('/client','static', 'assets', 'abc', client.id + '.js') );
+
+ bundler.dests.dir.should.be.equal( path.join(ss.root,'client','static','assets', client.name) );
+ bundler.dests.containerDir.should.be.equal( path.join(ss.root,'client','static','assets') );
+
+
+ //client.id = shortid.generate();
+ });
+ });
+
+ afterEach(function() {
+ ss.client.forget();
+ });
+
+ describe('#entries', function () {
+
+ beforeEach(function() {
+
+ options.defaultEntryInit = origDefaultEntryInit;
+
+ //ss.client.assets.unload();
+ //ss.client.assets.load();
+ });
+
+
+ it('should return entries for everything needed in view', function() {
+
+ //TODO set project root function
+ ss.root = ss.api.root = path.join(__dirname, '../../../fixtures/project');
+
+ var client = ss.client.define('abc', {
+ code: './abc/index.js',
+ view: './abc.html'
+ });
+
+ ss.client.load();
+
+ var bundler = ss.api.bundler.get('abc'),
+ entriesCSS = bundler.asset.entries('css'),
+ entriesJS = bundler.asset.entries('js');
+
+ entriesCSS.should.have.lengthOf(0);
+ entriesJS.should.have.lengthOf(5);
+
+ // libs
+ entriesJS[0].names.should.have.lengthOf(1);
+ entriesJS[0].names[0].should.be.equal('browserify.js');
+
+ // mod
+ entriesJS[1].name.should.be.equal('eventemitter2');
+ entriesJS[1].type.should.be.equal('mod');
+
+ // mod
+ entriesJS[2].name.should.be.equal('socketstream');
+ entriesJS[2].type.should.be.equal('mod');
+
+ // mod TODO
+ entriesJS[3].file.should.be.equal('./abc/index.js');
+ //entriesJS[3].type.should.be.equal('mod');
+
+ // start TODO
+ entriesJS[4].content.should.be.equal('require("./code/abc/entry");');
+ entriesJS[4].type.should.be.equal('start');
+
+
+ //entriesJS.should.be.equal([{ path:'./abc.js'}]);
+ });
+
+
+ it('should return be affected by includes flags');
+
+
+ });
+
+
+
+});
\ No newline at end of file
diff --git a/test/unit/client/index.test.js b/test/unit/client/index.test.js
index 751f0008..755f8c51 100644
--- a/test/unit/client/index.test.js
+++ b/test/unit/client/index.test.js
@@ -1,5 +1,9 @@
'use strict';
+var path = require('path'),
+ should = require('should'),
+ ss = require( '../../../lib/socketstream'),
+ options = ss.client.options;
describe('client asset manager index', function () {
diff --git a/test/unit/client/system/index.test.js b/test/unit/client/system/index.test.js
index 8d5e447c..47c027f2 100644
--- a/test/unit/client/system/index.test.js
+++ b/test/unit/client/system/index.test.js
@@ -1,59 +1,83 @@
'use strict';
var path = require('path'),
- ss = require( path.join(process.env.PWD, 'lib/socketstream') );
-
+ should = require('should'),
+ ss = require( path.join(process.env.PWD, 'lib/socketstream')),
+ options = ss.client.options;
describe('client system library', function () {
-
+ var origDefaultEntryInit = options.defaultEntryInit;
describe('#send', function () {
beforeEach(function() {
+ options.defaultEntryInit = origDefaultEntryInit;
+
ss.client.assets.unload();
ss.client.assets.load();
});
- it('should extend shims',function() {
-
+ it('should extend libs',function() {
+
var jsBefore, jsAfter;
- jsBefore = ss.client.assets.serve.js();
- ss.client.assets.send('shim','extra.js','var extra = 0;');
- jsAfter = ss.client.assets.serve.js();
- jsAfter.should.have.length(jsBefore.length + 1 + 14);
+ jsBefore = ss.api.bundler.systemLibs();
+ jsBefore.should.be.type('object');
+ jsBefore.type.should.be.equal('loader');
+ ss.client.assets.send('lib','extra.js','var extra = 0;');
+ jsAfter = ss.api.bundler.systemLibs();
+ jsAfter.should.be.type('object');
+ jsAfter.content.should.have.length(jsBefore.content.length + 1 + 14);
});
- it('should replace shims',function() {
+ it('should replace libs',function() {
var jsBefore, jsAfter;
- jsBefore = ss.client.assets.serve.js();
- ss.client.assets.send('shim','json.min.js','');
- jsAfter = ss.client.assets.serve.js();
- jsAfter.should.have.length(jsBefore.length - 1886);
+ jsBefore = ss.api.bundler.systemLibs();
+ jsBefore.should.be.type('object');
+ jsBefore.type.should.be.equal('loader');
+ ss.client.assets.send('lib','browserify.js','');
+ jsAfter = ss.api.bundler.systemLibs();
+ jsAfter.content.should.have.length(jsBefore.content.length - 8854);
});
- it('should extend libs',function() {
-
- var jsBefore, jsAfter;
+ it('should replace init code', function() {
- jsBefore = ss.client.assets.serve.js();
- ss.client.assets.send('lib','extra.js','var extra = 0;');
- jsAfter = ss.client.assets.serve.js();
- jsAfter.should.have.length(jsBefore.length + 1 + 14);
+ //ss.client.options.entryModuleName =
+ var expected = 'require("./entry");',//ss.client.options.defaultEntryInit,
+ client = {
+ entryInitPath: './entry'
+ };
+
+ // Code to execute once everything is loaded
+ ss.client.assets.send('code', 'init', options.defaultEntryInit);
+
+ var start = ss.api.bundler.startCode(client);
+ start.should.be.type('object');
+ start.type.should.be.equal('start');
+ start.content.should.be.equal(expected);
+ // client.entryInitPath
});
- it('should replace libs',function() {
+ it('should allow startCode for the client to be configured', function(){
+ var expected = 'require("./startCode");',
+ client = {};
- var jsBefore, jsAfter;
+ options.defaultEntryInit = 'require("./startCode");';
- jsBefore = ss.client.assets.serve.js();
- ss.client.assets.send('lib','browserify.js','');
- jsAfter = ss.client.assets.serve.js();
- jsAfter.should.have.length(jsBefore.length - 8854);
+ // Code to execute once everything is loaded
+ ss.client.assets.send('code', 'init', options.defaultEntryInit);
+
+ var start = ss.api.bundler.startCode(client);
+ start.should.be.type('object');
+ start.type.should.be.equal('start');
+ start.content.should.be.equal(expected);
});
+
+ //TODO options.entryModuleName
+ //TODO options.defaultEntryInit
});
diff --git a/test/unit/client/system/modules/socketstream.test.js b/test/unit/client/system/modules/socketstream.test.js
index 860c85a7..ace8f4bf 100644
--- a/test/unit/client/system/modules/socketstream.test.js
+++ b/test/unit/client/system/modules/socketstream.test.js
@@ -1,5 +1,7 @@
'use strict';
+var path = require('path'),
+ ss = require( path.join(process.env.PWD, 'lib/socketstream'));
describe('socketstream client library', function () {
@@ -24,8 +26,48 @@ describe('socketstream client library', function () {
describe('#send', function () {
+ it('should extend mods',function() {
+
+ ss.client.assets.send('mod','extra.js','var extra = 0;');
+ var extra = ss.api.bundler.systemModule('extra.js',false);
+ extra.should.be.type('object');
+ extra.name.should.be.equal('extra');
+ extra.file.should.be.equal('extra');
+ extra.path.should.be.equal(path.join(process.env.PWD,'lib/client/system/modules/','extra.js'));
+ extra.dir.should.be.equal(path.join(process.env.PWD,'lib/client/system/modules'));
+ extra.content.should.be.equal('var extra = 0;');
+
+ var extra = ss.api.bundler.systemModule('extra.js');
+ extra.should.be.type('object');
+ extra.name.should.be.equal('extra');
+ extra.file.should.be.equal('extra');
+ extra.path.should.be.equal(path.join(process.env.PWD,'lib/client/system/modules/','extra.js'));
+ extra.dir.should.be.equal(path.join(process.env.PWD,'lib/client/system/modules'));
+ extra.content.should.be.equal('require.define("extra", function (require, module, exports, __dirname, __filename){\n' +
+ 'var extra = 0;\n});');
+ });
-
+ it('should replace mods',function() {
+
+ ss.client.assets.send('mod','extra.js','var extra = 0;');
+ ss.client.assets.send('mod','extra.js','var extra2 = 100;');
+ var extra = ss.api.bundler.systemModule('extra.js',false);
+ extra.should.be.type('object');
+ extra.name.should.be.equal('extra');
+ extra.file.should.be.equal('extra');
+ extra.path.should.be.equal(path.join(process.env.PWD,'lib/client/system/modules/','extra.js'));
+ extra.dir.should.be.equal(path.join(process.env.PWD,'lib/client/system/modules'));
+ extra.content.should.be.equal('var extra2 = 100;');
+
+ var extra = ss.api.bundler.systemModule('extra.js');
+ extra.should.be.type('object');
+ extra.name.should.be.equal('extra');
+ extra.file.should.be.equal('extra');
+ extra.path.should.be.equal(path.join(process.env.PWD,'lib/client/system/modules/','extra.js'));
+ extra.dir.should.be.equal(path.join(process.env.PWD,'lib/client/system/modules'));
+ extra.content.should.be.equal('require.define("extra", function (require, module, exports, __dirname, __filename){\n' +
+ 'var extra2 = 100;\n});');
+ });
});