diff --git a/.gitignore b/.gitignore index d6eaaea2..82903334 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ node_modules dump.rdb npm-debug.log *.tgz +test/fixtures/project/client/static/assets/abc diff --git a/.jshintrc b/.jshintrc index 33d439aa..904eb73b 100644 --- a/.jshintrc +++ b/.jshintrc @@ -3,7 +3,6 @@ "curly" : true, "eqeqeq" : true, "eqnull" : true, - "forin" : true, "immed" : true, "latedef" : "nofunc", "newcap" : true, @@ -20,7 +19,6 @@ "sub" : true, "supernew" : true, "node" : true, - "onevar" : true, "unused" : true, "multistr" : true, "smarttabs": true, diff --git a/docs/js/docs-setup.js b/docs/js/docs-setup.js index 9d9a5710..3a8a76f9 100644 --- a/docs/js/docs-setup.js +++ b/docs/js/docs-setup.js @@ -30,7 +30,16 @@ NG_DOCS={ "type": "overview", "moduleName": "Client-Side Code", "shortDescription": "Client-Side Code", - "keywords": "ability absolutely access accessed add allowing allows alphanumerically amd amount app apply approaches as-is automatically aware awesome backbone background bear better blank boiler-plate brought browser browserify browserifyexcludepaths cache called calls case cases catch change chaos choice clean client client-code client-side code coffee coming command common components connection console copy create created critical critically default define demand depend dependencies depends developer developers difference directores directory disable distinction doesn don easily ensure entry entrymodulename error established exactly example exceptions exclude excluded excluding execuring execute executed exist expect experience explicitly export file files folder forget form full functions future globalmodules goal going great handle head history hopes https inbuilt info instantly internally javascript jquery js killing leading legacy libraries library libs lightweight list live lives ll load loading long magic major making manage managing manually md mess mind modification modify module modules namespacing node null onus options order org overview packaging path paths performs point position problem problems project querystring reconnecting reference regular relative require required requires send serve served server server-side set share sharing side single slash small socketstream solution solutions solves special ss stack statement string structure subdirectories substack syntax system top track treated tricky true tutorials type typically underscore unique unstructured url values variable view views wade wanted web websocket window work works write writing" + "keywords": "ability absolutely access accessed add allowing allows alphanumerically amd amount app apply approaches as-is automatically aware awesome backbone background bear better blank boiler-plate brought browser browserify browserifyexcludepaths cache called calls case cases catch change chaos choice clean client client-code client-side code coffee coming command common components connection console copy create created critical critically default define demand depend dependencies depends developer developers difference directores directory disable distinction doesn don easily ensure entry entrymodulename error established exactly example exceptions exclude excluded excluding execuring execute executed exist expect experience explicitly export file files folder forget form functions future goal going great handle head history hopes https inbuilt info instantly internally javascript jquery js killing leading legacy libraries library libs lightweight list live lives ll load loading long magic major making manage managing manually md mess mind modification modify module modules namespacing node null onus options order org overview packaging path paths performs point position problem problems project querystring reconnecting reference regular relative require required requires send serve served server server-side set share sharing side single slash small socketstream solution solutions solves special ss stack statement string structure subdirectories substack syntax system top track treated tricky tutorials type typically underscore unique unstructured url values variable view views wade wanted web websocket window work works write writing" + }, + { + "section": "tutorials", + "id": "client_side_development", + "shortName": "Client-Side Development", + "type": "overview", + "moduleName": "Client-Side Development", + "shortDescription": "Client-Side Development", + "keywords": "amount asset assets break browser bundle bundler bundlers caching client client-side css determine development directory entry file files fly generally html http injected js overview path pattern reduce relative separate separately served timestamp tutorials type url view watch work" }, { "section": "tutorials", @@ -41,6 +50,15 @@ NG_DOCS={ "shortDescription": "Client-Side Templates", "keywords": "accessible alice allowing allows angular apart app append argument astronomy bad bandwidth biology bob browser building built-in bundled called client client-side clients code coffeekup compiled construct convert correct create css data default define directories directory display div don dramatically easy ember engine engines enter example exiting extension external favorite file find flexibility folder format formatter functions generate github good happy html including inline iphone jade jquery js languages larger layoutless length lib library libs limit live ll logic main major manually match md mix mixing mobile model model-person module modules mustache npm number optional organizing out-of-the-box outputs overview pass passing people perfect person practice prefer prefix project raw ready recommended reduces refactoring refresh render required requires scale scope second send serve server serving simple simply single-page small socketstream solution source ss ss-hogan string student studies styl subdirectory suitable supported supports tag template templateengine templates templating time tmpl tmpl- tutorials types var view ways websocket wrappers wraps" }, + { + "section": "tutorials", + "id": "client_side_xbundler", + "shortName": "Client-Side Bundler", + "type": "overview", + "moduleName": "Client-Side Bundler", + "shortDescription": "Client-Side Bundler", + "keywords": "action additional arguments array asset assets aware based behaviour browserify built-in bulk bundle bundler bundlers bundling callback called cb changes client client-side code complete completely config considered contents create css current custom default define definition described destsfor development directory dirs discuss dropped early entries existing experimental features file files forced function functionality future html implement implementation implemented implementing individual jade js jspm lacks load lot method methods minification move named newer next_arg object objective optimisations options opts overview pack pass passed path paths production referenced relative responsibilities return returns revised saved scss separate served server set shorthand shouldn sourcepaths ss starting starts step supported templates text tmpl tutorials types var view views webpack webpackbundler worker write" + }, { "section": "tutorials", "id": "defining_multiple_clients", @@ -167,6 +185,15 @@ NG_DOCS={ "shortDescription": "Template Engine Wrappers", "keywords": "add app as-is attach automatically based bearing behavior best browser building bytes call called calls case catch client client-side clientcode closing code command compile compiled compiledtemplate compiling config consider console contents create default defaultformatter directly dot easy engine engines error example exports extension false file files find folder formatter formatters fs function functions github good guides handles hasownproperty hogan hogan-template ht html idea include init inside jade join js key language large lib library loads log map message mind module modules needed occasionally opening overview overwrite party pass passing path performance pick points pre-compile pre-processing preferred prefix process prototype prove readfilesync ready red reference relative render require return returns selectformatter selecting selects send sending server side simply socketstream ss suffix supports tag template templates templating text third throw time tips tmpl trade transform transforming true tutorials twitter type uncommon utf8 var variety vm wide wire wrap wrapper wrappers written" }, + { + "section": "tutorials", + "id": "url_scheme", + "shortName": "URL Scheme", + "type": "overview", + "moduleName": "URL Scheme", + "shortDescription": "URL Scheme", + "keywords": "ad assets calling change choose client code common completely consider considered contents css current demand development directory equivalent exported fetching files fly form future handled hoc html ideally js level loading middleware minified module modules on-demand open overview packed partially path paths production relative root saved scheme sense serve serveclient served serving static support time tutorials url urls view views whitelist work" + }, { "section": "tutorials", "id": "using_emberjs", @@ -194,6 +221,42 @@ NG_DOCS={ "shortDescription": "Custom Request Responder", "keywords": "access action add adding additional aimed allow allowing allows annotated api apitree app appending application applications arbitrary argument arguments assigned assuming async automatically backbone basic basically best bi-directional browser build built-in bundled byte bytes calculate call callback callbacks called case channel character choice choose clicks client client-side clone code combination comfortable commas complete complex concepts config connection conscious continually control coordinate core create creating custom data default define defined desire destined develop developer developers digits directions directly documentation easier easily echo efficient ember encountered entirely errors event events exactly example experiment experimental explore extend external feature fewer files find flexibility flowing format freenode function functionally future gaming git github going groups handler handles heartbeat help hesitate high-speed http https ideas implement implementing incoming increase info inspiration interfaces internal introduction investigate invoked io irc issue javascript js json json-serialized left length libraries ll load loaded log lot low-bandwidth main message messages method middleware mode model models module modules mongodb-esq mouse movements moving namespacing node non-blocking note notice npm number numeric object objects online optional org outgoing overhead overview packet-sniff packing param1 param2 pass passed passing persistent pick pipe prepended presence process production protocol pub publish receives recommend register registerapi registering regular repl request requests responder responderid responders result rpc rpc-style runs schema second send sending sends separated sequential serialization serialize server server-side simple simply socket socketstream source ss ss-console ss-echo-responder stable stack stage started status store streaming streams string strings suite support synching takes test third-party time touch transforms transports tutorials types typical ultimate ultra ultra-simple unique unlimited user users ve versa vice view virtual ways website websocket wire world write writing zeromq" }, + { + "section": "api", + "id": "bundler", + "shortName": "bundler", + "type": "service", + "moduleName": "bundler", + "shortDescription": "Bundlers included.", + "keywords": "api bundler bundlers included service" + }, + { + "section": "api", + "id": "bundler.default:default", + "shortName": "default", + "type": "service", + "moduleName": "bundler", + "shortDescription": "The default bundler of HTML, CSS & JS", + "keywords": "api asset assetcss assethtml assetjs assetloader assetstart assettype assetworker bundler client code collection config css default define depending directory entries entry execution functions html implementation includes initcode js libs list load loader logical method module modules output pack registered registration relative require resource return script service start starting system systemassets systemmodule tominifiedcss tominifiedjs type view worker wrapcode" + }, + { + "section": "api", + "id": "bundler.webpack:webpack", + "shortName": "webpack", + "type": "service", + "moduleName": "bundler", + "shortDescription": "The webpack bundler of HTML, CSS & JS", + "keywords": "api asset assettype bundler client collection concept css custom demonstration directory entries functions html improved initcode js libs list method modules output pack purposes relative service systemassets type validate view webpack" + }, + { + "section": "api", + "id": "events", + "shortName": "events", + "type": "service", + "moduleName": "events", + "shortDescription": "Internal Event bus.", + "keywords": "api assets bus emitted event events expended idea internal module note production saved server service socketstream ss-console starts" + }, { "section": "api", "id": "http.index:index", @@ -212,6 +275,85 @@ NG_DOCS={ "shortDescription": "Right now the router is simply an EventEmitter. This may change in the future", "keywords": "allows api callback cb change clients ee eventemitter exists fall find fully function future html5 http instance mock multiple object original passed pushstate recursively req request res respond route router routing service simply single-page speed support url" }, + { + "section": "api", + "id": "ss", + "shortName": "ss", + "type": "overview", + "moduleName": "ss", + "shortDescription": "Internal API object which is passed to sub-modules and can be used within your app", + "keywords": "access add api app env exports function internal log object overview passed require root session socketstream ss string sub-modules var" + }, + { + "section": "api", + "id": "ss.add", + "shortName": "ss.add", + "type": "function", + "moduleName": "ss", + "shortDescription": "Call from your app to safely extend the 'ss' internal API object passed through to your /server code", + "keywords": "add api app call code extend fn function internal key object passed safely ss" + }, + { + "section": "api", + "id": "ss.bundler:bundler", + "shortName": "bundler", + "type": "service", + "moduleName": "ss", + "shortDescription": "Client bundling API", + "keywords": "actual api args arguments assets bundler bundlers bundling call client containerdir content custom default define definition describe describing descriptor destinations destsfor determine dir directly entry entry-points file implementing libraries locations method module object offer params passed paths query relpaths replace require return service single ss store system systemlibs systemmodule true tweak wrap wrapped" + }, + { + "section": "api", + "id": "ss.client:client", + "shortName": "client", + "type": "service", + "moduleName": "ss", + "shortDescription": "Client serving, bundling, development, building.", + "keywords": "add allow allows api assets building bundling client clients code coffee compress content css defined development file flags format function html js lib library libs module options production require send served service serving single ss system type" + }, + { + "section": "api", + "id": "ss.env", + "shortName": "ss.env", + "type": "property", + "moduleName": "ss", + "keywords": "api change default development env environment execution node_env property set ss ss_env type variable" + }, + { + "section": "api", + "id": "ss.log:log", + "shortName": "log", + "type": "service", + "moduleName": "ss", + "shortDescription": "Contains method stubs for logging to console (by default) or", + "keywords": "api assigning choose console debug default error fairly function happened info informed keeping level log logging method override parameters provider require service socketstream ss stubs sysadmin takes time trivial unexpected var wakeup warn winston" + }, + { + "section": "api", + "id": "ss.root", + "shortName": "ss.root", + "type": "property", + "moduleName": "ss", + "shortDescription": "By default the project root is the current working directory", + "keywords": "api current default directory project property root ss working" + }, + { + "section": "api", + "id": "ss.version", + "shortName": "ss.version", + "type": "property", + "moduleName": "ss", + "keywords": "api major minor property ss version" + }, + { + "section": "api", + "id": "start", + "shortName": "start", + "type": "function", + "moduleName": "start", + "shortDescription": "Starts the development or production server", + "keywords": "api development function http instance module production server start starts" + }, { "section": "api", "id": "utils", @@ -230,15 +372,6 @@ NG_DOCS={ "shortDescription": "This is used to maintain lists of userIds to socketIds and channelIds to socketIds", "keywords": "absolute adapted api array basename basepath channelids contents css dir directory dorectory eror extension file filepath files find findextforbase findextforbasepath github givven https isdir jade json lists loadpackagejson loads lookup maintain matching mode null object package parse path readdirsync reads returns root service socketids socketstream start synchronous thorws true unable userids utils views" }, - { - "section": "api", - "id": "utils.log:log", - "shortName": "log", - "type": "service", - "moduleName": "utils", - "shortDescription": "Contains method stubs for logging to console (by default) or", - "keywords": "api assigning choose console debug default error fairly function happened info informed keeping level log logging method override parameters provider require service socketstream ss stubs sysadmin takes time trivial unexpected utils var wakeup warn winston" - }, { "section": "api", "id": "utils.misc:misc", diff --git a/docs/partials/api/bundler.default.default.html b/docs/partials/api/bundler.default.default.html new file mode 100644 index 00000000..da34af5e --- /dev/null +++ b/docs/partials/api/bundler.default.default.html @@ -0,0 +1,38 @@ +

default +
service in module bundler + +
+

+

Description

+

The default bundler of HTML, CSS & JS

+
+

Methods

+ +
+
diff --git a/docs/partials/api/bundler.html b/docs/partials/api/bundler.html new file mode 100644 index 00000000..965c70f0 --- /dev/null +++ b/docs/partials/api/bundler.html @@ -0,0 +1,8 @@ +

bundler +
+
+

+

Description

+

Bundlers included.

+
+
diff --git a/docs/partials/api/bundler.webpack.webpack.html b/docs/partials/api/bundler.webpack.webpack.html new file mode 100644 index 00000000..5e98c6d4 --- /dev/null +++ b/docs/partials/api/bundler.webpack.webpack.html @@ -0,0 +1,21 @@ +

webpack +
service in module bundler + +
+

+

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.

+
+

Methods

+ +
+
diff --git a/docs/partials/api/events.html b/docs/partials/api/events.html new file mode 100644 index 00000000..065c603c --- /dev/null +++ b/docs/partials/api/events.html @@ -0,0 +1,10 @@ +

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.

+
+
diff --git a/docs/partials/api/ss.add.html b/docs/partials/api/ss.add.html new file mode 100644 index 00000000..f3f5b8fc --- /dev/null +++ b/docs/partials/api/ss.add.html @@ -0,0 +1,18 @@ +

add +
service in module ss + +
+

+

Description

+

Call from your app to safely extend the 'ss' internal API object passed through to your /server code

+
+

Usage

+
add(name, fn);
+

Parameters

ParamTypeDetails
namestring
    +
  • Key in the ss API.
  • +
+
fnfunctionnumberbooleanstring
    +
  • value or function
  • +
+
+
diff --git a/docs/partials/api/ss.bundler.bundler.html b/docs/partials/api/ss.bundler.bundler.html new file mode 100644 index 00000000..63fe8ffb --- /dev/null +++ b/docs/partials/api/ss.bundler.bundler.html @@ -0,0 +1,44 @@ +

bundler +
service in module ss + +
+

+

Description

+

Client bundling API

+

Client bundling API for implementing a custom bundler.

+
+

Methods

+ +
+
diff --git a/docs/partials/api/ss.client.client.html b/docs/partials/api/ss.client.client.html new file mode 100644 index 00000000..067c7aec --- /dev/null +++ b/docs/partials/api/ss.client.client.html @@ -0,0 +1,26 @@ +

client +
service in module ss + +
+

+

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.

+
+

Methods

+ +
+
diff --git a/docs/partials/api/ss.env.html b/docs/partials/api/ss.env.html new file mode 100644 index 00000000..96cec4e5 --- /dev/null +++ b/docs/partials/api/ss.env.html @@ -0,0 +1,10 @@ +

env +
service in module ss + +
+

+

Usage

+
ss.env
+

Returns

string

Execution environment type. To change set environment variable NODE_ENV or SS_ENV. 'development' by default.

+
+
diff --git a/docs/partials/api/ss.html b/docs/partials/api/ss.html new file mode 100644 index 00000000..e41031f5 --- /dev/null +++ b/docs/partials/api/ss.html @@ -0,0 +1,7 @@ +

+
+
+

+

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;

+
diff --git a/docs/partials/api/utils.log.log.html b/docs/partials/api/ss.log.log.html similarity index 54% rename from docs/partials/api/utils.log.log.html rename to docs/partials/api/ss.log.log.html index da0e297f..1921c46e 100644 --- a/docs/partials/api/utils.log.log.html +++ b/docs/partials/api/ss.log.log.html @@ -1,47 +1,47 @@

log -
service in module utils +
service in module ss

Description

-

Contains method stubs for logging to console (by default) or +

Contains method stubs for logging to console (by default) or whatever logging provider you choose.

Methods

  • debug()

    -

    Debug level logging, uses console.log by default. Override by assigning a +

    Debug level logging, uses console.log by default. Override by assigning a function that takes the same parameters as console.log:

    var ss = require('socketstream');
     ss.api.log.debug = console.log;
     

    Example

    -
    ss.api.log.debug("Something fairly trivial happened");
    +
    ss.log.debug("Something fairly trivial happened");
     
  • error()

    -

    Error level logging, uses console.error by default. Override by assigning a +

    Error level logging, uses console.error by default. Override by assigning a function that takes the same parameters as console.error.

    Example

    -
    ss.api.log.error("Time to wakeup the sysadmin");
    +
    ss.log.error("Time to wakeup the sysadmin");
     
  • info()

    -

    Info level logging, uses console.log by default. Override by assigning a +

    Info level logging, uses console.log by default. Override by assigning a function that takes the same parameters as console.log.

    Example

    -
    ss.api.log.info("Just keeping you informed");
    +
    ss.log.info("Just keeping you informed");
     
  • warn()

    -

    Warn level logging, uses console.log by default. Override by assigning a +

    Warn level logging, uses console.log by default. Override by assigning a function that takes the same parameters as console.log:

    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!");
     
  • diff --git a/docs/partials/api/ss.root.html b/docs/partials/api/ss.root.html new file mode 100644 index 00000000..7b0c645d --- /dev/null +++ b/docs/partials/api/ss.root.html @@ -0,0 +1,13 @@ +

    root +
    service in module ss + +
    +

    +

    Description

    +

    By default the project root is the current working directory

    +
    +

    Usage

    +
    ss.root
    +

    Returns

    string

    Project root

    +
    +
    diff --git a/docs/partials/api/ss.version.html b/docs/partials/api/ss.version.html new file mode 100644 index 00000000..b37b755a --- /dev/null +++ b/docs/partials/api/ss.version.html @@ -0,0 +1,10 @@ +

    version +
    service in module ss + +
    +

    +

    Usage

    +
    ss.version
    +

    Returns

    number

    major.minor

    +
    +
    diff --git a/docs/partials/api/start.html b/docs/partials/api/start.html new file mode 100644 index 00000000..9ad7a0bc --- /dev/null +++ b/docs/partials/api/start.html @@ -0,0 +1,12 @@ +

    start +
    +
    +

    +

    Description

    +

    Starts the development or production server

    +
    +

    Usage

    +
    start(server);
    +

    Parameters

    ParamTypeDetails
    serverHTTPServer

    Instance of the server from the http module

    +
    +
    diff --git a/docs/partials/tutorials/client_side_code.html b/docs/partials/tutorials/client_side_code.html index 49a94c7d..acdb8c20 100644 --- a/docs/partials/tutorials/client_side_code.html +++ b/docs/partials/tutorials/client_side_code.html @@ -38,10 +38,6 @@

    Options

     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.

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.

+
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/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});'); + }); });