├── .gitignore ├── LICENSE ├── README.md ├── bin └── skit ├── docs.md ├── example ├── helloworld │ ├── library │ │ ├── BaseController.css │ │ ├── BaseController.html │ │ ├── BaseController.js │ │ └── GitHubAPIClient.js │ └── public │ │ ├── Home.html │ │ ├── Home.js │ │ ├── Home_item.html │ │ └── gist │ │ ├── Gist.html │ │ └── Gist.js ├── rottentomatoes │ ├── demo │ │ ├── library │ │ │ ├── BaseController.css │ │ │ └── BaseController.js │ │ └── public │ │ │ ├── Home.css │ │ │ ├── Home.html │ │ │ ├── Home.js │ │ │ ├── Home_movie.html │ │ │ └── __id__ │ │ │ ├── Movie.css │ │ │ ├── Movie.html │ │ │ └── Movie.js │ ├── main.js │ └── package.json ├── skeleton │ ├── __static__ │ │ └── favicon.ico │ ├── library │ │ ├── BaseController.css │ │ ├── BaseController.html │ │ └── BaseController.js │ └── public │ │ ├── Home.css │ │ ├── Home.html │ │ ├── Home.js │ │ ├── Home_item.html │ │ └── about │ │ ├── About.html │ │ └── About.js └── skitjs.com │ ├── __static__ │ ├── favicon.ico │ ├── skitrequest.png │ └── viewlayer.png │ ├── library │ ├── BaseController.js │ ├── BaseController_buttons.css │ ├── BaseController_layout.css │ └── BaseController_style.css │ └── public │ ├── Home.css │ ├── Home.html │ ├── Home.js │ └── getting-started │ ├── GettingStarted.html │ └── GettingStarted.js ├── lib ├── ControllerRenderer.js ├── SkitProxy.js ├── SkitServer.js ├── bootstrap.html ├── error.html ├── errors.js ├── loader │ ├── BundledLoader.js │ ├── NamedNode.js │ ├── SkitModule.js │ ├── loader.js │ ├── pooledmoduleloader.js │ ├── scriptresource.js │ └── styleresource.js ├── optimizer.js ├── skit.js ├── skit │ ├── browser │ │ ├── ElementWrapper.js │ │ ├── Event.js │ │ ├── dom.js │ │ ├── events.js │ │ ├── layout.js │ │ └── reset.css │ ├── platform │ │ ├── Controller.js │ │ ├── PubSub.js │ │ ├── cookies.js │ │ ├── cookies_browser.js │ │ ├── cookies_server.js │ │ ├── env.js │ │ ├── env_browser.js │ │ ├── env_server.js │ │ ├── iter.js │ │ ├── json.js │ │ ├── navigation.js │ │ ├── navigation_browser.js │ │ ├── navigation_server.js │ │ ├── net.js │ │ ├── net_Response.js │ │ ├── net_SendOptions.js │ │ ├── net_browser.js │ │ ├── net_server.js │ │ ├── netproxy.js │ │ ├── netproxy_browser.js │ │ ├── netproxy_server.js │ │ ├── object.js │ │ ├── string.js │ │ ├── urls.js │ │ └── util.js │ └── thirdparty │ │ ├── cookies.js │ │ ├── handlebars_runtime.js │ │ └── sizzle.js └── skitutil.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2014 Cluster Labs, Inc. https://cluster.co/ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | skit 2 | ==== 3 | A pure JavaScript frontend for building better web clients. 4 | 5 | ### Upcoming features 6 | 7 | * Client-side navigation/rendering and history management for subsequent pageloads. 8 | * Performance improvements, including the ability to cache content from backends. 9 | * Better integration points for backend proxies. 10 | 11 | ### Changelog 12 | 13 | Check out the Releases for a list of changes with every version. 14 | -------------------------------------------------------------------------------- /bin/skit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * @license 5 | * (c) 2015 Cluster Labs, Inc. https://cluster.co/ 6 | * License: MIT 7 | */ 8 | 9 | var fs = require('fs'); 10 | var path = require('path'); 11 | 12 | var minimist = require('minimist'); 13 | 14 | var skit = require('../../skit'); 15 | var loader = require('../lib/loader/loader'); 16 | 17 | 18 | function usage(command) { 19 | function usage_path_to_root() { 20 | console.log(' path_to_root - The root path for the skit tree, which') 21 | console.log('   contains "public" serving directory and') 22 | console.log('   serves as the root of skit module paths.') 23 | } 24 | function usage_path_to_optimized_root() { 25 | console.log(' path_to_optimized_root - The root path for the optimized') 26 | console.log('   tree, which will be generated by this') 27 | console.log('   command.') 28 | } 29 | 30 | switch (command) { 31 | case 'run': 32 | console.log('usage: skit run [--port=3001] [--debug] [--public-root=public]') 33 | console.log() 34 | console.log(' Run the skit webserver for a given module root.') 35 | console.log() 36 | console.log('arguments:') 37 | console.log() 38 | usage_path_to_root() 39 | console.log(' debug - Run the server in debug mode, which will') 40 | console.log('   reload the modules in the package root') 41 | console.log('   with every request, and output detailed') 42 | console.log('   error messaging.') 43 | console.log(' port - Port for the server to listen on.') 44 | console.log(' public-root - Name of the "public" directory for this app.') 45 | console.log() 46 | break; 47 | 48 | case 'optimize': 49 | console.log('usage: skit optimize ') 50 | console.log(' [--static-host=] [--public-root=public]') 51 | console.log() 52 | console.log(' Generate an optimized tree from the given skit root,') 53 | console.log(' writing the result tree to the given path. You can later') 54 | console.log(' run this tree in production with the standard command,') 55 | console.log(' `skit run `.') 56 | console.log() 57 | console.log('arguments:') 58 | console.log() 59 | usage_path_to_root() 60 | usage_path_to_optimized_root() 61 | break; 62 | 63 | case 'skeleton': 64 | console.log('usage: skit skeleton ') 65 | console.log() 66 | console.log(' Generate a new empty skit project with recommended') 67 | console.log(' project structure.') 68 | console.log() 69 | console.log('arguments:') 70 | console.log() 71 | console.log(' new_path_to_root - Path where we will create a new empty') 72 | console.log(' skit project for you.') 73 | console.log() 74 | break; 75 | 76 | default: 77 | console.log('usage: skit help ') 78 | console.log() 79 | console.log('Commands: run optimize skeleton') 80 | } 81 | process.exit(1); 82 | } 83 | 84 | 85 | function validate_root(root, opt_publicRootName) { 86 | try { 87 | var dir = fs.statSync(root); 88 | } catch (e) { 89 | return 'Could not read ', '(' + e + ')'; 90 | } 91 | 92 | if (!dir.isDirectory()) { 93 | return ' must be a directory.'; 94 | } 95 | 96 | try { 97 | var publicPath = path.join(root, opt_publicRootName || 'public'); 98 | var publicDir = fs.statSync(publicPath); 99 | } catch (e) { 100 | return 'Could not read "public" directory inside (' + publicPath + ')'; 101 | } 102 | 103 | if (!publicDir.isDirectory()) { 104 | return ' must contain "public" directory.'; 105 | } 106 | 107 | return null; 108 | } 109 | 110 | function command_run(args, positionalArgs) { 111 | var usage_run = usage.bind(this, 'run'); 112 | 113 | var root = positionalArgs[0] || path.resolve(path.dirname()); 114 | 115 | var errorMessage = validate_root(root, args['public-root']); 116 | if (errorMessage) { 117 | console.log(errorMessage); 118 | console.log(); 119 | usage_run(); 120 | } 121 | 122 | var options = { 123 | debug: args['debug'], 124 | publicRoot: args['public-root'], 125 | }; 126 | 127 | var notFoundProxy = args['not-found-proxy']; 128 | if (notFoundProxy) { 129 | options.notFoundProxy = notFoundProxy; 130 | } 131 | 132 | var aliasMap = args['alias-map']; 133 | if (aliasMap) { 134 | options.aliasMap = aliasMap; 135 | } 136 | 137 | var server = new skit.SkitServer(root, options); 138 | var port = args['port']; 139 | server.listen(port); 140 | console.log('Skit server listening on 0.0.0.0:' + port); 141 | } 142 | 143 | 144 | function command_optimize(args, positionalArgs) { 145 | var usage_optimize = usage.bind(this, 'optimize'); 146 | 147 | if (positionalArgs.length != 2) { 148 | usage_optimize(); 149 | } 150 | var root = positionalArgs[0]; 151 | var optimizedRoot = positionalArgs[1]; 152 | 153 | var errorMessage = validate_root(root, args['public-root']); 154 | if (errorMessage) { 155 | console.log(errorMessage); 156 | console.log(); 157 | usage_optimize(); 158 | } 159 | 160 | try { 161 | fs.statSync(optimizedRoot); 162 | 163 | console.log(' already exists!'); 164 | console.log() 165 | usage(); 166 | } catch (e) { 167 | if (e.code != 'ENOENT') { 168 | throw e; 169 | } 170 | } 171 | 172 | var aliasMap = args['alias-map'] || '__alias_map__.json'; 173 | 174 | var server = new skit.SkitServer(root, { 175 | publicRoot: args['public-root'], 176 | }); 177 | skit.optimizeServer(server, optimizedRoot, {aliasMap: aliasMap}) 178 | 179 | console.log() 180 | console.log('Generated optimized skit root. To run this, use:'); 181 | console.log() 182 | console.log(' skit run ' + optimizedRoot + ' --alias-map=' + aliasMap) 183 | console.log() 184 | } 185 | 186 | 187 | function command_skeleton(args, positionalArgs) { 188 | var usage_skeleton = usage.bind(this, 'skeleton'); 189 | 190 | if (positionalArgs.length != 1) { 191 | usage_skeleton(); 192 | } 193 | 194 | var newRoot = path.normalize(path.resolve(positionalArgs[0])); 195 | 196 | try { 197 | fs.statSync(newRoot); 198 | 199 | console.log(' already exists!'); 200 | console.log() 201 | usage(); 202 | } catch (e) { 203 | if (e.code != 'ENOENT') { 204 | throw e; 205 | } 206 | } 207 | 208 | // Copy all the example files to a new location. 209 | var originalBasePath = path.join(__dirname, '../example/skeleton/'); 210 | var exampleFiles = loader.walkSync(originalBasePath); 211 | exampleFiles.forEach(function(filename) { 212 | var newFilename = filename.replace(originalBasePath, newRoot); 213 | loader.mkdirPSync(path.dirname(newFilename)); 214 | fs.writeFileSync(newFilename, fs.readFileSync(filename)); 215 | }); 216 | 217 | console.log() 218 | console.log('Generated new skit root! To run this, use:'); 219 | console.log() 220 | console.log(' skit run ' + newRoot + ' --debug'); 221 | console.log() 222 | } 223 | 224 | 225 | function main() { 226 | var args = minimist(process.argv.slice(2), { 227 | default: { 228 | 'port': 3001 229 | }, 230 | string: ['static-root', 'alias-map', 'public-root', 'not-found-proxy'], 231 | boolean: ['debug'], 232 | }); 233 | 234 | var positionalArgs = args['_']; 235 | var command = positionalArgs.shift(); 236 | 237 | switch (command) { 238 | case 'run': 239 | command_run(args, positionalArgs); 240 | break; 241 | 242 | case 'optimize': 243 | command_optimize(args, positionalArgs); 244 | break; 245 | 246 | case 'skeleton': 247 | command_skeleton(args, positionalArgs); 248 | break; 249 | 250 | case 'help': 251 | usage(positionalArgs[0]); 252 | break; 253 | 254 | default: 255 | usage(); 256 | break; 257 | } 258 | } 259 | 260 | 261 | main(); 262 | -------------------------------------------------------------------------------- /docs.md: -------------------------------------------------------------------------------- 1 | Skit API reference 2 | ------------------ 3 | 4 | Welcome to the skit API reference. These modules are available inside skit apps as "skit.browser.dom", "skit.platform.iter", etc, like so: 5 | 6 | var Controller = skit.platform.Controller; 7 | var net = skit.platform.net; 8 | 9 | module.exports = Controller.create({ 10 | __preload__: function(done) { 11 | net.send('https://your-site.com/api/foo', { 12 | success: function(response) { 13 | // check response.code, etc. 14 | }, 15 | complete: done 16 | }); 17 | }, 18 | 19 | ... 20 | 21 | }); 22 | 23 | These modules are meant to work in all browsers back to IE7-ish. 24 | 25 | Skit modules 26 | ------------ 27 | 28 | Skit modules are collections of files with the same filename before the first "_". For example, these files in a directory: 29 | 30 | - Foo.html 31 | - Foo.js 32 | - Foo_Bar.js 33 | - Foo_bazbuz.js 34 | - Foo.css 35 | 36 | Provide a single module, "Foo", with several internal modules. In Foo.js, you might import some of them at the top of your file: 37 | 38 | // This is a global skit library. 39 | var net = skit.platform.net; 40 | 41 | // This is a class from another skit module we wrote. 42 | var MyLibraryClass = library.things.MyLibraryClass; 43 | 44 | var Bar = __module__.Bar; 45 | // This is a Handlebars compiled template. 46 | var html = __module__.html; 47 | 48 | CSS modules are not accessible this way: 49 | 50 | // this will not work: 51 | var css = __module__.css; 52 | 53 | 54 | Module conventions 55 | ------------------ 56 | 57 | 1. Files whose exports are a class are CapitalizedLikeAClass, eg. Controller.js 58 | 2. Files whose exports are a module arelikethis -- no spaces 59 | 3. Internal modules, eg. SomeModule_someinternalthing.js follow the same convention -- "someinternalthing" in this case is not a class, whereas SomeModule_InternalThing.js is a class 60 | 4. \_\_things\_like\_this\_\_ are generally _special_ skit API things. 61 | 5. Imports are grouped: first global, then project, then internal imports. 62 | 6. Imports can only be at the top of the file -- imports below the first non-import are ignored. 63 | 64 | 65 | skit.browser 66 | ------------ 67 | 68 | The browser module contains things that depend on "window", eg. client-side event listeners, DOM lookups and DOM layout measuring functionality. 69 | 70 | skit.platform 71 | ------------- 72 | 73 | The platform module is intended for use in both places: server and client-side. It contains several modules that work transparently on the server and in the browser: 74 | 75 | - *cookies* - Wraps cookie setting/reading. 76 | - *net* - Wraps XHR on the client and _request_ on the server to provide the ability to call HTTP endpoints from either place transparently. 77 | - *navigation* - Provides information about the current URL globally, and allows the server side to perform redirects using navigation.navigate() and issue 404s with navigation.notFound(). 78 | 79 | -------------------------------------------------------------------------------- /example/helloworld/library/BaseController.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | font-family: arial, sans-serif; 4 | font-size: 14px; 5 | } 6 | 7 | #content { 8 | max-width: 640px; 9 | margin: 20px auto; 10 | } -------------------------------------------------------------------------------- /example/helloworld/library/BaseController.html: -------------------------------------------------------------------------------- 1 |
2 | {{{ childHtml }}} 3 |
-------------------------------------------------------------------------------- /example/helloworld/library/BaseController.js: -------------------------------------------------------------------------------- 1 | var Controller = skit.platform.Controller; 2 | var template = __module__.html; 3 | module.exports = Controller.create({ 4 | __title__: function(childTitle) { 5 | // Parents get the title generated by children as an argument. 6 | return childTitle + ' | Hello World'; 7 | }, 8 | __body__: function(childHtml) { 9 | // Parents get the HTML generated by children as an argument. 10 | return template({childHtml: childHtml}); 11 | }, 12 | __ready__: function() { 13 | // Parents also get __preload__, __load__ and __ready__ calls. 14 | } 15 | }); -------------------------------------------------------------------------------- /example/helloworld/library/GitHubAPIClient.js: -------------------------------------------------------------------------------- 1 | 2 | var net = skit.platform.net; 3 | 4 | var GITHUB_BASE_URL = 'https://api.github.com/gists/'; 5 | 6 | var logError = function(response) { 7 | console.log('Error loading:', response.code, 8 | 'body:', response.body); 9 | }; 10 | 11 | module.exports = { 12 | loadGists: function(apiCallback, context) { 13 | var gists = []; 14 | var done = function() { 15 | apiCallback.call(context, gists); 16 | }; 17 | net.send(GITHUB_BASE_URL + 'public', { 18 | success: function(response) { 19 | gists = response.body; 20 | }, 21 | error: logError, 22 | complete: done 23 | }); 24 | }, 25 | 26 | loadGist: function(gistId, apiCallback, context) { 27 | var gist = null; 28 | var done = function() { 29 | apiCallback.call(context, gist); 30 | }; 31 | net.send(GITHUB_BASE_URL + encodeURIComponent(gistId), { 32 | success: function(response) { 33 | gist = response.body; 34 | }, 35 | error: logError, 36 | complete: done 37 | }) 38 | } 39 | }; -------------------------------------------------------------------------------- /example/helloworld/public/Home.html: -------------------------------------------------------------------------------- 1 |

2 | Hello, world! I’m a template. 3 | 4 |

5 | 6 |
    7 | {{#each gists }} 8 | {{> __module__.item.html }} 9 | {{/each}} 10 |
-------------------------------------------------------------------------------- /example/helloworld/public/Home.js: -------------------------------------------------------------------------------- 1 | 2 | var dom = skit.browser.dom; 3 | var events = skit.browser.events; 4 | var Controller = skit.platform.Controller; 5 | 6 | var BaseController = library.BaseController; 7 | var GitHubAPIClient = library.GitHubAPIClient; 8 | 9 | var template = __module__.html; 10 | 11 | module.exports = Controller.create(BaseController, { 12 | __preload__: function(loaded) { 13 | GitHubAPIClient.loadGists(function(gists) { 14 | this.gists = gists; 15 | loaded(); 16 | }, this); 17 | }, 18 | 19 | __title__: function() { 20 | return 'Home'; 21 | }, 22 | 23 | __body__: function() { 24 | return template({gists: this.gists}); 25 | }, 26 | 27 | __ready__: function() { 28 | var reload = dom.get('#reload'); 29 | events.bind(reload, 'click', this.reload, this); 30 | } 31 | }); -------------------------------------------------------------------------------- /example/helloworld/public/Home_item.html: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 | {{#each files }} 4 | {{ filename }} 5 | {{/each}} 6 | 7 | {{#if owner.login }} 8 | by {{ owner.login }} 9 | {{/if}} 10 |
  • -------------------------------------------------------------------------------- /example/helloworld/public/gist/Gist.html: -------------------------------------------------------------------------------- 1 | ← Home 2 | 3 |

    Gist by {{ gistOwner }}

    4 | 5 |

    Gist description

    6 |

    {{ gist.description }}

    7 |

    View on GitHub

    8 | 9 | {{#each gist.files }} 10 |

    {{@key}}

    11 |

    {{ size }} bytes, {{ type }} ({{ language }})

    12 |
    {{ content }}
    13 | {{/each}} 14 | -------------------------------------------------------------------------------- /example/helloworld/public/gist/Gist.js: -------------------------------------------------------------------------------- 1 | var Controller = skit.platform.Controller; 2 | var string = skit.platform.string; 3 | var navigation = skit.platform.navigation; 4 | var BaseController = library.BaseController; 5 | var GitHubAPIClient = library.GitHubAPIClient; 6 | var template = __module__.html; 7 | module.exports = Controller.create(BaseController, { 8 | __preload__: function(loaded) { 9 | var query = navigation.query(); 10 | GitHubAPIClient.loadGist(query['id'], function(gist) { 11 | if (!gist) { 12 | navigation.notFound(); 13 | } else { 14 | this.gist = gist; 15 | } 16 | 17 | loaded(); 18 | }, this); 19 | }, 20 | __title__: function() { 21 | return 'Gist by ' + string.escapeHtml(this.gistOwner()); 22 | }, 23 | __body__: function() { 24 | return template({gist: this.gist, gistOwner: this.gistOwner()}); 25 | }, 26 | gistOwner: function() { 27 | if (this.gist['owner']) { 28 | return this.gist['owner']['login']; 29 | } 30 | return 'anonymous'; 31 | } 32 | }); -------------------------------------------------------------------------------- /example/rottentomatoes/demo/library/BaseController.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body { 7 | font-family: "Helvetica Neue", hevetica, arial, sans-serif; 8 | font-size: 16px; 9 | line-height: 1.4em; 10 | 11 | width: 640px; 12 | margin: 20px auto; 13 | } 14 | 15 | h1, h2, p, hr, ul, ol { 16 | margin-bottom: 20px; 17 | } 18 | 19 | li { 20 | list-style-type: square; 21 | } 22 | 23 | a { 24 | color: blue; 25 | text-decoration: none; 26 | cursor: pointer; 27 | } 28 | 29 | .poster { 30 | float: left; 31 | width: 80px; 32 | margin: 10px; 33 | margin-top: 0; 34 | margin-left: 0; 35 | } -------------------------------------------------------------------------------- /example/rottentomatoes/demo/library/BaseController.js: -------------------------------------------------------------------------------- 1 | 2 | var Controller = skit.platform.Controller; 3 | var string = skit.platform.string; 4 | var Handlebars = skit.thirdparty.handlebars; 5 | 6 | Handlebars.registerHelper('slugify', function(arg) { 7 | return string.trim(arg).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+/, '').replace(/-+$/, ''); 8 | }); 9 | 10 | return Controller.create({}); 11 | -------------------------------------------------------------------------------- /example/rottentomatoes/demo/public/Home.css: -------------------------------------------------------------------------------- 1 | 2 | .movie-row { 3 | overflow: hidden; 4 | } 5 | .movie-row .poster { 6 | width: 20px; 7 | } -------------------------------------------------------------------------------- /example/rottentomatoes/demo/public/Home.html: -------------------------------------------------------------------------------- 1 |

    Home

    2 |

    This is a template. It is rendered using Handlebars.

    3 | 4 |

    5 | Navigation in skit (for now) is dictated by the 6 | directory structure of the “public” package. WTF? I know, it’s crazy. 7 |

    8 | 9 |

    Now the crazy part.

    10 | 11 |

    12 | We fetched and rendered the movies data (below) on the server side. 13 | Clicking another list ("Change list") will make a client-side request 14 | in the same re-instantiated class, and could re-render the list or something 15 | with the same exact templates. 16 |

    17 | 18 |
    19 | 20 |

    Movies

    21 | 22 |

    23 | Change list: 24 |

      25 | {{#each lists}} 26 |
    • {{name}}
    • 27 | {{/each}} 28 |
    29 |

    30 | 31 |

    {{ listName }}

    32 |
      33 | {{#movies}} 34 | 35 | {{> __module__.movie.html }} 36 | {{else}} 37 |
    1. No results. Terrible demo! (Start over)
    2. 38 | {{/movies}} 39 |
    40 | -------------------------------------------------------------------------------- /example/rottentomatoes/demo/public/Home.js: -------------------------------------------------------------------------------- 1 | 2 | // "skit" is globally available and has some useful things in it. 3 | var dom = skit.browser.dom; 4 | var events = skit.browser.events; 5 | var Controller = skit.platform.Controller; 6 | var net = skit.platform.net; 7 | var netproxy = skit.platform.netproxy; 8 | var iter = skit.platform.iter; 9 | var object = skit.platform.object; 10 | 11 | // This is another module in our library. 12 | var BaseController = library.BaseController; 13 | 14 | // Files with the same beginning ("Home" in this case) 15 | // are grouped together into "modules". 16 | // This is how you reference other files in this module. 17 | var template = __module__.html; 18 | 19 | var LISTS = [ 20 | {name: 'In theaters', key: 'movies/in_theaters'}, 21 | {name: 'Box office', key: 'movies/box_office'}, 22 | {name: 'Opening movies', key: 'movies/opening'}, 23 | {name: 'Upcoming movies', key: 'movies/upcoming'}, 24 | {name: 'Top rentals', key: 'dvds/top_rentals'} 25 | ]; 26 | 27 | 28 | return Controller.create(BaseController, { 29 | // Load some shit to render from a remove server. 30 | __preload__: function(onLoaded) { 31 | if (!this.currentList) { 32 | this.currentList = LISTS[0].key; 33 | } 34 | 35 | this.movies = []; 36 | net.send('lists/' + this.currentList, { 37 | proxy: netproxy.getProxyNamed('rottentomatoes'), 38 | success: function(response) { 39 | this.movies = response.body['movies']; 40 | }, 41 | complete: function() { 42 | onLoaded(); 43 | }, 44 | context: this 45 | }) 46 | }, 47 | 48 | // This dictates the page title on the server, but would also be used 49 | // in client-side navigation logic. Same with __body__ below. 50 | __title__: function() { 51 | return 'Home: ' + this.movies.length + ' movies in theaters'; 52 | }, 53 | 54 | __body__: function() { 55 | var listName; 56 | var lists = iter.map(LISTS, function(list) { 57 | list = object.copy(list); 58 | if (this.currentList == list.key) { 59 | listName = list.name; 60 | list.selected = true; 61 | } 62 | return list; 63 | }, this); 64 | return template({ 65 | movies: this.movies, 66 | listName: listName, 67 | lists: lists 68 | }); 69 | }, 70 | 71 | // This method, in contrast, is only called on the client. Kinda weird, right? 72 | __ready__: function() { 73 | this.listener_ = events.delegate(document.body, '.list-item', 'click', this.onClickListLink_, this); 74 | }, 75 | __unload__: function() { 76 | events.unbind(this.listener_); 77 | }, 78 | 79 | onClickListLink_: function(evt) { 80 | evt.preventDefault(); 81 | 82 | var $link = evt.currentTarget; 83 | if ($link.getData('loading')) { 84 | return; 85 | } 86 | $link.setData('loading', '1'); 87 | 88 | $link.setText('Loading...'); 89 | 90 | this.currentList = $link.getData('list'); 91 | this.reload(); 92 | } 93 | }); 94 | -------------------------------------------------------------------------------- /example/rottentomatoes/demo/public/Home_movie.html: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 | {{title}} ({{year}} • {{ratings.critics_score}}%) 4 | — 5 | {{#each abridged_cast}} 6 | {{name}} 7 | {{else}} 8 | (no cast) 9 | {{/each}} 10 |
  • -------------------------------------------------------------------------------- /example/rottentomatoes/demo/public/__id__/Movie.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taylorhughes/skit/69a0c848ab8c9a6d1cb3c00d567f725b4d3cb5ff/example/rottentomatoes/demo/public/__id__/Movie.css -------------------------------------------------------------------------------- /example/rottentomatoes/demo/public/__id__/Movie.html: -------------------------------------------------------------------------------- 1 |

    Detail page

    2 | 3 |

    4 | Regex paths are supported by directories with special names. 5 | In main.js we register regular expressions for these paths. 6 |

    7 | 8 |
    9 | 10 |

    {{ title }} ({{ year }})

    11 | 12 |

    13 | Tomatometer: {{ ratings.critics_score }}% (critics); {{ ratings.audience_score }}% (audience) 14 | — 15 | Rating: {{ mpaa_rating }} 16 |

    17 | 18 |

    Synopsis

    19 |

    20 | 21 | {{ synopsis }} 22 |

    23 | 24 |

    Cast

    25 |
      26 | {{#each abridged_cast }} 27 |
    • {{ name }} {{#if characters}}— {{#each characters }}{{this}}{{/each}}{{/if}}
    • 28 | {{/each}} 29 |
    30 | 31 |

    Back home

    -------------------------------------------------------------------------------- /example/rottentomatoes/demo/public/__id__/Movie.js: -------------------------------------------------------------------------------- 1 | 2 | // "skit" is globally available and has some useful things in it. 3 | var Controller = skit.platform.Controller; 4 | var navigation = skit.platform.navigation; 5 | var net = skit.platform.net; 6 | var netproxy = skit.platform.netproxy; 7 | 8 | var BaseController = library.BaseController; 9 | var template = __module__.html; 10 | 11 | return Controller.create(BaseController, { 12 | // Load some shit to render from a remove server. 13 | __preload__: function(onLoaded) { 14 | // We can get arguments out of the URL like so. 15 | var id = this.params['__id__']; 16 | id = parseInt(id.split('-').slice(-1)[0], 10); 17 | 18 | net.send('movies/' + id + '.json', { 19 | proxy: netproxy.getProxyNamed('rottentomatoes'), 20 | success: function(response) { 21 | this.movie = response.body; 22 | }, 23 | complete: function() { 24 | if (!this.movie) { 25 | navigation.notFound(); 26 | } 27 | onLoaded(); 28 | }, 29 | context: this 30 | }); 31 | }, 32 | 33 | __title__: function() { 34 | return this.movie['title']; 35 | }, 36 | 37 | __body__: function() { 38 | return template(this.movie); 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /example/rottentomatoes/main.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | var path = require('path'); 4 | 5 | try { 6 | var skit = require('../../../skit'); 7 | } catch (e) { 8 | var skit = require('skit'); 9 | } 10 | 11 | // "demo" here refers to the root of the skit module tree 12 | // this thing is going to build and serve modules from. 13 | // "debug" forces the server to reload our modules with 14 | // every request, which makes development easier. 15 | var server = new skit.SkitServer(path.join(__dirname, 'demo'), {debug: true}); 16 | 17 | server.registerProxy('rottentomatoes', 18 | function(proxyRequest, apiRequest) { 19 | var API_PATH = 'http://api.rottentomatoes.com/api/public/v1.0/'; 20 | apiRequest.url = API_PATH + apiRequest.url; 21 | if (apiRequest.url.indexOf('?') < 0) { 22 | apiRequest.url += '?'; 23 | } 24 | // Secret API key never leaves the server. 25 | apiRequest.url += '&apikey=b6pr5tn4s5342z5dz4qfkz67'; 26 | }, 27 | function(apiRequest, apiResponse, proxyResponse) { 28 | // pass 29 | }); 30 | 31 | // Set up our detail page path. Any directory in our "public" tree 32 | // named __id__ will match this regular expression. And in those 33 | // controllers, this.params['__id__'] will be set to the 34 | // matching path component. 35 | server.registerUrlArgument('__id__', /\w+(-\w+)*-\d+/); 36 | 37 | var port = 3002; 38 | server.listen(port); 39 | 40 | console.log('Listening on 0.0.0.0:' + port); 41 | -------------------------------------------------------------------------------- /example/rottentomatoes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "skit-demo", 3 | "dependencies": { 4 | "skit": "latest" 5 | }, 6 | "description": "...", 7 | "readme": "...", 8 | "repository": {} 9 | } 10 | -------------------------------------------------------------------------------- /example/skeleton/__static__/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taylorhughes/skit/69a0c848ab8c9a6d1cb3c00d567f725b4d3cb5ff/example/skeleton/__static__/favicon.ico -------------------------------------------------------------------------------- /example/skeleton/library/BaseController.css: -------------------------------------------------------------------------------- 1 | 2 | /* This file is automatically included with any dependent modules. */ 3 | 4 | body, p { 5 | font-family: Helvetica, arial, sans-serif; 6 | font-size: 13px; 7 | line-height: 1.6; 8 | } 9 | 10 | h1, h2, h3, h4 { 11 | font-weight: bold; 12 | } 13 | 14 | h1 { 15 | font-size: 200%; 16 | } 17 | h2 { 18 | font-size: 150%; 19 | } 20 | 21 | a { 22 | color: #069; 23 | text-decoration: none; 24 | } 25 | a:hover { 26 | text-decoration: underline; 27 | } 28 | 29 | h1, h2, h3, h4, p, li { 30 | margin-bottom: 10px; 31 | } 32 | 33 | #base { 34 | max-width: 640px; 35 | margin: 100px auto; 36 | } 37 | 38 | .upsell { 39 | border: 1px solid #ccc; 40 | background: #efefef; 41 | padding: 10px; 42 | margin-bottom: 10px; 43 | } 44 | .upsell p { 45 | margin: 0; 46 | } 47 | -------------------------------------------------------------------------------- /example/skeleton/library/BaseController.html: -------------------------------------------------------------------------------- 1 |
    2 | {{{ childHtml }}} 3 |
    -------------------------------------------------------------------------------- /example/skeleton/library/BaseController.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var reset = skit.browser.reset; 4 | var Controller = skit.platform.Controller; 5 | 6 | // This loads Base.html from this directory. 7 | var html = __module__.html; 8 | 9 | 10 | return Controller.create({ 11 | __title__: function(childTitle) { 12 | // Parent controllers can frame the title content of child controllers. 13 | return childTitle + ' | Skit'; 14 | }, 15 | 16 | __body__: function(childHtml) { 17 | // Parent controllers can frame the body content of child controllers. 18 | return html({ 19 | childHtml: childHtml 20 | }); 21 | } 22 | }); -------------------------------------------------------------------------------- /example/skeleton/public/Home.css: -------------------------------------------------------------------------------- 1 | 2 | /* This is automatically included and should contain homepage-specific styles. */ 3 | -------------------------------------------------------------------------------- /example/skeleton/public/Home.html: -------------------------------------------------------------------------------- 1 |

    Home

    2 | 3 |
    4 |

    Visit a subpage: About

    5 |
    6 | 7 |

    Dynamic preloaded content

    8 |
    9 |

    10 | This is dynamic content loaded from a remote location. 11 | This is a trivial example because the remote location is a hosted JSON file, 12 | but in a real application this would be data from an API of some kind. 13 |

    14 |
    15 |
      16 | {{#each items }} 17 | {{> __module__.item.html }} 18 | {{/each}} 19 |
    20 | 21 |

    See also

    22 |
    23 |

    24 | Check out the skit project on Github 25 | to see the full documentation. 26 |

    27 |
    -------------------------------------------------------------------------------- /example/skeleton/public/Home.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Controller = skit.platform.Controller; 4 | var net = skit.platform.net; 5 | 6 | // This is the base controller located in our "library" module. 7 | var BaseController = library.BaseController; 8 | 9 | // This loads Home.html so we can render the main content for the page. 10 | var html = __module__.html; 11 | 12 | 13 | // Specifying BaseController here makes BaseController the parent Controller: 14 | // It modify our body HTML, title, etc. See that module for more information. 15 | return Controller.create(BaseController, { 16 | __preload__: function(onLoaded) { 17 | // This is where you load any data necessary for the initial page render. 18 | // net.send() works from the client and server, exactly the same way. 19 | 20 | net.send('https://cluster-static.s3.amazonaws.com/skit/example.json', { 21 | success: function(response) { 22 | this.items = response.body['items']; 23 | }, 24 | error: function() { 25 | this.items = [{title: 'Oops!', description: 'Could not load the example data.'}]; 26 | }, 27 | complete: function() { 28 | onLoaded(); 29 | }, 30 | context: this 31 | }) 32 | }, 33 | 34 | __load__: function() { 35 | // This is called on the server and client in order to setup the Controller 36 | // object after the preload has completed. 37 | }, 38 | 39 | __title__: function() { 40 | return 'Home'; 41 | }, 42 | 43 | __body__: function() { 44 | return html({ 45 | items: this.items 46 | }); 47 | }, 48 | 49 | __ready__: function(container) { 50 | // This is where the client lifecycle begins; we hook up event listeners, 51 | // format things in the browser if necessary, etc. 52 | 53 | // var $link = dom.get('a.foo'); 54 | // events.bind($link, 'click', this.onClickLink, this); 55 | } 56 | }); -------------------------------------------------------------------------------- /example/skeleton/public/Home_item.html: -------------------------------------------------------------------------------- 1 |
  • 2 | {{ title }} — {{ description }} 3 |
  • -------------------------------------------------------------------------------- /example/skeleton/public/about/About.html: -------------------------------------------------------------------------------- 1 |

    About

    2 | 3 |
    4 |

    5 | Navigation is based on the directory structure of “public”. 6 | Pretty wacky, but there’s no routes file to worry about. 7 |

    8 |
    9 | 10 |

    Nam commodo dignissim mauris in mattis. Aenean congue justo id odio volutpat porttitor. Curabitur sed laoreet risus, eu sollicitudin arcu.

    11 | 12 |

    Proin tincidunt metus non eros euismod, eget porta lectus aliquam. Duis eu nunc ac orci interdum mollis. Phasellus sed mollis risus, eu tempor quam. Morbi at interdum mi, vel porta lacus.

    13 | 14 |

    ← Go back

    -------------------------------------------------------------------------------- /example/skeleton/public/about/About.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Controller = skit.platform.Controller; 4 | 5 | var BaseController = library.BaseController; 6 | 7 | // This loads About.html from this directory. 8 | var html = __module__.html; 9 | 10 | 11 | return Controller.create(BaseController, { 12 | // This controller doesn't preload anything. 13 | __title__: function() { 14 | return 'About'; 15 | }, 16 | 17 | __body__: function() { 18 | return html(); 19 | } 20 | }); -------------------------------------------------------------------------------- /example/skitjs.com/__static__/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taylorhughes/skit/69a0c848ab8c9a6d1cb3c00d567f725b4d3cb5ff/example/skitjs.com/__static__/favicon.ico -------------------------------------------------------------------------------- /example/skitjs.com/__static__/skitrequest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taylorhughes/skit/69a0c848ab8c9a6d1cb3c00d567f725b4d3cb5ff/example/skitjs.com/__static__/skitrequest.png -------------------------------------------------------------------------------- /example/skitjs.com/__static__/viewlayer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taylorhughes/skit/69a0c848ab8c9a6d1cb3c00d567f725b4d3cb5ff/example/skitjs.com/__static__/viewlayer.png -------------------------------------------------------------------------------- /example/skitjs.com/library/BaseController.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var reset = skit.browser.reset; 4 | var Controller = skit.platform.Controller; 5 | var urls = skit.platform.urls; 6 | var util = skit.platform.util; 7 | 8 | 9 | return Controller.create({ 10 | __title__: function(childTitle) { 11 | return childTitle ? 'skit: ' + childTitle : 'skit'; 12 | }, 13 | 14 | __ready__: function() { 15 | setTimeout(function() { 16 | var parsed = urls.parse(window.location.href); 17 | if (parsed.port && parsed.port != 80 && parsed.port != 443) { 18 | return; 19 | } 20 | 21 | var GA_TRACKING_ID = 'UA-61684202-1'; 22 | 23 | var _gaq = window._gaq = window._gaq || []; 24 | _gaq.push(['_setAccount', GA_TRACKING_ID]); 25 | _gaq.push(['_trackPageview']); 26 | 27 | var ga = document.createElement('script'); 28 | ga.type = 'text/javascript'; 29 | ga.async = true; 30 | ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; 31 | document.getElementsByTagName('head')[0].appendChild(ga); 32 | }, 100); 33 | } 34 | }); -------------------------------------------------------------------------------- /example/skitjs.com/library/BaseController_buttons.css: -------------------------------------------------------------------------------- 1 | /* BUTTONS */ 2 | 3 | .button, 4 | button, 5 | input[type="submit"] { 6 | display: inline-block; 7 | color: white; 8 | -webkit-border-radius: 3px; 9 | -moz-border-radius: 3px; 10 | border-radius: 3px; 11 | border: none; 12 | background: #3da5f0; 13 | padding: 15px 30px; 14 | font-size: 16px; 15 | font-weight: 300; 16 | font-family: inherit; 17 | text-align: center; 18 | cursor: pointer; 19 | } 20 | .button:hover, 21 | button:hover, 22 | input[type="submit"]:hover { 23 | background: #1470b2; 24 | text-decoration: none; 25 | } 26 | .button:active, 27 | button:active, 28 | input[type="submit"]:active, 29 | .button:focus, 30 | button:focus, 31 | input[type="submit"]:focus { 32 | background: #1470b2; 33 | } 34 | button::-moz-focus-inner { 35 | border: 0; 36 | } 37 | 38 | .button:disabled, 39 | button:disabled, 40 | input[type="submit"]:disabled, 41 | .button:disabled:hover, 42 | button:disabled:hover, 43 | input[type="submit"]:disabled:hover { 44 | background: #999; 45 | cursor: default; 46 | } 47 | 48 | button.wide, 49 | .button.wide { 50 | width: 100%; 51 | padding-left: 0; 52 | padding-right: 0; 53 | } 54 | 55 | .button.secondary, 56 | button.secondary, 57 | input[type="submit"].secondary { 58 | background: #ccc; 59 | color: #333; 60 | } 61 | .button.secondary:hover, 62 | button.secondary:hover, 63 | input[type="submit"].secondary:hover { 64 | background: #ddd; 65 | } 66 | .button.secondary:active, 67 | button.secondary:active, 68 | input[type="submit"].secondary:active, 69 | .button.secondary:focus, 70 | button.secondary:focus, 71 | input[type="submit"].secondary:focus { 72 | background: #bbb; 73 | } 74 | -------------------------------------------------------------------------------- /example/skitjs.com/library/BaseController_layout.css: -------------------------------------------------------------------------------- 1 | 2 | .content { 3 | max-width: 640px; 4 | padding: 0 25px; 5 | margin: 100px auto; 6 | } 7 | .content .wide-image-container { 8 | margin: 30px 0; 9 | text-align: center; 10 | } 11 | .content .wide-image-container img { 12 | max-width: 100%; 13 | } 14 | 15 | 16 | .content ul > li { 17 | list-style-type: square; 18 | } 19 | .content ol > li { 20 | list-style-type: decimal; 21 | } 22 | 23 | 24 | #header { 25 | background: #222; 26 | background: linear-gradient(#333, #111); 27 | color: #fff; 28 | overflow: hidden; 29 | } 30 | #header .content { 31 | padding-top: 80px; 32 | padding-bottom: 80px; 33 | } 34 | 35 | @media screen and (max-width: 959px) { 36 | #header .content { 37 | padding-top: 40px; 38 | padding-bottom: 40px; 39 | } 40 | } 41 | 42 | #header h2, 43 | #header p { 44 | font-weight: 100; 45 | font-size: 150%; 46 | } 47 | #header h2 { 48 | margin-bottom: 20px; 49 | } 50 | 51 | #header a { 52 | color: #fff; 53 | text-decoration: underline; 54 | } 55 | 56 | 57 | #body h1 { 58 | font-size: 150%; 59 | color: #666; 60 | text-transform: uppercase; 61 | border-bottom: 1px solid #ccc; 62 | margin: 80px 0 40px 0; 63 | } 64 | 65 | #body .section { 66 | margin-bottom: 30px; 67 | } 68 | 69 | 70 | #body h3 { 71 | text-transform: uppercase; 72 | color: #666; 73 | margin-top: 20px; 74 | } 75 | 76 | 77 | 78 | #github-banner { 79 | position: fixed; 80 | top: 0; 81 | left: 0; 82 | } 83 | 84 | @media screen and (max-width: 480px) { 85 | #github-banner { 86 | position: absolute; 87 | } 88 | } 89 | 90 | 91 | #body .wide-example-container h3 { 92 | text-transform: none; 93 | } 94 | 95 | @media screen and (min-width: 960px) { 96 | 97 | .wide-example-container { 98 | width: 960px; 99 | margin-left: -160px; 100 | overflow: hidden; 101 | } 102 | 103 | .wide-example-container .half { 104 | float: left; 105 | width: 47%; 106 | margin-left: 3%; 107 | } 108 | .wide-example-container .half:first-child { 109 | margin-left: 0; 110 | } 111 | 112 | } -------------------------------------------------------------------------------- /example/skitjs.com/library/BaseController_style.css: -------------------------------------------------------------------------------- 1 | 2 | body, p { 3 | font-family: Helvetica, arial, sans-serif; 4 | font-size: 16px; 5 | line-height: 1.6; 6 | } 7 | 8 | h1, h2, h3, h4 { 9 | font-weight: bold; 10 | } 11 | 12 | h1 { 13 | font-size: 300%; 14 | } 15 | h2 { 16 | font-size: 150%; 17 | } 18 | 19 | a { 20 | color: #069; 21 | text-decoration: none; 22 | } 23 | a:hover { 24 | text-decoration: underline; 25 | } 26 | 27 | h1, h2, h3, h4, p, li { 28 | margin-bottom: 10px; 29 | } 30 | 31 | pre, code { 32 | font-family: Consolas, Courier, monospace; 33 | font-size: 14px; 34 | background: #eee; 35 | } 36 | pre { 37 | border: 1px solid #aaa; 38 | border-radius: 3px; 39 | padding: 10px; 40 | margin: 0; 41 | margin-bottom: 10px; 42 | overflow-x: auto; 43 | } 44 | 45 | pre .no-select { 46 | color: #999; 47 | -moz-user-select: none; 48 | -khtml-user-select: none; 49 | -webkit-user-select: none; 50 | -o-user-select: none; 51 | user-select: none; 52 | } 53 | pre ins { 54 | text-decoration: none; 55 | color: #096; 56 | } 57 | 58 | li li { 59 | margin-left: 25px; 60 | } 61 | li li li { 62 | margin-left: 50px; 63 | } 64 | -------------------------------------------------------------------------------- /example/skitjs.com/public/Home.css: -------------------------------------------------------------------------------- 1 | 2 | /* What is skit? Diagram */ 3 | 4 | #controller-example, 5 | #load-cycle { 6 | margin: 10px 0; 7 | } 8 | 9 | #controller-example { 10 | text-align: left; 11 | font-size: 13px; 12 | display: inline-block; 13 | } 14 | #controller-example .code { 15 | font-weight: bold; 16 | background: #F8F7D2; 17 | margin: -5px -10px; 18 | padding: 5px 10px; 19 | margin-left: -5px; 20 | padding-left: 5px; 21 | } 22 | 23 | #load-cycle { 24 | display: block; 25 | padding: 5px; 26 | white-space: nowrap; 27 | } 28 | #load-cycle .code { 29 | display: inline-block; 30 | padding: 5px 10px; 31 | font-weight: bold; 32 | background: #F8F7D2; 33 | font-family: Consolas, Courier, monospace; 34 | font-size: 13px; 35 | } 36 | 37 | #load-cycle .title { 38 | font-weight: bold; 39 | text-transform: uppercase; 40 | font-size: 13px; 41 | display: block; 42 | line-height: 12px; 43 | margin-bottom: 6px; 44 | } 45 | #load-cycle .server, 46 | #load-cycle .client { 47 | display: inline-block; 48 | padding: 10px 20px; 49 | border: 1px solid #ccc; 50 | } 51 | #load-cycle .server { 52 | background: #D6E9FC; 53 | border-right: none; 54 | padding-right: 7px; 55 | } 56 | #load-cycle .client { 57 | background: #B7D1EC; 58 | border-left: none; 59 | padding-left: 13px; 60 | } 61 | #load-cycle .crossover { 62 | display: inline-block; 63 | width: 0; 64 | z-index: 1000; 65 | position: relative; 66 | } 67 | 68 | 69 | /* Skit controller lifecycle detailed */ 70 | 71 | #body .skit-lifecycle { 72 | padding: 5px 10px; 73 | background: #efefef; 74 | border: 1px solid #ccc; 75 | border-radius: 3px; 76 | overflow: hidden; 77 | } 78 | #body .skit-lifecycle-divider { 79 | margin: 10px 0; 80 | text-align: center; 81 | } 82 | #body .skit-lifecycle-divider em { 83 | margin: 0 20px; 84 | } 85 | 86 | #body .skit-lifecycle ol, 87 | #body .skit-lifecycle li { 88 | margin: 5px 0; 89 | } 90 | #body .skit-lifecycle li { 91 | margin-left: 30px; 92 | } 93 | 94 | #body .skit-lifecycle h2 { 95 | display: inline-block; 96 | font-size: inherit; 97 | text-transform: uppercase; 98 | color: #fff; 99 | padding: 5px 10px; 100 | line-height: 1.4; 101 | background: #a00; 102 | margin: 5px 0; 103 | border-radius: 4px; 104 | } 105 | 106 | #body .skit-lifecycle .your-code { 107 | padding: 5px 10px; 108 | background: #D6E9FC; 109 | border: 1px solid #ccc; 110 | border-radius: 3px; 111 | overflow: hidden; 112 | margin: 10px 0; 113 | } 114 | 115 | #body .skit-lifecycle .your-code h3 { 116 | margin: 0; 117 | margin-bottom: 10px; 118 | } 119 | 120 | #body .skit-lifecycle .your-code code { 121 | background: #F8F7D2; 122 | display: inline-block; 123 | padding: 2px 8px; 124 | border-radius: 4px; 125 | } 126 | 127 | 128 | -------------------------------------------------------------------------------- /example/skitjs.com/public/Home.html: -------------------------------------------------------------------------------- 1 | 13 | 14 |
    15 |

    Overview

    16 | 17 |
    18 |

    What is skit?

    19 |

    Skit is a JavaScript framework for building web pages with this controller lifecycle:

    20 |
    Controller.create({
     21 |   preload: function(done) {
     22 |     MyAPIClient.getThing(function(thing) {
     23 |       this.thing = thing;
     24 |       done();
     25 |     }, this);
     26 |   },
     27 |   render: function() {
     28 |     return template(this.thing);
     29 |   },
     30 |   ready: function() {
     31 |     events.listen(dom.get('.thing'), 'click', function() {
     32 |       this.thing.clicked = true;
     33 |       this.rerender();
     34 |     }, this);
     35 |   }
     36 | });
    37 | 38 |

    … that execute like this:

    39 |
    40 | 41 | Server 42 | preload()render() 43 | 44 | 45 | Browser 46 | ready() 47 | 48 |
    49 | 50 |

    … automatically, without having to configure anything.

    51 |
    52 | 53 |
    54 |

    What’s skit made of?

    55 |
      56 |
    1. 57 | A webserver that runs your controllers on the server, then 58 | sets them up in the browser with the same-ish state 59 | automatically. 60 |
    2. 61 |
    3. 62 | A module system for building components that consist of templates, 63 | stylesheets and JavaScript together. 64 |
    4. 65 |
    5. 66 | A set of lightweight libraries that facilitate issuing HTTP requests, 67 | managing cookies, and handling navigation on the server and client side 68 | transparently. 69 |
    6. 70 |
    71 |
    72 | 73 |
    74 |

    What’s it for?

    75 |

    76 | Skit is good for building web apps on existing HTTP-based APIs, 77 | like the one you probably already built for your mobile app. 78 |

    79 |
    80 | Skit lives in the view layer 81 |
    82 |

    83 | Skit is not a full-stack framework, 84 | or even a “Node.js framework” in the typical sense — 85 | it’s more like a client-side framework that also runs 86 | on the server side. 87 |

    88 |
    89 | 90 | 91 |

    Features

    92 | 93 |
    94 |

    Share client- and server-side code without thinking

    95 |
      96 |
    • 97 | Write a single JavaScript codebase 98 | for your web app. No more client-side app bootstrapping hackery 99 | or configuring static asset pipelines. 100 |
    • 101 |
    • 102 | Everything from URL routing to HTTP redirects to “DOMReady” 103 | is handled by the same JavaScript controller classes. 104 |
    • 105 |
    • 106 | Skit platform libraries are built to work on the client and server. 107 |
    • 108 |
    • 109 | Build a single HTTP API client for your backend in JavaScript. 110 |
    • 111 |
    • 112 | “Nearly 100%” — 113 | if you want, keep cookie/API secrets on the server side only by proxying 114 | API requests through skit. 115 |
    • 116 |
    117 |
    118 | 119 |
    120 |

    Zero configuration

    121 |
      122 |
    • No configuration files.
    • 123 |
    • Directory structure dictates URL routes for skit controllers. (!)
    • 124 |
    • Automatic resource grouping and minification in production mode.
    • 125 |
    • Awesome, unobfuscated development mode for the best development.
    • 126 |
    127 |
    128 | 129 |
    130 |

    The best modules ever

    131 |
      132 |
    • 133 | Build complete JavaScript/CSS/HTML modules. 134 | No need to configure the module loader, just start adding files. 135 |
    • 136 |
    • 137 | Modules are based on a common filename prefix, so you don’t 138 | have to add a bunch of boilerplate code to make a new one. 139 |
    • 140 |
    • 141 | Once your modules are defined, they are loaded on the server and 142 | client automatically. 143 |
    • 144 |
    • 145 | In production, resources are bundled, versioned and optimized 146 | automatically. 147 |
    • 148 |
    149 |
    150 | 151 |
    152 |

    SEO the Natural Way™

    153 |
      154 |
    • 155 | Render content on the server side, then use the same templates to render 156 | supplemental content on the client side as users interact with your app. 157 |
    • 158 |
    • 159 | Quit returning empty responses with a loading spinner. Seriously, cut that out. 160 |
    • 161 |
    • 162 | Robots should like you for you, don’t you think? 163 |
    • 164 |
    165 |
    166 | 167 |

    How it works

    168 | 169 |
    170 |
    171 | Skit initially renders a controller in the server, then reinstantiates the controller in the client. 172 |
    173 | 174 |

    Skit request lifecycle

    175 |

    176 | The skit lifecycle starts on the server side, where the server loads 177 | the current page’s controller module and renders a response. 178 | Then, in the browser, the controller module is reconstructed and 179 | the execution continues. 180 |

    181 | 182 |
    183 |

    Server side

    184 |
      185 |
    1. Skit parses the URL structure to find the current skit controller
    2. 186 |
    3. Skit instantiates your controller module
    4. 187 |
    5. 188 |

      Your controller: Example.js

      189 |
        190 |
      1. preload() — Loads data from the backend API
      2. 191 |
      3. load() — Sets up state after data is loaded
      4. 192 |
      5. render() — Generate <title> text and <body> HTML for the resulting page
      6. 193 |
      194 |
    6. 195 |
    7. Skit stores the state loaded in preload
    8. 196 |
    9. Skit outputs the HTML rendered during render
    10. 197 |
    11. Skit outputs a bunch of extra JavaScript to take over in the client
    12. 198 |
    199 |
    200 | 201 |

    HTTP transport

    202 | 203 |
    204 |

    Client side

    205 |
      206 |
    1. Skit reloads the same server-side modules in the client
    2. 207 |
    3. Skit restores the state loaded in preload, serialized as JSON
    4. 208 |
    5. 209 |

      Your controller: Example.js

      210 |
        211 |
      1. load() — Sets up state after data is loaded (now in the client)
      2. 212 |
      3. ready() — Sets up client-side event handlers for clicks, scrolling, etc.
      4. 213 |
      5. … whatever else your client does in the browser.
      6. 214 |
      215 |
    6. 216 |
    217 |
    218 |
    219 | 220 |
    221 |

    Default configuration

    222 |
      223 |
    1. 224 | Templates are rendered with Handlebars. 225 | There exists a facility to roll your own and use your own template 226 | compiler here, but this is not documented yet. 227 |
    2. 228 |
    3. 229 | CSS is just plain CSS. CSS files defined in modules are always 230 | included when the module is required. 231 |
    4. 232 |
    5. 233 | JavaScript is plain JavaScript. There is support for inserting your own 234 | compiler step here (eg. CoffeeScript), but it is not documented yet. 235 |
    6. 236 |
    237 |
    238 | 239 |
    240 |

    Try it

    241 |

    Install skit and run an example project to get a feel for it:

    242 |
     $ npm install skit
    243 |  $ ./node_modules/.bin/skit skeleton skit-example
    244 |  $ ./node_modules/.bin/skit run skit-example --debug
    245 |

    246 | Also check out Getting Started for a more 247 | comprehensive walkthrough. 248 |

    249 |
    250 | 251 |
    252 |

    See skit in action

    253 |

    254 | Visit https://launchkit.io/ and log in to see 255 | skit in action. Inspect the source returned from the server to find bits of skit magic. 256 |

    257 |
    258 | 259 |
    260 |

    FAQ

    261 |
      262 |
    • 263 |

      264 | You seriously made another module loader? 265 |

      266 |

      267 | Yeah. There are several reasons why: 268 |

      269 |
        270 |
      1. 271 | To create a new-feeling environment that is clearly not node, because 272 | you’re writing code that runs in node and IE9 and 273 | Chrome etc. It shouldn’t feel like any old node.js module. 274 |
      2. 275 |
      3. 276 | In order to automatically include stylesheets and template partials 277 | used by other templates. 278 |
      4. 279 |
      5. 280 | To add a notion of internal-module-only includes. Internal includes, usually 281 | partials and secondary stylesheets and sometimes internal classes, can’t 282 | be loaded by other modules. 283 |
      6. 284 |
      285 |
    • 286 |
    • 287 |

      288 | Can I use this with <my favorite framework>? 289 |

      290 |

      291 | Maybe! It won’t help too much if your existing client-side 292 | framework of choice depends on DOM manipulation for rendering, however. 293 |

      294 |

      295 | I have successfully integrated React (and automagic .jsx compilation) 296 | in this example project; 297 | I’m no React expert, but it seems pretty cool. 298 |

      299 |
    • 300 |
    • 301 |

      Did you know “skit” means “shit” in Swedish?!

      302 |

      303 | Now I do! 304 | Tusen tack! Var är toaletten? 305 |

      306 |
    • 307 |
    308 |
    309 |
    310 | 311 | -------------------------------------------------------------------------------- /example/skitjs.com/public/Home.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Controller = skit.platform.Controller; 4 | 5 | var BaseController = library.BaseController; 6 | 7 | var html = __module__.html; 8 | 9 | 10 | return Controller.create(BaseController, { 11 | __title__: function() { 12 | return 'JavaScript web application environment for first-class web clients'; 13 | }, 14 | 15 | __body__: function() { 16 | return html(); 17 | } 18 | }); -------------------------------------------------------------------------------- /example/skitjs.com/public/getting-started/GettingStarted.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Controller = skit.platform.Controller; 4 | 5 | var BaseController = library.BaseController; 6 | 7 | var html = __module__.html; 8 | 9 | 10 | return Controller.create(BaseController, { 11 | __title__: function() { 12 | return 'Getting Started'; 13 | }, 14 | 15 | __body__: function() { 16 | return html(); 17 | } 18 | }); -------------------------------------------------------------------------------- /lib/ControllerRenderer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @license 5 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 6 | * License: MIT 7 | */ 8 | 9 | var events = require('events'); 10 | var fs = require('fs'); 11 | var util = require('util'); 12 | 13 | var Handlebars = require('handlebars'); 14 | 15 | var scriptresource = require('./loader/scriptresource'); 16 | var skitutil = require('./skitutil'); 17 | 18 | 19 | var TargetEnvironment = scriptresource.TargetEnvironment; 20 | 21 | Handlebars.registerHelper('json', function(arg, opt_pretty) { 22 | try { 23 | return new Handlebars.SafeString(skitutil.safeJSONStringify(arg, opt_pretty)); 24 | } catch (e) { 25 | var err = new Error('Could not JSON-encode object: ' + arg); 26 | err.originalError = e; 27 | throw err; 28 | } 29 | }); 30 | 31 | var BOOTSTRAP_TEMPLATE = (function() { 32 | var templateSource = fs.readFileSync(__dirname + '/bootstrap.html').toString(); 33 | return Handlebars.compile(templateSource); 34 | })(); 35 | 36 | var NET_SERVER_MODULE = 'skit.platform.net:server'; 37 | var ENV_SERVER_MODULE = 'skit.platform.env:server'; 38 | var COOKIES_SERVER_MODULE = 'skit.platform.cookies:server'; 39 | var NAVIGATION_MODULE = 'skit.platform.navigation:server'; 40 | var PROXY_MODULE = 'skit.platform.netproxy'; 41 | var PROXY_RESOURCE = PROXY_MODULE + ':js'; 42 | 43 | 44 | function ControllerRenderer(moduleScope, bundles, server, request) { 45 | events.EventEmitter.apply(this); 46 | 47 | this.moduleScope = moduleScope; 48 | this.bundles = bundles; 49 | this.server = server; 50 | this.request = request; 51 | 52 | this.setupProxies_(); 53 | this.setupNet_(); 54 | this.setupCookies_(); 55 | this.setupEnv_(); 56 | } 57 | util.inherits(ControllerRenderer, events.EventEmitter); 58 | 59 | 60 | ControllerRenderer.ERROR = 'rendererror'; 61 | ControllerRenderer.NOT_FOUND = 'notfound'; 62 | ControllerRenderer.REDIRECT = 'redirect'; 63 | ControllerRenderer.WRITE_HTML = 'html'; 64 | ControllerRenderer.DONE_WRITING = 'done'; 65 | 66 | 67 | ControllerRenderer.prototype.setupProxies_ = function() { 68 | var netproxy = this.moduleScope.getObjectByModulePath(PROXY_MODULE); 69 | if (!netproxy) { 70 | return; 71 | } 72 | 73 | this.server.eachProxy(function(name, proxy) { 74 | var reqWrap = { 75 | headers: this.request.headers, 76 | getCookie: this.request.getCookie, 77 | connection: this.request.connection, 78 | }; 79 | var resWrap = { 80 | getCookie: this.request.getCookie, 81 | setCookie: this.request.setCookie, 82 | }; 83 | 84 | var apiRequest; 85 | netproxy.__register__(name, { 86 | modifyRequestInternal: (function(apiRequest_) { 87 | apiRequest = apiRequest_; 88 | try { 89 | proxy.modifyRequest(reqWrap, apiRequest); 90 | } catch(e) { 91 | this.emit(ControllerRenderer.ERROR, e); 92 | } 93 | }).bind(this), 94 | modifyResponseInternal: (function(apiResponse) { 95 | try { 96 | proxy.modifyResponse(apiRequest, apiResponse, resWrap); 97 | } catch(e) { 98 | this.emit(ControllerRenderer.ERROR, e); 99 | } 100 | }).bind(this) 101 | }, this); 102 | }, this); 103 | }; 104 | 105 | 106 | ControllerRenderer.prototype.setupNet_ = function() { 107 | var net = this.moduleScope.getObjectByResourcePath(NET_SERVER_MODULE); 108 | if (!net) { 109 | return; 110 | } 111 | 112 | net.__setErrorHandler__((function(e) { 113 | this.emit(ControllerRenderer.ERROR, e); 114 | }).bind(this)); 115 | }; 116 | 117 | 118 | ControllerRenderer.prototype.setupCookies_ = function() { 119 | var cookies = this.moduleScope.getObjectByResourcePath(COOKIES_SERVER_MODULE); 120 | if (!cookies) { 121 | return; 122 | } 123 | 124 | cookies.__setGetSet__(this.request.getCookie, this.request.setCookie); 125 | }; 126 | 127 | 128 | ControllerRenderer.prototype.setupEnv_ = function() { 129 | var env = this.moduleScope.getObjectByResourcePath(ENV_SERVER_MODULE); 130 | if (!env) { 131 | return; 132 | } 133 | 134 | env.__setEnv__(this.server.env); 135 | }; 136 | 137 | 138 | ControllerRenderer.prototype.serve = function() { 139 | var ControllerKlass = this.moduleScope.mainObject; 140 | if (!ControllerKlass || !ControllerKlass.__controller__) { 141 | // Only controller modules should exist inside "public". 142 | var err = new Error('Module at this path is not a controller.'); 143 | this.emit(ControllerRenderer.ERROR, err); 144 | return; 145 | } 146 | 147 | this.controller = new ControllerKlass(this.request.params); 148 | 149 | var initialControllerProperties = {}; 150 | for (var k in this.controller) { 151 | if (this.controller.hasOwnProperty(k)) { 152 | initialControllerProperties[k] = this.controller[k]; 153 | } 154 | } 155 | 156 | var controllersToLoadInOrder = []; 157 | var CurrentControllerKlass = ControllerKlass; 158 | while (CurrentControllerKlass) { 159 | controllersToLoadInOrder.unshift(CurrentControllerKlass); 160 | CurrentControllerKlass = CurrentControllerKlass.__parent__; 161 | } 162 | 163 | var i = 0; 164 | var preloadNext = (function() { 165 | var CurrentControllerKlass = controllersToLoadInOrder[i++]; 166 | if (!CurrentControllerKlass) { 167 | loadAndRender(); 168 | return; 169 | } 170 | 171 | this.preloadController(CurrentControllerKlass, preloadNext); 172 | }).bind(this); 173 | 174 | var loadAndRender = (function() { 175 | var controllerProperties = {}; 176 | for (var k in this.controller) { 177 | if (this.controller.hasOwnProperty(k) && initialControllerProperties[k] != this.controller[k]) { 178 | controllerProperties[k] = this.controller[k]; 179 | } 180 | } 181 | 182 | this.controller.recursiveLoad(); 183 | 184 | this.render(controllerProperties); 185 | }).bind(this); 186 | 187 | preloadNext(); 188 | }; 189 | 190 | 191 | ControllerRenderer.prototype.preloadController = function(ControllerKlass, onComplete) { 192 | var scheme = this.request.headers['x-forwarded-proto'] || 'http'; 193 | var schemeAndHost = scheme + '://' + this.request.headers['host']; 194 | 195 | var navigation = this.moduleScope.getObjectByResourcePath(NAVIGATION_MODULE); 196 | if (navigation) { 197 | var fullUrl = schemeAndHost + this.request.url; 198 | navigation.__reset__(fullUrl, this.request.headers['user-agent'], this.request.headers['referer']); 199 | } 200 | 201 | var hasPreload = ControllerKlass.prototype.hasOwnProperty('__preload__'); 202 | var preload = ControllerKlass.prototype.__preload__; 203 | if (!hasPreload) { 204 | preload = function defaultPreload(f) { f(); }; 205 | } 206 | 207 | var preloadComplete = (function(var_args) { 208 | if (navigation) { 209 | if (navigation.__notfound__()) { 210 | this.emit(ControllerRenderer.NOT_FOUND); 211 | return; 212 | } 213 | 214 | var redirects = navigation.__redirects__(); 215 | if (redirects && redirects.length) { 216 | var lastRedirect = redirects[redirects.length - 1]; 217 | var redirectUrl = lastRedirect.url; 218 | if (redirectUrl.indexOf(schemeAndHost) == 0) { 219 | redirectUrl = redirectUrl.replace(schemeAndHost, ''); 220 | } 221 | this.emit(ControllerRenderer.REDIRECT, redirectUrl, !!lastRedirect.permanent); 222 | return; 223 | } 224 | } 225 | 226 | onComplete(); 227 | }).bind(this); 228 | 229 | try { 230 | preload.call(this.controller, preloadComplete); 231 | } catch (e) { 232 | this.emit(ControllerRenderer.ERROR, e); 233 | } 234 | }; 235 | 236 | 237 | ControllerRenderer.prototype.render = function(controllerProperties) { 238 | try { 239 | var title = this.controller.getFullTitle(); 240 | var meta = this.controller.getFullMeta(); 241 | var body = this.controller.renderFullBody(); 242 | } catch (e) { 243 | this.emit(ControllerRenderer.ERROR, e); 244 | return; 245 | } 246 | 247 | var cssUrls = {}; 248 | var jsUrls = {}; 249 | 250 | var cssBundleUrls = []; 251 | var jsBundleUrls = []; 252 | 253 | this.bundles.forEach(function(bundle) { 254 | bundle.allStyles().forEach(function(resource) { 255 | var css = this.server.getResourceUrl(resource); 256 | var cssUrl = css.path || css; 257 | var integrity = css.integrity || null; 258 | if (!(cssUrl in cssUrls)) { 259 | cssUrls[cssUrl] = 1; 260 | cssBundleUrls.push({ 261 | bundle: bundle.name, 262 | url: cssUrl, 263 | integrity: integrity, 264 | }); 265 | } 266 | }, this); 267 | 268 | bundle.allScripts().forEach(function(resource) { 269 | if (resource.includeInEnvironment(TargetEnvironment.BROWSER)) { 270 | var js = this.server.getResourceUrl(resource); 271 | var jsUrl = js.path || js; 272 | var integrity = js.integrity || null; 273 | if (!(jsUrl in jsUrls)) { 274 | jsUrls[jsUrl] = 1; 275 | jsBundleUrls.push({ 276 | bundle: bundle.name, 277 | url: jsUrl, 278 | integrity: integrity, 279 | }); 280 | } 281 | } 282 | }, this); 283 | }, this); 284 | 285 | var clientProxyObjects = []; 286 | this.server.eachProxy(function(name, proxy) { 287 | clientProxyObjects.push({ 288 | name: name, 289 | csrfToken: proxy.generateCSRF(this.request) 290 | }); 291 | }, this); 292 | 293 | var html = BOOTSTRAP_TEMPLATE({ 294 | title: title, 295 | meta: meta, 296 | body: body, 297 | currentUrlAfterRedirect: this.request.originalUrl !== this.request.url ? this.request.url : null, 298 | 299 | env: this.server.env, 300 | 301 | cssUrls: cssBundleUrls, 302 | jsUrls: jsBundleUrls, 303 | 304 | netproxyModulePath: PROXY_RESOURCE, 305 | clientProxyObjects: clientProxyObjects, 306 | 307 | params: this.request.params, 308 | controllerModulePath: this.moduleScope.mainObjectResourcePath, 309 | controllerProperties: controllerProperties, 310 | }); 311 | 312 | // TODO(taylor): Render CSS before the preload is done. 313 | this.emit(ControllerRenderer.WRITE_HTML, html); 314 | this.emit(ControllerRenderer.DONE_WRITING); 315 | }; 316 | 317 | 318 | module.exports = ControllerRenderer; 319 | -------------------------------------------------------------------------------- /lib/SkitProxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @license 5 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 6 | * License: MIT 7 | */ 8 | 9 | var crypto = require('crypto'); 10 | 11 | var CSRF_COOKIE_PREFIX = 'csrf_'; 12 | 13 | var X_FORWARDED_FOR = 'x-forwarded-for'; 14 | var X_FORWARDED_PROTO = 'x-forwarded-proto'; 15 | 16 | 17 | function SkitProxy(name, modifyRequest, modifyResponse) { 18 | this.name = name; 19 | this.modifyRequest_ = modifyRequest; 20 | this.modifyResponse_ = modifyResponse; 21 | } 22 | 23 | SkitProxy.prototype.modifyRequest = function(proxyRequest, apiRequest) { 24 | // Include the original remote IP and add our own to the forwarded list. 25 | var xForwardedFor = proxyRequest.headers[X_FORWARDED_FOR]; 26 | var remoteIp = proxyRequest.connection.remoteAddress; 27 | if (xForwardedFor) { 28 | xForwardedFor += ', ' + remoteIp; 29 | } else { 30 | xForwardedFor = remoteIp; 31 | } 32 | apiRequest.headers[X_FORWARDED_FOR] = xForwardedFor; 33 | 34 | // Include whether the request was originally initiated over https. 35 | if (proxyRequest.headers[X_FORWARDED_PROTO]) { 36 | apiRequest.headers[X_FORWARDED_PROTO] = proxyRequest.headers[X_FORWARDED_PROTO]; 37 | } else if (proxyRequest.connection.encrypted) { 38 | apiRequest.headers[X_FORWARDED_PROTO] = 'https'; 39 | } 40 | 41 | this.modifyRequest_(proxyRequest, apiRequest); 42 | }; 43 | 44 | SkitProxy.prototype.modifyResponse = function(apiRequest, apiResponse, proxyResponse) { 45 | this.modifyResponse_(apiRequest, apiResponse, proxyResponse); 46 | }; 47 | 48 | SkitProxy.prototype.cookieName_ = function() { 49 | return CSRF_COOKIE_PREFIX + this.name; 50 | }; 51 | 52 | SkitProxy.prototype.verifyCSRF = function(req, token) { 53 | var cookieValue = req.getCookie(this.cookieName_()); 54 | if (token == cookieValue) { 55 | return true; 56 | } 57 | 58 | console.log('Invalid CSRF token:', token, typeof token, 'expected:', cookieValue, typeof cookieValue, 59 | req.headers['x-forwarded-for']); 60 | 61 | return false; 62 | }; 63 | 64 | SkitProxy.prototype.generateCSRF = function(req) { 65 | var cookieValue = req.getCookie(this.cookieName_()); 66 | if (!cookieValue) { 67 | cookieValue = crypto.randomBytes(16).toString('base64'); 68 | req.setCookie(this.cookieName_(), cookieValue, {httpOnly: true}); 69 | } 70 | return cookieValue; 71 | }; 72 | 73 | module.exports = SkitProxy; -------------------------------------------------------------------------------- /lib/bootstrap.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ title }} 5 | {{#each cssUrls}} 6 | 7 | {{/each}} 8 | {{{ meta }}} 9 | {{#if currentUrlAfterRedirect }} 10 | 17 | {{/if}} 18 | 19 |
    {{{ body }}}
    20 | 50 | {{#each jsUrls}} 51 | 52 | {{/each}} 53 | 75 | -------------------------------------------------------------------------------- /lib/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Error: {{ message }} 4 | 64 | 65 | 66 |
    67 |

    68 | {{#if code }} 69 | [{{code}}] 70 | {{/if}} 71 | Error processing request 72 |

    73 |

    74 | {{#if error }} 75 | {{error}} 76 | {{else}} 77 | {{message}} 78 | {{/if}} 79 |

    80 | 81 | {{#if fileName }} 82 |
    83 |

    Location

    84 |

    {{ fileName }}:{{ lineNumber }}

    85 | {{#if excerptHtml }} 86 |
    {{{ excerptHtml }}}
    87 | {{/if}} 88 | {{/if}} 89 | 90 | {{#if error.stack }} 91 |
    92 |

    Original stack

    93 |
    {{error.stack}}
    94 | {{/if}} 95 |
    96 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @license 5 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 6 | * License: MIT 7 | */ 8 | 9 | var fs = require('fs'); 10 | 11 | var Handlebars = require('handlebars'); 12 | 13 | var skitutil = require('./skitutil'); 14 | 15 | 16 | var ERROR_TEMPLATE = (function() { 17 | var templateSource = fs.readFileSync(__dirname + '/error.html').toString(); 18 | return Handlebars.compile(templateSource); 19 | })(); 20 | 21 | 22 | function renderError(req, res, error) { 23 | var message = error.message || 'Error processing request'; 24 | 25 | console.log('Rendering error response for:', req.url, 'stack:', error.stack); 26 | 27 | var fileName, lineNumber; 28 | if (error.fileName) { 29 | fileName = error.fileName; 30 | lineNumber = error.lineNumber; 31 | } else if (error.stack) { 32 | var firstLine = error.stack.split(/\n/)[1]; 33 | var fileAndLineNumber = firstLine.match(/\((\/.+):(\d+):\d+\)$/); 34 | if (fileAndLineNumber) { 35 | fileName = fileAndLineNumber[1]; 36 | lineNumber = +(fileAndLineNumber[2]); 37 | } 38 | } 39 | 40 | var excerptHtml; 41 | if (fileName && lineNumber) { 42 | var fileContent = ''; 43 | try { 44 | fileContent = fs.readFileSync(fileName, 'utf8'); 45 | } catch (e) { 46 | console.log('Could not read file: ', e); 47 | } 48 | 49 | var lines = fileContent.split(/\n/).map(function(line, i) { 50 | line = '' + (' ' + (i + 1)).slice(-4) + '' + skitutil.escapeHtml(line); 51 | if (i == lineNumber - 1) { 52 | line = '' + line + ''; 53 | } 54 | return line; 55 | }); 56 | var relevantLines = lines.slice(Math.max(0, lineNumber - 5), lineNumber + 5); 57 | excerptHtml = relevantLines.join('\n'); 58 | } 59 | 60 | var html = ERROR_TEMPLATE({ 61 | message: message, 62 | code: error.status, 63 | error: error, 64 | fileName: fileName, 65 | lineNumber: lineNumber, 66 | excerptHtml: excerptHtml 67 | }); 68 | 69 | res.writeHead(error.status || 502, {'Content-Type': 'text/html; charset=utf-8'}); 70 | res.write(html); 71 | res.end(); 72 | } 73 | 74 | 75 | module.exports = { 76 | renderError: renderError 77 | }; 78 | -------------------------------------------------------------------------------- /lib/loader/BundledLoader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var SkitModule = require('./SkitModule'); 5 | var loader = require('./loader'); 6 | 7 | 8 | 9 | function validateBundleConfiguration(bundles) { 10 | if (!Array.isArray(bundles)) { 11 | throw new Error('Bundles should be an array of bundle configuration objects.'); 12 | } 13 | 14 | bundles.forEach(function(bundle) { 15 | if (!bundle) { 16 | throw new Error('Bundle configuration should be an object.'); 17 | } 18 | for (var k in bundle) { 19 | if (['name', 'paths', 'modules', 'options'].indexOf(k) < 0) { 20 | throw new Error('Unknown bundle configuration key: ' + k + '.'); 21 | } 22 | } 23 | if (!bundle.name) { 24 | throw new Error('Add "name" key to bundle configuration.'); 25 | } 26 | if (!(bundle.paths || bundle.modules || []).length) { 27 | throw new Error('Add "paths" or "modules" to bundle configuration.'); 28 | } 29 | (bundle.paths || []).forEach(function(p) { 30 | var index = p.indexOf('*'); 31 | if (index >= 0 && index < p.length - 1) { 32 | throw new Error('Star in "path" must be last character, or not present.'); 33 | } 34 | }); 35 | }); 36 | } 37 | 38 | 39 | function ResourceBundle(name, modules, previouslyIncludedResources, opt_options) { 40 | this.name = name; 41 | 42 | this.resourcePaths = {}; 43 | this.modulePaths = {}; 44 | this.styles = []; 45 | this.scripts = []; 46 | this.options = opt_options || {}; 47 | 48 | modules.forEach(function(module) { 49 | this.modulePaths[module.modulePath] = module; 50 | 51 | var allResources = module.buildResourceList(); 52 | allResources.forEach(function(resource) { 53 | if (!(resource.resourcePath in previouslyIncludedResources) && 54 | !(resource.resourcePath in this.resourcePaths)) { 55 | this.resourcePaths[resource.resourcePath] = resource; 56 | 57 | if (resource.getCssString) { 58 | this.styles.push(resource); 59 | } else { 60 | this.scripts.push(resource); 61 | } 62 | } 63 | }, this); 64 | }, this); 65 | } 66 | 67 | ResourceBundle.prototype.containsResourcePath = function(resourcePath) { 68 | return resourcePath in this.resourcePaths; 69 | }; 70 | 71 | ResourceBundle.prototype.allResourcePaths = function() { 72 | return Object.keys(this.resourcePaths); 73 | }; 74 | 75 | ResourceBundle.prototype.allScripts = function() { 76 | return this.scripts; 77 | }; 78 | 79 | ResourceBundle.prototype.allStyles = function() { 80 | return this.styles; 81 | }; 82 | 83 | 84 | 85 | function allSkitModulesInRoot(root) { 86 | var skitModules = []; 87 | 88 | var children = root.children(); 89 | while (children.length) { 90 | var child = children.shift(); 91 | if (child instanceof SkitModule) { 92 | skitModules.push(child); 93 | } else { 94 | children = children.concat(child.children()); 95 | } 96 | } 97 | 98 | return skitModules; 99 | } 100 | 101 | 102 | function pathToModulePathComponents_(fullPath) { 103 | var parts = fullPath.split(path.sep); 104 | if (parts[0] == '') { 105 | parts.splice(0, 1); 106 | } 107 | if (parts[parts.length - 1] == '') { 108 | parts.pop(); 109 | } 110 | return parts; 111 | } 112 | 113 | 114 | function BundledLoader(packagePath, publicRootName, bundleConfig) { 115 | this.packagePath_ = packagePath; 116 | this.publicRootName_ = publicRootName; 117 | validateBundleConfiguration(bundleConfig); 118 | this.bundleConfig_ = bundleConfig; 119 | 120 | this.load_(); 121 | } 122 | 123 | 124 | BundledLoader.prototype.reload = function() { 125 | this.load_(); 126 | }; 127 | 128 | 129 | BundledLoader.prototype.load_ = function() { 130 | console.log('[skit] loading module tree') 131 | var root = loader.buildModuleTree(this.packagePath_); 132 | var skit = loader.loadSkitTree(); 133 | root.addChildNode(skit); 134 | this.root_ = root; 135 | 136 | var previouslyIncludedResources = {}; 137 | this.bundles_ = this.bundleConfig_.map(function(bundle) { 138 | var bundle = this.loadBundleFromConfig_(bundle, previouslyIncludedResources); 139 | bundle.allResourcePaths().forEach(function(modulePath) { 140 | previouslyIncludedResources[modulePath] = 1; 141 | }); 142 | return bundle; 143 | }, this); 144 | }; 145 | 146 | 147 | BundledLoader.prototype.getPublicRoot = function() { 148 | return this.root_.getChildWithName(this.publicRootName_); 149 | }; 150 | 151 | 152 | BundledLoader.prototype.loadBundleFromConfig_ = function(config, previouslyIncludedResources) { 153 | var modulesToInclude = []; 154 | 155 | var paths = config.paths || []; 156 | paths.forEach(function(fullPath) { 157 | var parts = pathToModulePathComponents_(fullPath); 158 | var publicRoot = this.getPublicRoot(); 159 | 160 | // parts can be empty for "/", which should just be the homepage module. 161 | var lastPart = parts[parts.length - 1] || ''; 162 | if (lastPart.indexOf('*') >= 0) { 163 | var matcher = parts.pop(); 164 | var base = publicRoot.findNodeWithPathComponents(parts); 165 | matcher = matcher.substring(0, matcher.length - 1); 166 | if (!matcher) { 167 | // matcher is '*' for this path -- include all my children, which includes 168 | // the controller at this path. (eg. /* -- includes /Home.js and /foo/Foo.js) 169 | var allMyModules = allSkitModulesInRoot(base); 170 | modulesToInclude = modulesToInclude.concat(allMyModules); 171 | } else { 172 | // matcher has a child path, so filter my children based on that and don't 173 | // include modules at this level. 174 | base.eachChild(function(node) { 175 | if (node.name.indexOf(matcher) == 0) { 176 | var allMyModules = allSkitModulesInRoot(node); 177 | modulesToInclude = modulesToInclude.concat(allMyModules); 178 | } 179 | }, this); 180 | } 181 | 182 | } else { 183 | var base = publicRoot.findNodeWithPathComponents(parts); 184 | base.eachChild(function(node) { 185 | if (node instanceof SkitModule) { 186 | modulesToInclude.push(node); 187 | } 188 | }, this); 189 | 190 | } 191 | }, this); 192 | 193 | var modules = config.modules || []; 194 | modules.forEach(function(moduleName) { 195 | var module = this.root_.findNodeWithPath(moduleName); 196 | if (!module) { 197 | throw new Error('Unable to find module: ' + moduleName + ' for bundle: ' + config.name); 198 | } 199 | 200 | if (module instanceof SkitModule) { 201 | modulesToInclude.push(module); 202 | } else { 203 | // recursively find all children that are skit modules. 204 | module.descendants().forEach(function(module) { 205 | if (module instanceof SkitModule) { 206 | modulesToInclude.push(module); 207 | } 208 | }); 209 | } 210 | }, this); 211 | 212 | return new ResourceBundle(config.name, modulesToInclude, previouslyIncludedResources, config.options); 213 | }; 214 | 215 | 216 | BundledLoader.prototype.bundlesRequiredForModule = function(module) { 217 | var allResourcePaths = module.buildResourceList().map(function(res) { 218 | return res.resourcePath; 219 | }, this); 220 | 221 | var needsCatchallBundle = false; 222 | var includedResourcePaths = {}; 223 | var bundles = []; 224 | 225 | // Load these backwards, because dependencies start from the 226 | // target (last) module rather than the other way around. 227 | allResourcePaths.reverse(); 228 | allResourcePaths.forEach(function(resourcePath) { 229 | var includedBundle = includedResourcePaths[resourcePath]; 230 | if (includedBundle) { 231 | // continue; 232 | return; 233 | } 234 | 235 | var notFound = this.bundles_.every(function(bundle) { 236 | if (bundle.containsResourcePath(resourcePath)) { 237 | bundles.push(bundle); 238 | bundle.allResourcePaths().forEach(function(rp) { 239 | includedResourcePaths[rp] = bundle.name; 240 | }); 241 | return false; 242 | } 243 | return true; 244 | }, this); 245 | 246 | if (notFound) { 247 | needsCatchallBundle = true; 248 | } 249 | }, this); 250 | 251 | bundles.reverse(); 252 | if (needsCatchallBundle) { 253 | bundles.push(new ResourceBundle('catchall', [module], includedResourcePaths)); 254 | } 255 | 256 | return bundles; 257 | }; 258 | 259 | 260 | BundledLoader.prototype.allBundles = function() { 261 | return this.bundles_; 262 | }; 263 | 264 | 265 | BundledLoader.prototype.resourceAtModulePath = function(modulePath, resourceName) { 266 | var module = this.root_.findNodeWithPath(modulePath); 267 | if (module) { 268 | return module.getResourceNamed(resourceName); 269 | } 270 | return null; 271 | }; 272 | 273 | 274 | module.exports = BundledLoader; 275 | -------------------------------------------------------------------------------- /lib/loader/NamedNode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @license 5 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 6 | * License: MIT 7 | */ 8 | 9 | 10 | function NamedNode(name) { 11 | this.name = name; 12 | this.parent = null; 13 | this.children_ = {}; 14 | } 15 | 16 | 17 | NamedNode.prototype.root = function() { 18 | var current = this; 19 | while (current.parent) { 20 | current = current.parent; 21 | if (current === this) { 22 | throw new Error('Cyclical tree.'); 23 | } 24 | } 25 | return current; 26 | }; 27 | 28 | 29 | NamedNode.prototype.findNodeWithPath = function(string, opt_separator) { 30 | var separator = opt_separator || '.'; 31 | var components = string.split(separator).filter(function(s) { return !!s; }); 32 | 33 | return this.findNodeWithPathComponents(components); 34 | }; 35 | 36 | 37 | NamedNode.prototype.findNodeWithPathComponents = function(components) { 38 | var current = this; 39 | while (current && components.length) { 40 | current = current.getChildWithName(components[0]); 41 | components = components.slice(1); 42 | } 43 | return current; 44 | }; 45 | 46 | 47 | NamedNode.prototype.contains = function(node) { 48 | var current = node; 49 | while (current.parent) { 50 | current = current.parent; 51 | if (current === this) { 52 | return true; 53 | } 54 | if (current === node) { 55 | throw new Error('Cyclical tree.'); 56 | } 57 | } 58 | return false; 59 | }; 60 | 61 | 62 | NamedNode.prototype.order = function() { 63 | var i = 0; 64 | var current = this; 65 | while (current.parent) { 66 | current = current.parent; 67 | i++; 68 | if (current === this) { 69 | throw new Error('Cyclical tree.'); 70 | } 71 | } 72 | return i; 73 | }; 74 | 75 | 76 | NamedNode.prototype.addChildNode = function(node) { 77 | if (node.parent) { 78 | node.parent.removeChildNode(node); 79 | } 80 | node.parent = this; 81 | this.children_[node.name] = node; 82 | }; 83 | 84 | 85 | NamedNode.prototype.removeChildNode = function(node) { 86 | if (node.parent === this) { 87 | delete this.children_[node.name]; 88 | node.parent = null; 89 | } 90 | }; 91 | 92 | 93 | NamedNode.prototype.getChildWithName = function(name) { 94 | return this.children_[name] || null; 95 | }; 96 | 97 | 98 | NamedNode.prototype.children = function() { 99 | var children = []; 100 | for (var n in this.children_) { 101 | children.push(this.children_[n]); 102 | } 103 | return children; 104 | }; 105 | 106 | 107 | NamedNode.prototype.eachChild = function(fn, opt_context) { 108 | for (var n in this.children_) { 109 | fn.call(opt_context, this.children_[n]); 110 | } 111 | }; 112 | 113 | 114 | NamedNode.prototype.childNames = function() { 115 | return Object.keys(this.children_); 116 | }; 117 | 118 | 119 | NamedNode.prototype.descendants = function() { 120 | if (this.__handling) { 121 | throw new Error('Cyclical tree.'); 122 | } 123 | this.__handling = true; 124 | 125 | var list = []; 126 | for (var n in this.children_) { 127 | var child = this.children_[n]; 128 | child.descendants().forEach(function(child) { 129 | list.push(child); 130 | }); 131 | list.push(child); 132 | } 133 | 134 | delete this.__handling; 135 | return list; 136 | }; 137 | 138 | 139 | NamedNode.prototype.toJSON = function() { 140 | var result = {'__name__': this.name}; 141 | for (var sub in this.children_) { 142 | result[sub] = this.children_[sub]; 143 | } 144 | return result; 145 | }; 146 | 147 | 148 | NamedNode.prototype.nodePath = function() { 149 | var parts = []; 150 | var current = this; 151 | while (current && current.name) { 152 | parts.unshift(current.name); 153 | current = current.parent; 154 | } 155 | return parts; 156 | }; 157 | 158 | 159 | module.exports = NamedNode; 160 | -------------------------------------------------------------------------------- /lib/loader/SkitModule.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @license 5 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 6 | * License: MIT 7 | */ 8 | 9 | var fs = require('fs'); 10 | var path = require('path'); 11 | var util = require('util'); 12 | 13 | var NamedNode = require('./NamedNode'); 14 | var scriptresource = require('./scriptresource'); 15 | var styleresource = require('./styleresource'); 16 | var TargetEnvironment = scriptresource.TargetEnvironment; 17 | 18 | 19 | function SkitModule(name, modulePath) { 20 | NamedNode.call(this, name); 21 | this.modulePath = modulePath; 22 | 23 | this.scripts_ = {}; 24 | this.styles_ = {}; 25 | } 26 | util.inherits(SkitModule, NamedNode); 27 | 28 | 29 | SkitModule.moduleName = function(fullPath) { 30 | var basename = path.basename(fullPath); 31 | // Foo.js, Foo_bar.js, Foo.bar.js, Foo_bar.bz.js -> all belong to the "Foo" module. 32 | var moduleName = basename.split('.').slice(0, 1)[0]; 33 | return moduleName.split('_').slice(0, 1)[0]; 34 | }; 35 | 36 | 37 | SkitModule.prototype.addFile = function(fullPath) { 38 | var basename = path.basename(fullPath); 39 | 40 | // Foo.js -> 'js' 41 | // Foo.html -> 'html' 42 | // Foo_bar.html -> 'bar.html' 43 | // Foo_bar.js -> 'bar' 44 | if (basename.indexOf(this.name) != 0) { 45 | var err = new Error('Invalid module file, does not match module name: ' + this.name); 46 | err.fileName = fullPath; 47 | throw err; 48 | } 49 | var nickname = basename.replace(this.name, '').replace(/^[_.]+/, '').replace(/\.js$/, ''); 50 | 51 | var extension = path.extname(fullPath); 52 | var isStyle = false; 53 | var ResourceKlass = scriptresource.getResourceWrapper(extension); 54 | if (!ResourceKlass) { 55 | isStyle = true; 56 | ResourceKlass = styleresource.getResourceWrapper(extension); 57 | } 58 | 59 | if (!ResourceKlass) { 60 | var err = new Error('Invalid resource -- could not identify wrapper: ' + fullPath); 61 | err.fileName = fullPath; 62 | throw err; 63 | } 64 | 65 | var source = fs.readFileSync(fullPath).toString(); 66 | var resourcePath = this.modulePath + ':' + nickname; 67 | var resource = new ResourceKlass(fullPath, resourcePath, source); 68 | 69 | if (isStyle) { 70 | this.styles_[nickname] = resource; 71 | } else { 72 | this.scripts_[nickname] = resource; 73 | } 74 | }; 75 | 76 | 77 | SkitModule.prototype.getResourceNamed = function(name) { 78 | this.buildResourceList(); 79 | return this.scripts_[name] || this.styles_[name]; 80 | }; 81 | 82 | 83 | SkitModule.prototype.buildResourceList = function() { 84 | if (!this.__resourceList__) { 85 | var mainNickname = 'js'; 86 | if (!(mainNickname in this.scripts_)) { 87 | mainNickname = Object.keys(this.scripts_)[0]; 88 | } 89 | 90 | var alwaysInclude = this.buildAlwaysIncludeResourceList_(); 91 | var resourcesList; 92 | if (mainNickname) { 93 | resourcesList = this.buildResourceListForScriptNamed_(mainNickname); 94 | Array.prototype.splice.apply(resourcesList, [-1, 0].concat(alwaysInclude)); 95 | } else { 96 | resourcesList = alwaysInclude; 97 | } 98 | this.__resourceList__ = resourcesList; 99 | } 100 | return this.__resourceList__; 101 | }; 102 | 103 | 104 | SkitModule.prototype.buildAlwaysIncludeResourceList_ = function() { 105 | return Object.keys(this.styles_).map(function(k) { return this.styles_[k]; }, this); 106 | }; 107 | 108 | 109 | SkitModule.prototype.buildResourceListForScriptNamed_ = function(name) { 110 | var loaded = {}; 111 | var all = []; 112 | 113 | var scriptResource = this.scripts_[name]; 114 | if (!scriptResource) { 115 | throw new Error('Invalid reference to submodule "' + name + '" in module ' + this.modulePath); 116 | } 117 | 118 | var relativeDependencies = scriptResource.getRelativeDependencyPaths(); 119 | var absoluteDependencies = []; 120 | 121 | relativeDependencies.forEach(function(dependencyPath) { 122 | var resources = this.getResourceListForRelativeDependency_(dependencyPath); 123 | if (!resources) { 124 | throw new Error('Invalid dependency: "' + dependencyPath + '" in module: ' + this.modulePath + ':' + name); 125 | } 126 | 127 | var absoluteDependency = resources[resources.length - 1]; 128 | absoluteDependencies.push(absoluteDependency.resourcePath); 129 | 130 | resources.forEach(function(resource) { 131 | if (resource.resourcePath in loaded) { 132 | // continue 133 | return; 134 | } 135 | 136 | loaded[resource.resourcePath] = true; 137 | all.push(resource); 138 | }); 139 | }, this); 140 | 141 | all.push(scriptResource); 142 | scriptResource.setAbsoluteDependencyPaths(absoluteDependencies); 143 | 144 | return all; 145 | }; 146 | 147 | 148 | SkitModule.prototype.getResourceListForRelativeDependency_ = function(relativePath) { 149 | // Inner-module dependency; load that file first. 150 | if (relativePath.indexOf('__module__.') == 0) { 151 | var depNickname = relativePath.replace('__module__.', ''); 152 | return this.buildResourceListForScriptNamed_(depNickname); 153 | } 154 | 155 | // Dependency in another module -- find its main object. 156 | var dependency = this.root().findNodeWithPath(relativePath); 157 | if (!dependency || !dependency.buildResourceList) { 158 | return null; 159 | } 160 | 161 | return dependency.buildResourceList(); 162 | }; 163 | 164 | 165 | module.exports = SkitModule; 166 | -------------------------------------------------------------------------------- /lib/loader/loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @license 5 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 6 | * License: MIT 7 | */ 8 | 9 | var path = require('path'); 10 | var fs = require('fs'); 11 | 12 | var NamedNode = require('./NamedNode'); 13 | var SkitModule = require('./SkitModule'); 14 | var pooledmoduleloader = require('./pooledmoduleloader'); 15 | 16 | 17 | function walkSync(dir) { 18 | var remaining = 1; 19 | 20 | var filePaths = []; 21 | var paths = fs.readdirSync(dir); 22 | paths.forEach(function(pathToTest) { 23 | pathToTest = dir + path.sep + pathToTest; 24 | var stat = fs.statSync(pathToTest); 25 | if (stat.isDirectory()) { 26 | var paths = walkSync(pathToTest); 27 | paths.forEach(function(file) { 28 | filePaths.push(file); 29 | }); 30 | } else { 31 | filePaths.push(pathToTest); 32 | } 33 | }); 34 | return filePaths; 35 | } 36 | module.exports.walkSync = walkSync; 37 | 38 | 39 | function mkdirPSync(dir) { 40 | // 'taylor' to '/home/taylor' to ['home', 'taylor'] 41 | var parts = path.normalize(dir).split(path.sep); 42 | 43 | for (var i = 0; i < parts.length; i++) { 44 | var currentPath = parts.slice(0, i + 1).join(path.sep) || path.sep; 45 | currentPath = path.normalize(currentPath); 46 | 47 | try { 48 | fs.mkdirSync(currentPath); 49 | } catch (e) { 50 | if (e.code != 'EEXIST' && e.code != 'EISDIR') { 51 | throw e; 52 | } 53 | } 54 | } 55 | } 56 | module.exports.mkdirPSync = mkdirPSync; 57 | 58 | 59 | function buildModuleTree(rootPath, opt_rootName) { 60 | var root = new NamedNode(opt_rootName); 61 | 62 | var realPath = fs.realpathSync(rootPath); 63 | var files = walkSync(realPath); 64 | 65 | files.forEach(function(file) { 66 | var relativePath = file.replace(realPath + path.sep, ''); 67 | if (relativePath.indexOf('__') == 0) { 68 | // continue 69 | return; 70 | } 71 | 72 | var basename = path.basename(relativePath); 73 | if (basename.substring(0, 1) == '.') { 74 | // continue 75 | return; 76 | } 77 | 78 | var dirname = path.dirname(relativePath); 79 | var parent = root; 80 | if (dirname != '.') { 81 | dirname.split(path.sep).forEach(function(component) { 82 | var child = parent.getChildWithName(component); 83 | if (!child) { 84 | var child = new NamedNode(component); 85 | parent.addChildNode(child); 86 | } 87 | parent = child; 88 | }); 89 | } 90 | 91 | var moduleName = SkitModule.moduleName(file); 92 | var moduleNode = parent.getChildWithName(moduleName); 93 | if (!moduleNode) { 94 | var modulePath = parent.nodePath().concat([moduleName]).join('.'); 95 | moduleNode = new SkitModule(moduleName, modulePath); 96 | parent.addChildNode(moduleNode); 97 | } 98 | 99 | try { 100 | moduleNode.addFile(file); 101 | } catch (e) { 102 | e.fileName = file; 103 | throw e; 104 | } 105 | }); 106 | 107 | return root; 108 | } 109 | module.exports.buildModuleTree = buildModuleTree; 110 | 111 | 112 | var __skitTree__ = null; 113 | function globalScopedLoaderForModule(modulePath, cb) { 114 | if (!__skitTree__) { 115 | __skitTree__ = new NamedNode('root'); 116 | __skitTree__.addChildNode(loadSkitTree()); 117 | 118 | pooledmoduleloader.setPoolSize('global', 10); 119 | } 120 | var module = __skitTree__.findNodeWithPath(modulePath); 121 | 122 | pooledmoduleloader.borrowModuleScope('global', module, function(scope) { 123 | cb(scope); 124 | }); 125 | } 126 | module.exports.globalScopedLoaderForModule = globalScopedLoaderForModule; 127 | 128 | 129 | function loadSkitTree() { 130 | // Note that this shouldn't be cached, since a tree in memory can only 131 | // belong to a single parent, and we take this tree and add it to 132 | // a different tree in load() below. 133 | var skitPath = path.resolve(__dirname, '..', 'skit'); 134 | console.log('[skit] Loading skit in: ' + skitPath); 135 | return buildModuleTree(skitPath, 'skit'); 136 | } 137 | module.exports.loadSkitTree = loadSkitTree; 138 | -------------------------------------------------------------------------------- /lib/loader/pooledmoduleloader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @license 5 | * (c) 2015 Cluster Labs, Inc. https://cluster.co/ 6 | * License: MIT 7 | */ 8 | 9 | var vm = require('vm'); 10 | 11 | var scriptresource = require('./scriptresource'); 12 | var TargetEnvironment = scriptresource.TargetEnvironment; 13 | 14 | 15 | function ModuleLoaderPool(size) { 16 | this.size = size; 17 | this.available = size; 18 | this.built = 0; 19 | this.pool = []; 20 | this.waiters = []; 21 | } 22 | 23 | ModuleLoaderPool.prototype.getLoader = function(cb) { 24 | if (this.available) { 25 | this.available--; 26 | var loader = this.pool.shift(); 27 | if (!loader) { 28 | loader = new ProgressiveModuleLoader(); 29 | this.built++; 30 | //console.log('built new module loader'); 31 | } 32 | cb(loader); 33 | } else { 34 | this.waiters.push(cb); 35 | } 36 | }; 37 | 38 | ModuleLoaderPool.prototype.release = function(loader) { 39 | this.pool.push(loader); 40 | this.available++; 41 | 42 | if (this.waiters.length) { 43 | var cb = this.waiters.shift(); 44 | this.getLoader(cb); 45 | } 46 | }; 47 | 48 | 49 | 50 | function ProgressiveModuleLoader() { 51 | // TODO(Taylor): Limit require() usage here to specific modules? 52 | // Or provide a few globally required things? 53 | this.context = vm.createContext({ 54 | require: require, 55 | console: console, 56 | }); 57 | 58 | this.objectsByResourcePath = {}; 59 | this.objectsByModulePath = {}; 60 | this.mainResourceByModulePath = {}; 61 | } 62 | 63 | 64 | ProgressiveModuleLoader.prototype.loadModule = function(module) { 65 | var allResources = module.buildResourceList(); 66 | 67 | for (var i = 0; i < allResources.length; i++) { 68 | var resource = allResources[i]; 69 | if (resource.getCssString || resource.resourcePath in this.objectsByResourcePath) { 70 | continue; 71 | } 72 | 73 | if (!resource.includeInEnvironment(TargetEnvironment.SERVER)) { 74 | continue; 75 | } 76 | 77 | // console.log('loading resource:', resource.resourcePath); 78 | 79 | var script = resource.__script__; 80 | if (!script) { 81 | // Errors here bubble up to the try/catch around serveController(). 82 | var functionString = resource.getFunctionString(); 83 | script = resource.__script__ = vm.createScript(functionString, resource.filePath); 84 | } 85 | 86 | var evaluatedFunction = script.runInContext(this.context); 87 | var evaluatedDependencies = resource.getAbsoluteDependencyPaths().map(function(resourcePath) { 88 | return this.objectsByResourcePath[resourcePath]; 89 | }, this); 90 | 91 | var modulePath = resource.resourcePath.split(':')[0]; 92 | 93 | var evaluated = evaluatedFunction.apply({}, evaluatedDependencies); 94 | this.objectsByResourcePath[resource.resourcePath] = evaluated; 95 | // This might be set multiple times for multiple resources in a module, 96 | // but will eventually be correct. 97 | this.objectsByModulePath[modulePath] = evaluated; 98 | this.mainResourceByModulePath[modulePath] = resource; 99 | }; 100 | }; 101 | 102 | 103 | 104 | function LoadedModuleScope(module, pool, loader) { 105 | this.module = module; 106 | 107 | this.pool = pool; 108 | this.loader = loader; 109 | this.loader.loadModule(module); 110 | 111 | this.mainObject = this.loader.objectsByModulePath[module.modulePath]; 112 | this.mainObjectResourcePath = this.loader.mainResourceByModulePath[module.modulePath].resourcePath; 113 | } 114 | 115 | LoadedModuleScope.prototype.getObjectByResourcePath = function(resourcePath) { 116 | return this.loader.objectsByResourcePath[resourcePath]; 117 | }; 118 | 119 | LoadedModuleScope.prototype.getObjectByModulePath = function(modulePath) { 120 | return this.loader.objectsByModulePath[modulePath]; 121 | }; 122 | 123 | LoadedModuleScope.prototype.release = function() { 124 | if (!this.pool) { 125 | console.log('[skit internal] A loaded module scope was released multiple times.'); 126 | } 127 | 128 | this.pool.release(this.loader); 129 | delete this.loader; 130 | delete this.pool; 131 | }; 132 | 133 | 134 | 135 | var pools_ = {}; 136 | 137 | module.exports = { 138 | setPoolSize: function(name, size) { 139 | pools_[name] = new ModuleLoaderPool(size); 140 | }, 141 | 142 | resetPool: function(name) { 143 | pools_[name] = new ModuleLoaderPool(pools_[name].size); 144 | }, 145 | 146 | borrowModuleScope: function(name, module, cb, opt_context) { 147 | var myPool = pools_[name]; 148 | myPool.getLoader(function(loader) { 149 | var scope = new LoadedModuleScope(module, myPool, loader); 150 | cb.call(opt_context, scope); 151 | }); 152 | } 153 | }; 154 | -------------------------------------------------------------------------------- /lib/loader/scriptresource.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @license 5 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 6 | * License: MIT 7 | */ 8 | 9 | var path = require('path'); 10 | var util = require('util'); 11 | 12 | var Handlebars = require('handlebars'); 13 | var acorn = require('acorn'); 14 | 15 | 16 | var TargetEnvironment = { 17 | BROWSER: 'browser', 18 | SERVER: 'server' 19 | }; 20 | module.exports.TargetEnvironment = TargetEnvironment; 21 | 22 | 23 | function ScriptResource(filePath, resourcePath, source) { 24 | this.filePath = filePath; 25 | this.resourcePath = resourcePath; 26 | this.source = source; 27 | } 28 | module.exports.ScriptResource = ScriptResource; 29 | 30 | ScriptResource.prototype.getRelativeDependencyPaths = function() { 31 | if (!this.relativeDependencyPaths_) { 32 | this.relativeDependencyPaths_ = this.findDependencyPaths_(); 33 | } 34 | return this.relativeDependencyPaths_; 35 | }; 36 | 37 | ScriptResource.prototype.getAbsoluteDependencyPaths = function() { 38 | return this.absoluteDependencyPaths_; 39 | }; 40 | ScriptResource.prototype.setAbsoluteDependencyPaths = function(paths) { 41 | this.absoluteDependencyPaths_ = paths; 42 | }; 43 | 44 | ScriptResource.prototype.getFunctionString = function() { 45 | if (!this.functionString_) { 46 | this.functionString_ = this.buildFunctionString_(); 47 | this.cleanup_(); 48 | } 49 | return this.functionString_; 50 | }; 51 | 52 | ScriptResource.prototype.findDependencyPaths_ = function() { 53 | // TO OVERRIDE. 54 | return []; 55 | }; 56 | 57 | ScriptResource.prototype.aliasForDependencyPath = function(dependencyPath) { 58 | return dependencyPath.replace(/[^a-zA-Z0-9_]/g, '_'); 59 | }; 60 | 61 | ScriptResource.prototype.buildFunctionString_ = function() { 62 | // TO OVERRIDE. 63 | return '(function(){})'; 64 | }; 65 | 66 | ScriptResource.prototype.includeInEnvironment = function(targetEnvironment) { 67 | // TO OVERRIDE. 68 | return true; 69 | }; 70 | 71 | ScriptResource.prototype.cleanup_ = function() { 72 | }; 73 | 74 | ScriptResource.prototype.bodyContentType = function() { 75 | var deps = this.getAbsoluteDependencyPaths(); 76 | var body = [ 77 | 'skit.define(' + JSON.stringify(this.resourcePath) + ', ' + JSON.stringify(deps) + ', function() {', 78 | ' return (' + this.getFunctionString() + ').apply(this, arguments)', 79 | '});' 80 | ].join(''); 81 | 82 | return { 83 | contentType: 'application/javascript', 84 | body: body, 85 | } 86 | }; 87 | 88 | 89 | var ACORN_OPTIONS = { 90 | allowReturnOutsideFunction: true, 91 | // Does not work in older IE, so disallow here. 92 | allowTrailingCommas: false, 93 | }; 94 | 95 | var JavaScriptResource = function() { 96 | ScriptResource.apply(this, arguments); 97 | }; 98 | util.inherits(JavaScriptResource, ScriptResource); 99 | 100 | JavaScriptResource.prototype.getParsedBody = function() { 101 | if (!this.parsed_) { 102 | try { 103 | this.parsed_ = acorn.parse(this.source, ACORN_OPTIONS); 104 | } catch(e) { 105 | e.fileName = this.filePath; 106 | if (e.loc) { 107 | e.lineNumber = e.loc.line; 108 | } 109 | console.log('Error parsing: ', e.fileName, e.lineNumber ? '(line ' + e.lineNumber + ')' : undefined); 110 | throw e; 111 | } 112 | } 113 | return this.parsed_.body; 114 | }; 115 | 116 | JavaScriptResource.prototype.includeInEnvironment = function(environment) { 117 | if (!this.initializedIncluded_) { 118 | this.initializedIncluded_ = true; 119 | 120 | var parsedBody = this.getParsedBody(); 121 | 122 | this.serverOnly_ = false; 123 | this.browserOnly_ = false; 124 | 125 | for (var i = 0; i < parsedBody.length; i++) { 126 | var node = parsedBody[i]; 127 | if (node.type == 'ExpressionStatement' && node.expression.type == 'Literal') { 128 | var value = node.expression.value; 129 | if (value === 'server-only') { 130 | this.serverOnly_ = true; 131 | } else if (value === 'browser-only') { 132 | this.browserOnly_ = true; 133 | } 134 | } else { 135 | break; 136 | } 137 | } 138 | } 139 | 140 | if (this.browserOnly_) { 141 | return environment == TargetEnvironment.BROWSER; 142 | } 143 | 144 | if (this.serverOnly_) { 145 | return environment == TargetEnvironment.SERVER; 146 | } 147 | 148 | return true; 149 | }; 150 | 151 | JavaScriptResource.prototype.findDependencyPaths_ = function() { 152 | var dependencies = []; 153 | 154 | var body = this.getParsedBody(); 155 | for (var i = 0; i < body.length; i++) { 156 | var node = body[i]; 157 | if (node.type != 'VariableDeclaration') { 158 | // Allows for 'use strict'; 159 | if (node.type == 'ExpressionStatement' && node.expression.type == 'Literal') { 160 | continue; 161 | } else { 162 | break; 163 | } 164 | } 165 | 166 | var declarations = node.declarations; 167 | var gotAny = false; 168 | for (var j = 0; j < declarations.length; j++) { 169 | var declaration = declarations[j]; 170 | if (!declaration.init || declaration.init.type != 'MemberExpression') { 171 | continue; 172 | } 173 | 174 | var dependency = this.source.substring(declaration.init.start, declaration.init.end); 175 | dependencies.push(dependency); 176 | gotAny = true; 177 | } 178 | 179 | if (!gotAny) { 180 | break; 181 | } 182 | } 183 | 184 | return dependencies; 185 | }; 186 | 187 | var regexEscape = function(str) { 188 | return str.replace(/[\[\]\/\\{}()*+?.^$|-]/g, '\\$&'); 189 | }; 190 | 191 | JavaScriptResource.prototype.buildFunctionString_ = function() { 192 | var source = this.source; 193 | var depList = this.getRelativeDependencyPaths(); 194 | 195 | var aliases = []; 196 | for (var i = 0; i < depList.length; i++) { 197 | var dependencyPath = depList[i]; 198 | var alias = this.aliasForDependencyPath(dependencyPath); 199 | 200 | // Replace all foo.bar with foo_bar_12345 aliases, but only when 201 | // we know for sure it's an assignment situation. 202 | var regex = new RegExp('=\\s*' + regexEscape(dependencyPath) + '(?=\\s*(?:[,;]|$))', 'gm'); 203 | source = source.split(regex).join('= ' + alias); 204 | 205 | aliases.push(alias); 206 | } 207 | 208 | // Note: I'm sorry. This is all on one line to keep 209 | // line numbers the same in generated code. 210 | source = [ 211 | "var module = {exports: {}};", 212 | "var defined = null;", 213 | "function define() {", 214 | " for (var i = 0; i < arguments.length; i++) {", 215 | " if (typeof arguments[i] == 'function') { defined = arguments[i](); break; }", 216 | " }", 217 | "}", 218 | "define.amd = true;", 219 | 220 | "var result = (function " + this.resourcePath.replace(/[^\w]/g,'_') + "() {", 221 | ].join(' ') + source + "})(); return result || defined || module.exports;"; 222 | 223 | // Build a function with the given source, using aliases as arguments. 224 | // Then call the function with the actual objects in the correct order. 225 | var functionDefinition = '(function(' + aliases.join(',') + ') { ' + source + ' })'; 226 | return functionDefinition; 227 | }; 228 | 229 | JavaScriptResource.prototype.cleanup_ = function() { 230 | delete this.parsed_; 231 | }; 232 | 233 | 234 | 235 | function HandlebarsResource() { 236 | ScriptResource.apply(this, arguments); 237 | } 238 | util.inherits(HandlebarsResource, ScriptResource); 239 | 240 | HandlebarsResource.HANDLEBARS_MODULE = 'skit.thirdparty.handlebars'; 241 | 242 | HandlebarsResource.prototype.findDependencyPaths_ = function() { 243 | var deps = [HandlebarsResource.HANDLEBARS_MODULE]; 244 | 245 | var source = this.source; 246 | var matcher = /\{\{>\s*([\w.]+)/g; 247 | var result; 248 | while (result = matcher.exec(source)) { 249 | deps.push(result[1]); 250 | } 251 | 252 | return deps; 253 | }; 254 | 255 | HandlebarsResource.prototype.aliasForDependencyPath = function(dependencyPath) { 256 | if (dependencyPath == HandlebarsResource.HANDLEBARS_MODULE) { 257 | return 'Handlebars'; 258 | } 259 | return ScriptResource.prototype.aliasForDependencyPath.call(this, dependencyPath); 260 | }; 261 | 262 | HandlebarsResource.prototype.buildFunctionString_ = function() { 263 | var source = this.source; 264 | var depList = this.getRelativeDependencyPaths(); 265 | 266 | var args = []; 267 | var partials = []; 268 | depList.forEach(function(dependencyPath) { 269 | var alias = this.aliasForDependencyPath(dependencyPath); 270 | source = source.split(dependencyPath).join(alias); 271 | args.push(alias); 272 | 273 | if (dependencyPath != HandlebarsResource.HANDLEBARS_MODULE) { 274 | // All other dependencies are partials. 275 | partials.push(alias); 276 | } 277 | }, this); 278 | 279 | // Don't look at me that way. I know. I KNOW! 280 | var partialDeclarations = partials.map(function(alias) { 281 | return JSON.stringify(alias) + ': ' + alias; 282 | }); 283 | var partialMapString = '{' + partialDeclarations.join(',') + '}'; 284 | 285 | var template; 286 | try { 287 | // TODO(Taylor): Allow other options to be passed in somehow. 288 | template = Handlebars.precompile(source, { 289 | preventIndent: true 290 | }); 291 | 292 | } catch (e) { 293 | e.fileName = this.filePath; 294 | var lineNumberMatch = (e+'').match(/Parse error on line (\d+)/); 295 | if (lineNumberMatch) { 296 | e.lineNumber = +(lineNumberMatch[1]); 297 | } 298 | throw e; 299 | } 300 | 301 | var wrapped = [ 302 | '(function(' + args.join(',') + ') {', 303 | ' var template = Handlebars.VM.template(' + template + ', Handlebars);', 304 | ' var partials = ' + partialMapString + ';' + 305 | ' return function(context, opt_options) {', 306 | ' var options = opt_options || {};', 307 | ' options.partials = partials;', 308 | ' return template(context, options);', 309 | ' }', 310 | '})'].join('\n'); 311 | return wrapped; 312 | }; 313 | 314 | 315 | function JSONResource() { 316 | ScriptResource.apply(this, arguments); 317 | } 318 | util.inherits(JSONResource, ScriptResource); 319 | 320 | JSONResource.prototype.buildFunctionString_ = function() { 321 | return '(function(){ return ' + this.source + '; })'; 322 | }; 323 | 324 | JSONResource.prototype.includeInEnvironment = function(targetEnvironment) { 325 | if (typeof this.environment_ === 'undefined') { 326 | this.environment_ = JSON.parse(this.source)['__environment__'] || null; 327 | } 328 | 329 | if (this.environment_ && this.environment_ != targetEnvironment) { 330 | return false; 331 | } 332 | return true; 333 | }; 334 | 335 | 336 | module.exports.ScriptResource = ScriptResource; 337 | module.exports.JavaScriptResource = JavaScriptResource; 338 | module.exports.HandlebarsResource = HandlebarsResource; 339 | module.exports.JSONResource = JSONResource; 340 | 341 | 342 | var RESOURCE_WRAPPERS = {}; 343 | 344 | 345 | function setResourceWrapper(extension, fn) { 346 | RESOURCE_WRAPPERS[extension] = fn; 347 | } 348 | module.exports.setResourceWrapper = setResourceWrapper; 349 | 350 | 351 | function getResourceWrapper(extension) { 352 | return RESOURCE_WRAPPERS[extension] || null; 353 | } 354 | module.exports.getResourceWrapper = getResourceWrapper; 355 | 356 | 357 | setResourceWrapper('.js', JavaScriptResource); 358 | setResourceWrapper('.html', HandlebarsResource); 359 | setResourceWrapper('.json', JSONResource); 360 | 361 | -------------------------------------------------------------------------------- /lib/loader/styleresource.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @license 5 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 6 | * License: MIT 7 | */ 8 | 9 | 10 | function StyleResource(filePath, resourcePath, source) { 11 | this.filePath = filePath; 12 | this.resourcePath = resourcePath; 13 | this.source = source; 14 | } 15 | module.exports.StyleResource = StyleResource; 16 | 17 | StyleResource.prototype.getCssString = function() { 18 | return this.source; 19 | }; 20 | 21 | StyleResource.prototype.bodyContentType = function() { 22 | return { 23 | contentType: 'text/css', 24 | body: this.getCssString() 25 | }; 26 | }; 27 | 28 | 29 | var RESOURCE_WRAPPERS = {}; 30 | 31 | 32 | function setResourceWrapper(extension, fn) { 33 | RESOURCE_WRAPPERS[extension] = fn; 34 | } 35 | module.exports.setResourceWrapper = setResourceWrapper; 36 | 37 | 38 | function getResourceWrapper(extension) { 39 | return RESOURCE_WRAPPERS[extension] || null; 40 | } 41 | module.exports.getResourceWrapper = getResourceWrapper; 42 | 43 | 44 | setResourceWrapper('.css', StyleResource); 45 | -------------------------------------------------------------------------------- /lib/optimizer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @license 5 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 6 | * License: MIT 7 | */ 8 | 9 | var crypto = require('crypto'); 10 | var fs = require('fs'); 11 | var path = require('path'); 12 | 13 | var uglify = require('uglify-js'); 14 | 15 | var SkitModule = require('./loader/SkitModule'); 16 | var loader = require('./loader/loader'); 17 | var scriptresource = require('./loader/scriptresource'); 18 | var skitutil = require('./skitutil'); 19 | 20 | 21 | var TargetEnvironment = scriptresource.TargetEnvironment; 22 | var HASH_FUNCTION = 'sha256'; 23 | 24 | 25 | // 26 | // OPTIMIZER 27 | // 28 | 29 | 30 | function VersionedFile(opt_name) { 31 | this.filename_ = opt_name || null; 32 | this.resourcePaths_ = []; 33 | } 34 | VersionedFile.prototype.addResourcePath = function(path) { 35 | this.resourcePaths_.push(path); 36 | }; 37 | VersionedFile.prototype.getResourcePaths = function() { 38 | return this.resourcePaths_.slice(); 39 | }; 40 | VersionedFile.prototype.filename_ = null; 41 | VersionedFile.prototype.setFilename = function(filename) { 42 | if (this.filename_) { 43 | throw new Error('filename already set'); 44 | } 45 | this.filename_ = filename; 46 | }; 47 | VersionedFile.prototype.getFilename = function() { return this.filename_; }; 48 | VersionedFile.prototype.bundle = null; 49 | VersionedFile.prototype.content_ = null; 50 | VersionedFile.prototype.setContent = function(content) { 51 | if (this.hash_) { 52 | throw new Error('hash already computed') 53 | } 54 | this.content_ = content; 55 | }; 56 | VersionedFile.prototype.getContent = function() { return this.content_; }; 57 | VersionedFile.prototype.hash_ = null; 58 | VersionedFile.prototype.getHash = function() { 59 | return this.hash_; 60 | }; 61 | VersionedFile.prototype.computeHash = function() { 62 | var hasher = crypto.createHash(HASH_FUNCTION).update( 63 | this.content_.replace ? new Buffer(this.content_, 'utf8') : this.content_); 64 | this.hash_ = hasher.digest('base64'); 65 | }; 66 | VersionedFile.prototype.getIntegrity = function() { 67 | if (!this.hash_) { 68 | throw new Error('Hash not computed yet'); 69 | } 70 | 71 | return HASH_FUNCTION + '-' + this.hash_; 72 | }; 73 | VersionedFile.prototype.getVersionedFilename = function(staticPrefix, publicStaticRoot) { 74 | if (!this.hash_) { 75 | throw new Error('Hash not computed yet'); 76 | } 77 | 78 | var parts = this.filename_.split('.'); 79 | parts[0] += '-v' + this.hash_.replace(/\+/g, '-').replace(/\//g, '_').substring(0, 24); 80 | var filename = parts.join('.'); 81 | 82 | if (publicStaticRoot) { 83 | return filename.replace(staticPrefix, publicStaticRoot); 84 | } 85 | return filename; 86 | }; 87 | 88 | 89 | function optimizeServer(server, optimizedPackagePath, opt_options) { 90 | optimizedPackagePath = path.resolve(optimizedPackagePath); 91 | 92 | var options = opt_options || {}; 93 | 94 | var aliasMapFilename = options.aliasMap; 95 | 96 | var staticPrefix = '/' + server.staticPrefix; 97 | var publicStaticRoot = options.staticRoot || ''; 98 | 99 | var addFile; 100 | var forEachFile; 101 | (function createFilesCollection() { 102 | var allFiles_ = {}; 103 | 104 | addFile = function(file) { 105 | if (!file.getFilename()) { 106 | throw new Error('filename not set yet') 107 | } 108 | allFiles_[file.getFilename()] = file; 109 | }; 110 | 111 | forEachFile = function(fn) { 112 | for (var filename in allFiles_) { 113 | fn(allFiles_[filename]); 114 | } 115 | }; 116 | })(); 117 | 118 | server.loader.allBundles().forEach(function(bundle) { 119 | var bundleScripts = []; 120 | var bundleStylesheets = []; 121 | 122 | console.log('Building bundle "' + bundle.name + '"...'); 123 | 124 | var cssFile = new VersionedFile(path.join(staticPrefix, bundle.name + '.css')); 125 | bundle.allStyles().forEach(function(css) { 126 | var body = css.bodyContentType().body; 127 | bundleStylesheets.push(body); 128 | 129 | cssFile.addResourcePath(css.resourcePath); 130 | }); 131 | cssFile.bundle = bundle; 132 | cssFile.setContent(bundleStylesheets.join('\n')); 133 | addFile(cssFile); 134 | 135 | var jsFile = new VersionedFile(path.join(staticPrefix, bundle.name + '.js')); 136 | bundle.allScripts().forEach(function(script) { 137 | if (!script.includeInEnvironment(TargetEnvironment.BROWSER)) { 138 | return; 139 | } 140 | 141 | var body = script.bodyContentType().body; 142 | bundleScripts.push(body); 143 | jsFile.addResourcePath(script.resourcePath); 144 | }); 145 | jsFile.bundle = bundle; 146 | jsFile.setContent(bundleScripts.join('\n')); 147 | addFile(jsFile); 148 | }); 149 | 150 | // VERSION, UPDATE AND COPY ALL STATIC FILES 151 | 152 | console.log('Loading raw files that might need updated references...'); 153 | 154 | var resolvedPackagePath = server.packagePath; 155 | if (resolvedPackagePath.charAt(resolvedPackagePath.length - 1) == '/') { 156 | resolvedPackagePath = resolvedPackagePath.substring(0, resolvedPackagePath.length - 1); 157 | } 158 | 159 | loader.walkSync(resolvedPackagePath).forEach(function(filename) { 160 | var basename = path.basename(filename); 161 | if (basename.indexOf('.') == 0) { 162 | return; 163 | } 164 | 165 | var relativeFilename = filename.replace(server.packagePath, ''); 166 | var file = new VersionedFile(relativeFilename); 167 | 168 | var content = fs.readFileSync(filename); 169 | var stringContent = content + ''; 170 | if (stringContent.indexOf('\ufffd') == -1) { 171 | content = stringContent; 172 | } 173 | file.setContent(content); 174 | 175 | addFile(file); 176 | }); 177 | 178 | // BUILD UNBUNDLED RESOURCE FILES AS IF THEY WERE REAL FILES, TOO 179 | 180 | console.log('Loading any unbundled public resources to static root...'); 181 | 182 | (function loadUnbundledModuleFiles() { 183 | // organize these by resource path and cache. 184 | var filesByResourcePath = {}; 185 | forEachFile(function(file) { 186 | file.getResourcePaths().forEach(function(resourcePath) { 187 | filesByResourcePath[resourcePath] = file; 188 | }); 189 | }); 190 | 191 | // load ALL module files in the whole enchilada. 192 | var allPublicModules = server.loader.getPublicRoot().descendants(); 193 | allPublicModules.forEach(function(module) { 194 | if (!(module instanceof SkitModule)) { 195 | // just a parent dir. 196 | return; 197 | } 198 | 199 | module.buildResourceList().forEach(function(res) { 200 | if (filesByResourcePath[res.resourcePath]) { 201 | // already in a bundle. 202 | return; 203 | } 204 | 205 | if (res.includeInEnvironment && !res.includeInEnvironment(TargetEnvironment.BROWSER)) { 206 | return; 207 | } 208 | 209 | var bodyContentType = res.bodyContentType(); 210 | var extension = '.js'; 211 | if (bodyContentType.contentType.indexOf('/css') > 0) { 212 | extension = '.css'; 213 | } 214 | var relativeFilename = res.resourcePath.replace(/[:\.]/g, '_') + extension; 215 | var staticFilename = path.join(staticPrefix, '__resource__', relativeFilename); 216 | 217 | var file = new VersionedFile(staticFilename); 218 | file.addResourcePath(res.resourcePath); 219 | file.setContent(bodyContentType.body); 220 | addFile(file); 221 | }); 222 | }); 223 | })(); 224 | 225 | 226 | // Minify these before doing the versioning in order to minimize the number 227 | // of changes that actually generate new versions of these files. 228 | // ie. comments / whitespace / local variable names should not create new 229 | // versions as long as we minify first. 230 | forEachFile(function(file) { 231 | var filename = file.getFilename(); 232 | 233 | if (/\.js$/.test(filename) && filename.indexOf(staticPrefix) == 0) { 234 | console.log('Minifying:', filename, '...'); 235 | 236 | var uglifyOptions = { 237 | fromString: true, 238 | output: { 239 | comments: /@preserve|@cc_on|\blicense\b/i, 240 | }, 241 | }; 242 | 243 | // Allow bundle to specify uglify options. 244 | var bundle = file.bundle; 245 | if (bundle && bundle.options) { 246 | if ('minify' in bundle.options && !bundle.options.minify) { 247 | // continue; 248 | return; 249 | } 250 | 251 | if (bundle.options.uglifyOptions) { 252 | for (var k in bundle.options.uglifyOptions) { 253 | uglifyOptions[k] = bundle.options.uglifyOptions[k]; 254 | } 255 | } 256 | } 257 | 258 | var minified = uglify.minify(file.getContent(), uglifyOptions).code; 259 | file.setContent(minified); 260 | } 261 | }); 262 | 263 | (function doRecursiveVersioning() { 264 | console.log('Versioning static files...'); 265 | 266 | var fileByName = {}; 267 | var staticBasenames = {}; 268 | forEachFile(function(file) { 269 | var filename = file.getFilename(); 270 | fileByName[filename] = file; 271 | if (filename.indexOf(staticPrefix) == 0) { 272 | staticBasenames[path.basename(filename)] = 1; 273 | } 274 | }); 275 | var escapedBasenames = Object.keys(staticBasenames).map(function(basename) { 276 | return skitutil.escapeRegex(basename); 277 | }); 278 | var buildFilenamesRegex = new RegExp( 279 | "(['\"(]|,\\s+)(/?(?:[\\w.-]+/)*(?:" + escapedBasenames.join('|') + "))((\\\\?['\")])|(\\s+\\dx))", 'g'); 280 | 281 | function updateFileAndReferences(file) { 282 | if (!file.getHash()) { 283 | if (file.visiting) { 284 | throw new Error('Cyclical dependency! ' + file.getFilename()); 285 | } 286 | file.visiting = true; 287 | 288 | var content = file.getContent(); 289 | if (content.replace) { 290 | content = content.replace(buildFilenamesRegex, function(_, quote1, filenameMatch, quote2) { 291 | if (filenameMatch.indexOf('/') != 0 && filenameMatch.indexOf('://') == -1) { 292 | filenameMatch = path.join(path.dirname(filename), filenameMatch); 293 | } 294 | 295 | var referencedFile = fileByName[filenameMatch]; 296 | if (referencedFile) { 297 | filenameMatch = updateFileAndReferences(referencedFile); 298 | } 299 | return quote1 + filenameMatch + quote2; 300 | }); 301 | 302 | // this will fail if the node has already been visited. 303 | file.setContent(content); 304 | } 305 | 306 | // After all the replacements are done, actually update the file. 307 | file.computeHash(); 308 | delete file.visiting; 309 | } 310 | 311 | return file.getVersionedFilename(staticPrefix, publicStaticRoot); 312 | } 313 | 314 | forEachFile(updateFileAndReferences); 315 | })(); 316 | 317 | 318 | (function buildAliasMap() { 319 | var moduleToStaticAliasMap = {}; 320 | forEachFile(function(file) { 321 | var paths = file.getResourcePaths(); 322 | paths.forEach(function(resourcePath) { 323 | moduleToStaticAliasMap[resourcePath] = { 324 | path: file.getVersionedFilename(staticPrefix, publicStaticRoot), 325 | integrity: file.getIntegrity(), 326 | }; 327 | }); 328 | }); 329 | 330 | var aliasMap = new VersionedFile(aliasMapFilename); 331 | aliasMap.setContent(JSON.stringify(moduleToStaticAliasMap, null, ' ')); 332 | aliasMap.computeHash(); 333 | addFile(aliasMap); 334 | })(); 335 | 336 | // WRITE ALL OPTIMIZED FILES TO DISK 337 | 338 | (function writeAllFiles() { 339 | console.log('Writing optimized files to disk...'); 340 | 341 | loader.mkdirPSync(optimizedPackagePath); 342 | 343 | forEachFile(function(file) { 344 | var body = file.getContent(); 345 | 346 | var filename = file.getFilename(); 347 | var outfiles = [filename]; 348 | if (filename.indexOf(staticPrefix) === 0) { 349 | // NOTE: Leave staticPrefix here because these are local filenames. 350 | outfiles.push(file.getVersionedFilename()); 351 | } 352 | 353 | outfiles.forEach(function(destinationFilename) { 354 | var absoluteFilename = path.join(optimizedPackagePath, destinationFilename); 355 | loader.mkdirPSync(path.dirname(absoluteFilename)); 356 | 357 | fs.writeFileSync(absoluteFilename, body); 358 | }); 359 | }); 360 | })(); 361 | 362 | console.log('All done!'); 363 | } 364 | 365 | 366 | module.exports = { 367 | optimizeServer: optimizeServer 368 | }; -------------------------------------------------------------------------------- /lib/skit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @license 5 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 6 | * License: MIT 7 | */ 8 | 9 | 10 | var SkitServer = require('./SkitServer'); 11 | var optimizer = require('./optimizer'); 12 | var scriptresource = require('./loader/scriptresource'); 13 | var styleresource = require('./loader/styleresource'); 14 | 15 | module.exports = { 16 | 'SkitServer': SkitServer, 17 | 'optimizeServer': optimizer.optimizeServer, 18 | 19 | 'styleresource': styleresource, 20 | 'scriptresource': scriptresource, 21 | }; 22 | -------------------------------------------------------------------------------- /lib/skit/browser/ElementWrapper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'browser-only'; 3 | 4 | /** 5 | * @license 6 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 7 | * License: MIT 8 | */ 9 | 10 | /** @ignore */ 11 | var iter = skit.platform.iter; 12 | /** @ignore */ 13 | var string = skit.platform.string; 14 | /** @ignore */ 15 | var sizzle = skit.thirdparty.sizzle; 16 | 17 | 18 | /** @ignore */ 19 | var ELEMENT_NODE_TYPE = 1; 20 | 21 | 22 | /** 23 | * A DOM manipulation helper that wraps a single DOM element. 24 | * 25 | * @param {Element} element The element to perform operations on. 26 | * @constructor 27 | */ 28 | var ElementWrapper = function(element) { 29 | this.element = element; 30 | }; 31 | 32 | 33 | /** 34 | * For a given HTML string, generate DOM nodes and wrap them with 35 | * ElementWrappers, returning the result array. 36 | * 37 | * @param {string} html The HTML to parse into an ElementWrapper. 38 | * @return {Array} The wrapped DOM elements from the given HTML. 39 | */ 40 | ElementWrapper.elementsFromHtml = function(html) { 41 | var div = document.createElement('div'); 42 | div.innerHTML = html; 43 | return iter.map((new ElementWrapper(div)).children(), function($child) { 44 | $child.remove(); 45 | return $child; 46 | }); 47 | }; 48 | 49 | 50 | /** 51 | * For a given HTML string, generate a single DOM node and wrap it with 52 | * an ElementWrapper, returning the result. 53 | * 54 | * @param {string} html The HTML to parse into an ElementWrapper. 55 | * @return {Array} The wrapped DOM element from the given HTML. 56 | */ 57 | ElementWrapper.fromHtml = function(html) { 58 | return ElementWrapper.elementsFromHtml(html)[0]; 59 | }; 60 | 61 | 62 | /** 63 | * @return {Array} The {ElementWrapper}-wrapped children of this element. 64 | */ 65 | ElementWrapper.prototype.children = function() { 66 | var filtered; 67 | if (this.element.children) { 68 | filtered = iter.toArray(this.element.children); 69 | } else { 70 | filtered = iter.filter(this.element.childNodes, function(node) { 71 | return node.nodeType == ELEMENT_NODE_TYPE; 72 | }); 73 | } 74 | 75 | return iter.map(filtered, function(element) { 76 | return new ElementWrapper(element); 77 | }); 78 | }; 79 | 80 | 81 | /** 82 | * @param {Element|ElementWrapper} otherEl The other element, which is possibly 83 | * a child of this element. 84 | * @return {boolean} Whether {otherEl} is a child of this element. 85 | */ 86 | ElementWrapper.prototype.contains = function(otherEl) { 87 | var current = otherEl.element || otherEl; 88 | while (current) { 89 | if (current == this.element) { 90 | return true; 91 | } 92 | current = current.parentNode; 93 | } 94 | return false; 95 | }; 96 | 97 | 98 | /** 99 | * @return {ElementWrapper} The {ElementWrapper}-wrapped parent of this element. 100 | */ 101 | ElementWrapper.prototype.parent = function() { 102 | if (this.element.parentNode && this.element.parentNode.nodeType == ELEMENT_NODE_TYPE) { 103 | return new ElementWrapper(this.element.parentNode); 104 | } 105 | return null; 106 | }; 107 | 108 | 109 | /** 110 | * @param {string} selector A CSS selector. 111 | * @return {boolean} Whether this element matches the given selector. 112 | */ 113 | ElementWrapper.prototype.matches = function(selector) { 114 | return sizzle.matchesSelector(this.element, selector); 115 | }; 116 | 117 | 118 | /** 119 | * @param {string} selector A CSS selector. 120 | * @return {Array} An array of ElementWrapper-wrapped descendants of this 121 | * element that match the provided selector. 122 | */ 123 | ElementWrapper.prototype.find = function(selector) { 124 | return iter.map(sizzle(selector, this.element), function(el) { 125 | return new ElementWrapper(el); 126 | }); 127 | }; 128 | 129 | 130 | /** 131 | * @param {string} selector A CSS selector. 132 | * @return {ElementWrapper} The first ElementWrapper-wrapped descendant of this 133 | * element that matches the provided selector. 134 | */ 135 | ElementWrapper.prototype.get = function(selector) { 136 | var found = this.find(selector); 137 | return found.length ? found[0] : null; 138 | }; 139 | 140 | 141 | /** 142 | * @return {ElementWrapper?} The first ElementWrapper-wrapped child of this 143 | * element, or null if it has none. 144 | */ 145 | ElementWrapper.prototype.first = function() { 146 | var children = this.children(); 147 | return children.length ? children[0] : null; 148 | }; 149 | 150 | 151 | /** 152 | * @param {string} selector A CSS selector. 153 | * @return {ElementWrapper?} The first ElementWrapper-wrapped ancestor of this 154 | * element that matches a given selector. 155 | */ 156 | ElementWrapper.prototype.up = function(selector) { 157 | var current = this; 158 | while (current && !current.matches(selector)) { 159 | current = current.parent(); 160 | } 161 | return current; 162 | }; 163 | 164 | 165 | /** 166 | * Removes this element from its parent. 167 | */ 168 | ElementWrapper.prototype.remove = function() { 169 | if (this.element.parentNode) { 170 | this.element.parentNode.removeChild(this.element); 171 | } 172 | }; 173 | 174 | 175 | /** 176 | * Replace the current node with the given node or HTML fragment. 177 | * 178 | * @param {string|Element|ElementWrapper} htmlOrElement The fragment to replace 179 | * the current element with. 180 | */ 181 | ElementWrapper.prototype.replaceWith = function(htmlOrElement) { 182 | var replacement; 183 | if (typeof htmlOrElement == 'string') { 184 | replacement = ElementWrapper.fromHtml(htmlOrElement).element; 185 | } else { 186 | replacement = htmlOrElement.element || htmlOrElement; 187 | } 188 | 189 | if (this.element.parentNode) { 190 | this.element.parentNode.insertBefore(replacement, this.element); 191 | this.element.parentNode.removeChild(this.element); 192 | } 193 | this.element = replacement; 194 | }; 195 | 196 | 197 | /** 198 | * Removes the element's children. 199 | */ 200 | ElementWrapper.prototype.removeChildren = function() { 201 | for (var i = this.element.childNodes.length - 1; i >= 0; i--) { 202 | this.element.removeChild(this.element.childNodes[i]); 203 | } 204 | }; 205 | 206 | 207 | /** 208 | * Replace the current element's children with the given fragment. 209 | * 210 | * @param {string|Element|ElementWrapper} htmlOrElement The fragment to replace 211 | * the current element's children with. 212 | * @param {boolean=} opt_withChildren If true, use the replacement element's 213 | * children as replacements for my children, not the replacement element 214 | * itself. 215 | */ 216 | ElementWrapper.prototype.replaceChildren = function(htmlOrElement, opt_withChildren) { 217 | var replacement; 218 | if (typeof htmlOrElement == 'string') { 219 | replacement = ElementWrapper.fromHtml(htmlOrElement).element; 220 | } else { 221 | replacement = htmlOrElement.element || htmlOrElement; 222 | } 223 | 224 | this.removeChildren(); 225 | 226 | if (!opt_withChildren) { 227 | this.element.appendChild(replacement); 228 | } else { 229 | for (var i = 0, len = replacement.childNodes.length; i < len; i++) { 230 | var child = replacement.childNodes[0]; 231 | replacement.removeChild(child); 232 | this.element.appendChild(child); 233 | } 234 | } 235 | }; 236 | 237 | 238 | /** 239 | * @return {Array} An array of string class names belonging to this element. 240 | */ 241 | ElementWrapper.prototype.classes = function() { 242 | return string.trim(this.element.className).split(/\s+/); 243 | }; 244 | 245 | 246 | /** 247 | * @param {string} className A class name this element might have. 248 | * @return {boolean} Whether the current element has the given class name. 249 | */ 250 | ElementWrapper.prototype.hasClass = function(className) { 251 | var classes = this.classes(); 252 | for (var i = 0; i < classes.length; i++) { 253 | if (classes[i] == className) { 254 | return true; 255 | } 256 | } 257 | return false; 258 | }; 259 | 260 | 261 | /** 262 | * Add a class to an element, unless it already has the class name. 263 | * @param {string} className A class name this element might already have. 264 | */ 265 | ElementWrapper.prototype.addClass = function(className) { 266 | if (!this.hasClass(className)) { 267 | this.element.className = string.trim(this.element.className + ' ' + className); 268 | } 269 | }; 270 | 271 | 272 | /** 273 | * Remove a class from an element, unless it does not have the class. 274 | * @param {string} className A class name this element might have. 275 | */ 276 | ElementWrapper.prototype.removeClass = function(classToRemove) { 277 | var classes = iter.filter(this.classes(), function(className) { 278 | return className != classToRemove; 279 | }); 280 | 281 | this.element.className = classes.join(' '); 282 | }; 283 | 284 | 285 | /** 286 | * Add a class if the element doesn't have it yet; remove it if it does have 287 | * it already. 288 | * 289 | * @param {string} className A class name this element might have. 290 | */ 291 | ElementWrapper.prototype.toggleClass = function(className) { 292 | if (this.hasClass(className)) { 293 | this.removeClass(className); 294 | } else { 295 | this.addClass(className); 296 | } 297 | }; 298 | 299 | 300 | /** 301 | * Retrieve an item from the element's dataset. 302 | * 303 | * @param {string} key The "attribute-style" key name, which should be 304 | * lowercase and hyphenated. For an attribute named data-foo-bar="baz", 305 | * this would be "foo-bar". 306 | * @return {string?} The dataset element if it is set. 307 | */ 308 | ElementWrapper.prototype.getData = function(key) { 309 | if (this.element.dataset) { 310 | return this.element.dataset[string.camelCase(key)]; 311 | } 312 | return this.element.getAttribute('data-' + key); 313 | }; 314 | 315 | 316 | /** 317 | * Set an item into the element's dataset. 318 | * 319 | * @param {string} key The "attribute-style" key name, which should be 320 | * lowercase and hyphenated. For an attribute eg. data-foo-bar="baz", 321 | * this would be "foo-bar". 322 | * @param {string} value The string value to set into the dataset. If this 323 | * element is not a string, it will be returned as one from getData(). 324 | */ 325 | ElementWrapper.prototype.setData = function(key, value) { 326 | if (this.element.dataset) { 327 | this.element.dataset[string.camelCase(key)] = value; 328 | } else { 329 | this.element.setAttribute('data-' + key, value); 330 | } 331 | }; 332 | 333 | 334 | /** 335 | * @return {string} The text content of this node. 336 | */ 337 | ElementWrapper.prototype.getText = function() { 338 | if (typeof this.element.textContent !== 'undefined') { 339 | return this.element.textContent; 340 | } else if (typeof this.element.innerText !== 'undefined') { 341 | return this.element.innerText; 342 | } else { 343 | return this.element.innerHTML; 344 | } 345 | }; 346 | 347 | 348 | /** 349 | * Set text content of this node. 350 | * 351 | * @param {string} value The text to set as the text content of this node. 352 | */ 353 | ElementWrapper.prototype.setText = function(value) { 354 | this.element.innerHTML = string.escapeHtml(value); 355 | }; 356 | 357 | 358 | /** 359 | * @return {Object} A key-value list of elements in the form. 360 | */ 361 | ElementWrapper.prototype.serializeForm = function() { 362 | var values = {}; 363 | var elements = this.find('select, input, button, textarea'); 364 | iter.forEach(elements, function($el) { 365 | var name = $el.element.name; 366 | if (!name) { 367 | return; 368 | } 369 | 370 | var v = $el.value(); 371 | if (v !== null) { 372 | values[name] = v; 373 | } 374 | }); 375 | return values; 376 | }; 377 | 378 | 379 | /** 380 | * Set a value inside this
    element according to the form input's name. 381 | * @param {string} name The "name" attribute of the form element. 382 | * @param {string} value The value of the form element. 383 | */ 384 | ElementWrapper.prototype.setFormValue = function(name, value) { 385 | var els = this.find('[name=' + name + ']'); 386 | iter.forEach(els, function($el) { 387 | if ($el.isCheckable()) { 388 | // NOTE: Do not use .value() here in case the element is disabled 389 | // or unchecked. 390 | $el.element.checked = ($el.element.value == value); 391 | } else { 392 | $el.setValue(value); 393 | } 394 | }); 395 | }; 396 | 397 | 398 | /** 399 | * @return {boolean} Whether this element is a radio or checkbox element that 400 | * can be "checked". 401 | */ 402 | ElementWrapper.prototype.isCheckable = function() { 403 | var type = (this.element.type || '').toLowerCase(); 404 | return (type == 'radio' || type == 'checkbox'); 405 | }; 406 | 407 | 408 | /** 409 | * @return {boolean} Whether this input is checked. 410 | */ 411 | ElementWrapper.prototype.getChecked = function() { 412 | return !this.element.disabled && !!this.element.checked; 413 | }; 414 | 415 | 416 | /** 417 | * Set checkedness for a checkable element. 418 | * @param {boolean} Whether this input is checked. 419 | */ 420 | ElementWrapper.prototype.setChecked = function(checked) { 421 | return this.element.checked = checked; 422 | }; 423 | 424 | 425 | /** 426 | * @return {?string} The value of the input, select, button or textarea. 427 | * Returns null if the element is disabled or unchecked. 428 | */ 429 | ElementWrapper.prototype.getValue = function() { 430 | if (this.element.disabled) { 431 | return null; 432 | } 433 | 434 | if (this.isCheckable() && !this.getChecked()) { 435 | return null; 436 | } 437 | 438 | if (typeof this.element.selectedIndex == 'number') { 439 | var option = this.element.options[this.element.selectedIndex]; 440 | return (option || {}).value; 441 | } 442 | return this.element.value; 443 | }; 444 | ElementWrapper.prototype.value = ElementWrapper.prototype.getValue; 445 | 446 | 447 | /** 448 | * @return {string} The value of the input or textarea. 449 | */ 450 | ElementWrapper.prototype.setValue = function(value) { 451 | var el = this.element; 452 | if (typeof el.selectedIndex == 'number') { 453 | iter.forEach(el.options, function(opt, i, stop) { 454 | if (opt.value == value) { 455 | el.selectedIndex = i; 456 | stop(); 457 | } 458 | }); 459 | } else { 460 | el.value = value; 461 | } 462 | }; 463 | 464 | 465 | /** 466 | * Set this element as disabled. 467 | */ 468 | ElementWrapper.prototype.disable = function() { 469 | this.element.disabled = true; 470 | }; 471 | 472 | 473 | /** 474 | * Set this element as not disabled. 475 | */ 476 | ElementWrapper.prototype.enable = function() { 477 | this.element.disabled = false; 478 | }; 479 | 480 | 481 | /** 482 | * Append this element to another element. 483 | * 484 | * @param {Element|ElementWrapper} toElement My new parent element. 485 | */ 486 | ElementWrapper.prototype.appendTo = function(toElement) { 487 | (toElement.element || toElement).appendChild(this.element); 488 | }; 489 | 490 | 491 | /** 492 | * Append an element or HTML string to the current element, maintaining 493 | * the existing DOM structure. 494 | * 495 | * @param {Element|ElementWrapper|string} htmlOrElement The HTML fragment 496 | * or DOM node to append. 497 | */ 498 | ElementWrapper.prototype.append = function(htmlOrElement) { 499 | var elements; 500 | if (typeof htmlOrElement == 'string') { 501 | elements = ElementWrapper.elementsFromHtml(htmlOrElement); 502 | } else if (htmlOrElement.length) { 503 | elements = htmlOrElement; 504 | } else { 505 | elements = [htmlOrElement]; 506 | } 507 | 508 | iter.forEach(elements, function(element) { 509 | this.element.appendChild(element.element || element); 510 | }, this); 511 | }; 512 | 513 | 514 | module.exports = ElementWrapper; 515 | -------------------------------------------------------------------------------- /lib/skit/browser/Event.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'browser-only'; 3 | 4 | /** 5 | * @license 6 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 7 | * License: MIT 8 | */ 9 | 10 | /** @ignore */ 11 | var ElementWrapper = skit.browser.ElementWrapper; 12 | 13 | 14 | /** 15 | * A wrapper for native browser events to fix browser inconsistencies. 16 | * 17 | * @param {Event} evt The native browser event. 18 | * @constructor 19 | */ 20 | var Event = function(evt) { 21 | this.originalEvent = evt; 22 | 23 | this.target = new ElementWrapper(evt.srcElement || evt.target); 24 | this.currentTarget = null; 25 | 26 | this.type = evt.type; 27 | 28 | this.keyCode = evt.keyCode || null; 29 | this.shiftKey = evt.shiftKey || false; 30 | this.altKey = evt.altKey || false; 31 | this.metaKey = evt.metaKey || false; 32 | this.ctrlKey = evt.ctrlKey || false; 33 | 34 | var posX = 0; 35 | var posY = 0; 36 | if (evt.pageX || evt.pageY) { 37 | posX = evt.pageX; 38 | posY = evt.pageY; 39 | } else if (evt.clientX || evt.clientY) { 40 | posX = evt.clientX + document.body.scrollLeft 41 | + document.documentElement.scrollLeft; 42 | posY = evt.clientY + document.body.scrollTop 43 | + document.documentElement.scrollTop; 44 | } 45 | 46 | this.pageX = posX; 47 | this.pageY = posY; 48 | }; 49 | 50 | 51 | /** 52 | * Prevent this element from continuing to bubble. 53 | */ 54 | Event.prototype.stopPropagation = function() { 55 | if (this.originalEvent.stopPropagation) { 56 | this.originalEvent.stopPropagation(); 57 | } else { 58 | this.originalEvent.cancelBubble = true; 59 | } 60 | }; 61 | 62 | 63 | /** 64 | * Prevent the default behavior of this event from continuing. 65 | */ 66 | Event.prototype.preventDefault = function() { 67 | if (this.originalEvent.preventDefault) { 68 | this.originalEvent.preventDefault(); 69 | } else { 70 | this.originalEvent.returnValue = false; 71 | } 72 | }; 73 | 74 | 75 | module.exports = Event; 76 | -------------------------------------------------------------------------------- /lib/skit/browser/dom.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'browser-only'; 3 | 4 | /** 5 | * Find and manipulate DOM nodes. 6 | * 7 | * @module 8 | * @license 9 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 10 | * License: MIT 11 | */ 12 | 13 | 14 | /** @ignore */ 15 | var ElementWrapper = skit.browser.ElementWrapper; 16 | /** @ignore */ 17 | var iter = skit.platform.iter; 18 | /** @ignore */ 19 | var sizzle = skit.thirdparty.sizzle; 20 | 21 | 22 | /** 23 | * @return {Array} An array of elements wrapped in ElementWrapper objects that 24 | * match a given DOM query selector. 25 | */ 26 | module.exports.find = function(selector) { 27 | return iter.map(sizzle(selector), function(el) { 28 | return new ElementWrapper(el); 29 | }); 30 | }; 31 | 32 | 33 | /** 34 | * @return {ElementWrapper?} The first element that matches a given query, 35 | * wrapped in an ElementWrapper object. 36 | */ 37 | module.exports.get = function(selector) { 38 | return module.exports.find(selector)[0]; 39 | }; 40 | 41 | -------------------------------------------------------------------------------- /lib/skit/browser/events.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'browser-only'; 3 | 4 | /** 5 | * Add and remove event listeners on DOM elements. 6 | * 7 | * @module 8 | * @license 9 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 10 | * License: MIT 11 | */ 12 | 13 | 14 | /** @ignore */ 15 | var Event = skit.browser.Event; 16 | /** @ignore */ 17 | var iter = skit.platform.iter; 18 | /** @ignore */ 19 | var ElementWrapper = skit.browser.ElementWrapper; 20 | 21 | 22 | /** @ignore */ 23 | var boundHandlerId = 0; 24 | /** @ignore */ 25 | var boundHandlers = {}; 26 | /** @ignore */ 27 | var globalListeners_ = []; 28 | 29 | 30 | /** 31 | * Listen for a named DOM event on a given DOM node or {ElementWrapper}. 32 | * 33 | * @param {Element|ElementWrapper} maybeWrappedElement The DOM element or 34 | * ElementWrapper to listen on. 35 | * @param {string} evtName The event name, eg. 'click'. 36 | * @param {Function} callback The callback function. 37 | * @param {Object=} opt_context The object that should be {this} inside 38 | * the callback. 39 | * @return {number} The listener ID, which can be passed to unbind(). 40 | */ 41 | module.exports.bind = function(maybeWrappedElement, evtName, callback, opt_context) { 42 | var element = maybeWrappedElement.element ? maybeWrappedElement.element : maybeWrappedElement; 43 | var wrappedCallback = function(evt) { 44 | var wrapped = new Event(evt); 45 | callback.call(opt_context, wrapped); 46 | }; 47 | var listenerId = ++boundHandlerId + ''; 48 | boundHandlers[listenerId] = { 49 | element: element, 50 | evtName: evtName, 51 | handler: wrappedCallback 52 | }; 53 | 54 | if (element.addEventListener) { 55 | element.addEventListener(evtName, wrappedCallback); 56 | } else { 57 | element.attachEvent('on' + evtName, wrappedCallback); 58 | } 59 | 60 | if (element === window || element === document || element == document.body || element === document.documentElement) { 61 | globalListeners_.push(listenerId); 62 | } 63 | 64 | return listenerId; 65 | }; 66 | 67 | 68 | /** 69 | * Unisten for an event given the listenerId returned by {bind}. Unattaches 70 | * the event listener from the original DOM element. 71 | * 72 | * @param {number} listenerId The listener ID returned by bind(). 73 | */ 74 | module.exports.unbind = function(listenerId) { 75 | var wrapper = boundHandlers[listenerId]; 76 | if (!wrapper) { 77 | return; 78 | } 79 | 80 | delete boundHandlers[listenerId]; 81 | 82 | if (wrapper.element.addEventListener) { 83 | wrapper.element.removeEventListener(wrapper.evtName, wrapper.handler); 84 | } else { 85 | wrapper.element.detachEvent(wrapper.evtName, wrapper.handler); 86 | } 87 | }; 88 | 89 | 90 | /** 91 | * Listen for an event on a matching child element from a parent element. 92 | * 93 | * @param {Element|ElementWrapper} maybeWrappedElement The DOM element or 94 | * ElementWrapper to listen on. 95 | * @param {string} selector The selector used to determine {originalTarget} 96 | * of the resulting {Event} object passed to {callback}. 97 | * @param {string} evtName The event name, eg. 'click'. 98 | * @param {Function} callback The callback function. 99 | * @param {Object=} opt_context The object that should be {this} inside 100 | * the callback. 101 | * @return {number} The listener ID, which can be passed to unbind(). 102 | */ 103 | module.exports.delegate = function(element, selector, evtName, callback, opt_context) { 104 | return module.exports.bind(element, evtName, function(evt) { 105 | var currentTarget = evt.target.up(selector); 106 | if (currentTarget) { 107 | evt.currentTarget = currentTarget; 108 | callback.apply(opt_context, arguments); 109 | } 110 | }); 111 | }; 112 | 113 | 114 | /** 115 | * Remove all listeners added to the global window/document/body. 116 | */ 117 | module.exports.removeGlobalListeners = function() { 118 | var listeners = globalListeners_; 119 | globalListeners_ = []; 120 | iter.forEach(listeners, function(listener) { 121 | module.exports.unbind(listener); 122 | }); 123 | }; 124 | -------------------------------------------------------------------------------- /lib/skit/browser/layout.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'browser-only'; 3 | 4 | /** 5 | * @module 6 | * @license 7 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 8 | * License: MIT 9 | */ 10 | 11 | 12 | /** @ignore */ 13 | function isGlobal(element) { 14 | return (element === window || element === document || element === document.body); 15 | } 16 | 17 | 18 | /** 19 | * @param {Element|ElementWrapper|Window|Document} A DOM element. 20 | * @return {number} The "scrollTop" value for the given element, ie. how far 21 | * the given element is scrolled. If window, document or body is passed, 22 | * use the browser-appropriate scrollTop measure. 23 | */ 24 | function scrollTop(element) { 25 | if (isGlobal(element)) { 26 | return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0; 27 | } 28 | 29 | return element.scrollTop; 30 | } 31 | module.exports.scrollTop = scrollTop; 32 | 33 | 34 | /** 35 | * @param {Element|ElementWrapper|Window|Document} A DOM element. 36 | * @return {number} The "scrollLeft" value for the given element, ie. how far 37 | * the given element is scrolled. If window, document or body is passed, 38 | * use the browser-appropriate scrollLeft measure. 39 | */ 40 | function scrollLeft(element) { 41 | if (isGlobal(element)) { 42 | return window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0; 43 | } 44 | 45 | return element.scrollLeft; 46 | } 47 | module.exports.scrollLeft = scrollLeft; 48 | 49 | 50 | /** 51 | * @param {Element|ElementWrapper|Window|Document} A DOM element. 52 | * @return {number} The "scrollHeight" value for the given element, ie. how 53 | * tall the element is if it had no scroll bar. If window, document or 54 | * body is passed, use the browser-appropriate scrollHeight measure. 55 | */ 56 | module.exports.scrollHeight = function(element) { 57 | if (isGlobal(element)) { 58 | if (typeof document.body.scrollHeight !== 'number') { 59 | return document.documentElement.scrollHeight; 60 | } 61 | return document.body.scrollHeight; 62 | } 63 | 64 | return element.scrollHeight; 65 | }; 66 | 67 | 68 | /** 69 | * @param {Element|ElementWrapper} A DOM element. 70 | * @return {Element} The offset parent for this element, which is the first 71 | * ancestor element that is not statically positioned. 72 | */ 73 | module.exports.offsetParent = function(element) { 74 | element = element.element || element; 75 | return element.offsetParent || element.ownerDocument.body; 76 | }; 77 | 78 | 79 | /** 80 | * @param {Element|ElementWrapper} A DOM element. 81 | * @return {{left: number, top: number, width: number, height: number}} The 82 | * current position, width and height of the given element. If the element 83 | * is hidden, returns all zeroes. 84 | */ 85 | function boundingRect(element) { 86 | element = element.element || element; 87 | 88 | var rect = null; 89 | if (element.getClientRects().length) { 90 | rect = element.getBoundingClientRect(); 91 | } 92 | 93 | if (!(rect && (rect.width || rect.height))) { 94 | return {top: 0, left: 0, width: 0, height: 0}; 95 | } 96 | 97 | var document = element.ownerDocument; 98 | var window = document.defaultView; 99 | var documentElement = document.documentElement; 100 | 101 | return { 102 | top: rect.top + window.pageYOffset - documentElement.clientTop, 103 | left: rect.left + window.pageXOffset - documentElement.clientLeft, 104 | height: rect.height, 105 | width: rect.width 106 | }; 107 | } 108 | module.exports.boundingRect = boundingRect; 109 | 110 | 111 | /** 112 | * @param {Element|ElementWrapper} A DOM element. 113 | * @return {{left: number, top: number}} The current position of the given element. 114 | */ 115 | module.exports.position = function(element) { 116 | var rect = boundingRect(element); 117 | return {top: rect.top, left: rect.left}; 118 | }; 119 | 120 | 121 | /** 122 | * @param {Element|ElementWrapper|Window|Document} A DOM element. 123 | * @return {number} The outer width of the given element, which includes 124 | * padding and borders. 125 | */ 126 | module.exports.width = function(element) { 127 | element = element.element || element; 128 | if (isGlobal(element)) { 129 | return document.documentElement.clientWidth || window.innerWidth; 130 | } 131 | return boundingRect(element).width; 132 | }; 133 | 134 | 135 | /** 136 | * @param {Element|ElementWrapper|Window|Document} A DOM element. 137 | * @return {number} The outer height of the given element, which includes 138 | * padding and borders. 139 | */ 140 | module.exports.height = function(element) { 141 | element = element.element || element; 142 | if (isGlobal(element)) { 143 | return document.documentElement.clientHeight || window.innerHeight; 144 | } 145 | return boundingRect(element).height; 146 | }; 147 | -------------------------------------------------------------------------------- /lib/skit/browser/reset.css: -------------------------------------------------------------------------------- 1 | 2 | body, h1, h2, h3, h4, h5, h6, p, ol, ul, form, blockquote { 3 | padding: 0; 4 | margin: 0; 5 | } 6 | 7 | h1, h2, h3, h4, h5, h6 { 8 | font-size: 1em; 9 | font-weight: normal; 10 | } 11 | 12 | a { 13 | text-decoration: none; 14 | cursor: pointer; 15 | } 16 | 17 | a img { 18 | border: none; 19 | } 20 | 21 | ol, 22 | ul, 23 | li { 24 | list-style-type: none; 25 | } 26 | 27 | * { 28 | outline: none; 29 | } 30 | -------------------------------------------------------------------------------- /lib/skit/platform/Controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @license 5 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 6 | * License: MIT 7 | */ 8 | 9 | 10 | /** @ignore */ 11 | var iter = skit.platform.iter; 12 | /** @ignore */ 13 | var util = skit.platform.util; 14 | /** @ignore */ 15 | var util = skit.platform.util; 16 | 17 | 18 | /** 19 | * Render a page for a skit URL path. 20 | * @class 21 | * @name Controller 22 | */ 23 | var Controller = util.createClass(/** @lends Controller# */{ 24 | /** 25 | * Constructor function should not be called directly and should not be 26 | * overridden. 27 | */ 28 | __init__: function(params) { 29 | this.params = params; 30 | }, 31 | 32 | 33 | /** 34 | * Loads any necessary data from the backend API and handles any navigation 35 | * that should occur before render time. Only called once, typically on the 36 | * server side. 37 | * 38 | * At this point, there is no window or document available, so this method 39 | * should only call into a backend API, or read data supplied by other 40 | * skit modules. window/document access should wait until the call to 41 | * __ready__(). 42 | * 43 | * Properties assigned to {this} during this stage, eg. this.data = foo, 44 | * will be serialized to JSON and re-assigned to this controller object on 45 | * the client side. 46 | * 47 | * @param onLoaded {Function} A callback to call when you are done preloading 48 | * content from the backend. 49 | */ 50 | __preload__: function(onLoaded) { 51 | onLoaded(); 52 | }, 53 | 54 | 55 | /** 56 | * Called on the server and client after preload has completed. You can use 57 | * this method like a constructor to set up any state for the rest of the object to use. 58 | * 59 | * IMPORTANT: You will not have access to window or document at this point, 60 | * since this might be running on the server. Any window/document access 61 | * should start in __ready__(). 62 | */ 63 | __load__: function() {}, 64 | 65 | 66 | /** 67 | * Returns the title for this page. If this is a parent constructor, you will 68 | * also get the title provided by the child controller as an argument. 69 | * 70 | * IMPORTANT: You will not have access to window or document at this point, 71 | * since this might be running on the server. Any window/document access 72 | * should start in __ready__(). 73 | * 74 | * @param {string=} childTitle The title generated by a child controller, if 75 | * this is a parent controller and the child controller generated a 76 | * title. 77 | * @return {string} The title of this page. 78 | */ 79 | __title__: function(childTitle) { 80 | return childTitle; 81 | }, 82 | 83 | 84 | /** 85 | * Returns additional content to put in the head of this page, which would 86 | * typically be things like meta tags (hence the name). If this is a parent 87 | * controller, the argument will be the string generated by the child 88 | * controller, which you should include in your response. 89 | * 90 | * IMPORTANT: You will not have access to window or document at this point, 91 | * since this might be running on the server. Any window/document access 92 | * should start in __ready__(). 93 | * 94 | * @param {string=} childMeta The HTML generated by the child controller, if 95 | * this is a parent controller and the child generated any HTML. 96 | * @return {string} The HTML content of the head tag for this page. 97 | */ 98 | __meta__: function(childMeta) { 99 | return childMeta; 100 | }, 101 | 102 | 103 | /** 104 | * Returns the HTML representing this page. If this is a parent controller, 105 | * the argument will be the result of calling __body__ on the child 106 | * controller, which you can integrate into the response as appropriate. 107 | * 108 | * IMPORTANT: You will not have access to window or document at this point, 109 | * since this might be running on the server. Any window/document access 110 | * should start in __ready__(). 111 | * 112 | * @param {string=} childBody The HTML generated by the child controller, if 113 | * this is a parent controller and the child generated any HTML. 114 | * @return {string} The HTML content of the body tag for this page. 115 | */ 116 | __body__: function(childBody) { 117 | return childBody; 118 | }, 119 | 120 | 121 | /** 122 | * Called when we become alive in the browser. This method should wire up 123 | * the DOM that was rendered in __body__, handle any onload state, etc. 124 | * This is the point where you have a valid window and document. 125 | */ 126 | __ready__: function() {}, 127 | 128 | 129 | /** 130 | * Cleans up any event listeners, etc. that should be cleaned up before 131 | * we navigate away from this page (or rerender it). 132 | */ 133 | __unload__: function() {}, 134 | 135 | 136 | /** 137 | * Transform the body from whatever it is (probably HTML) to HTML. This can 138 | * be used if {__body__} returns something other than HTML, eg. a React 139 | * component, to transform the object into raw HTML the server can return. 140 | * 141 | * @param {Object} body The full body (result of recursive __body__() calls) 142 | * from this page. 143 | */ 144 | __bodyToHtml__: function(body) { 145 | return body; 146 | }, 147 | 148 | 149 | /** 150 | * Called in the browser to reload and regenerate the current page. 151 | * 152 | * First calls {__preload__} and {__load__} in proper order, then calls 153 | * {rerender} to rerender the page. 154 | */ 155 | reload: function() { 156 | // TODO(Taylor): Ability to reload the full stack including parents. 157 | this.__preload__(util.bind(function() { 158 | this.__load__.apply(this, arguments); 159 | this.rerender(); 160 | }, this)); 161 | }, 162 | 163 | 164 | /** 165 | * Called in the browser to regenerate the current page from the controller's 166 | * current state. 167 | * 168 | * Calls {recursiveUnload}, then {getFullTitle} and {renderFullBody} to 169 | * regenerate the page, then calls {recursiveReady} once the DOM is updated. 170 | */ 171 | rerender: function() { 172 | this.recursiveUnload(); 173 | 174 | document.title = this.getFullTitle(); 175 | document.getElementById('skit-controller').innerHTML = this.renderFullBody(); 176 | this.recursiveReady(); 177 | }, 178 | 179 | 180 | /** @ignore */ 181 | callEachAscending: function(attr, opt_fn) { 182 | var current = this.constructor; 183 | var fns = []; 184 | while (current) { 185 | if (current.prototype.hasOwnProperty(attr)) { 186 | fns.unshift(current.prototype[attr]); 187 | } 188 | current = current.__parent__; 189 | } 190 | iter.forEach(fns, opt_fn || function(r) { r.call(this); }, this); 191 | }, 192 | 193 | 194 | /** @ignore */ 195 | callEachDescending: function(attr) { 196 | var value = ''; 197 | var current = this.constructor; 198 | while (current) { 199 | if (current.prototype.hasOwnProperty(attr)) { 200 | value = current.prototype[attr].call(this, value); 201 | } 202 | current = current.__parent__; 203 | } 204 | return value; 205 | }, 206 | 207 | 208 | /** 209 | * Calls the full controller stack in ascending order. Calls each 210 | * controller class's __load__ method, starting with the base controller 211 | * all the way up to the current controller. 212 | */ 213 | recursiveLoad: function() { 214 | this.callEachAscending('__load__'); 215 | }, 216 | 217 | 218 | /** 219 | * Calls the full controller stack in ascending order. Calls each 220 | * controller class's __ready__ method, starting with the base controller 221 | * all the way up to the current controller. 222 | */ 223 | recursiveReady: function() { 224 | this.callEachAscending('__ready__'); 225 | }, 226 | 227 | 228 | /** 229 | * Calls the full controller stack in reverse order to unload. Calls each 230 | * controller class's __unload__ method, starting with the current controller 231 | * all the way through the topmost parent. 232 | */ 233 | recursiveUnload: function() { 234 | this.callEachDescending('__unload__'); 235 | }, 236 | 237 | 238 | /** 239 | * Calls the full controller stack in reverse order to generate the full 240 | * content of the title tag for the page. Calls each class's 241 | * __title__ method, passing the current title as an argument, starting 242 | * with the current controller all the way through the topmost parent 243 | * controller. 244 | * 245 | * @return {string} The HTML to put in the page's title. 246 | */ 247 | getFullTitle: function() { 248 | return this.callEachDescending('__title__'); 249 | }, 250 | 251 | 252 | /** 253 | * Calls the full controller stack in reverse order to generate the full 254 | * content of the head tag for the page. 255 | * 256 | * @return {string} The HTML to append to the page's head. 257 | */ 258 | getFullMeta: function() { 259 | return this.callEachDescending('__meta__'); 260 | }, 261 | 262 | 263 | /** 264 | * Calls the full controller stack in reverse order, rendering the full page 265 | * and returning the result as a string. 266 | * 267 | * @param {boolean=} opt_raw Whether to return the body without calling 268 | * __bodyToHtml__ first. 269 | * @return {string} The full body HTML for this page as a string. 270 | */ 271 | renderFullBody: function(opt_raw) { 272 | var body = this.callEachDescending('__body__'); 273 | if (!opt_raw) { 274 | body = this.__bodyToHtml__(body); 275 | } 276 | return body; 277 | } 278 | }); 279 | 280 | 281 | /** 282 | * Create a Controller class from the given object keys. If the first 283 | * parameter is another Controller class, it will become this controller's 284 | * parent, which will add it to the same lifecycle as the child, and its 285 | * lifecycle methods will be called automatically. 286 | * 287 | * @param {Function} parent The parent Controller. If the first 288 | * parameter is not a Controller, it will be used as {object}. 289 | * @param {Object} object The object whose member properties will become 290 | * the prototype members of the resulting class. 291 | */ 292 | Controller.create = function(var_args) { 293 | var args = Array.prototype.slice.apply(arguments); 294 | var object, parent; 295 | if (args.length == 2) { 296 | parent = args[0]; 297 | if (!parent.__controller__) { 298 | throw new Error('Specified parent is not a Controller subclass.'); 299 | } 300 | object = args[1]; 301 | } else { 302 | object = args[0]; 303 | } 304 | 305 | var klass = util.createClass(parent || Controller, object); 306 | klass.__controller__ = true; 307 | 308 | if (parent) { 309 | klass.__parent__ = parent; 310 | } 311 | return klass; 312 | }; 313 | 314 | 315 | module.exports = Controller; 316 | -------------------------------------------------------------------------------- /lib/skit/platform/PubSub.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @license 5 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 6 | * License: MIT 7 | */ 8 | 9 | /** @ignore */ 10 | var iter = skit.platform.iter; 11 | 12 | 13 | /** 14 | * A simple PubSub for publishing and subscribing to notifications by name. 15 | * 16 | * @class 17 | */ 18 | var PubSub = function() { 19 | this.listeners_ = []; 20 | this.listenersById_ = {}; 21 | this.lastId_ = 0; 22 | }; 23 | 24 | 25 | var shared_; 26 | 27 | 28 | /** 29 | * @return {PubSub} A shared instance of PubSub for all to enjoy. 30 | */ 31 | PubSub.sharedPubSub = function() { 32 | if (!shared_) { 33 | shared_ = new PubSub(); 34 | } 35 | return shared_; 36 | }; 37 | 38 | 39 | /** 40 | * Subscribe for notifications for a given event name. 41 | * 42 | * @param {string} eventName The name of the event you want to be notified 43 | * about. 44 | * @param {function} callback The callback function to call when the 45 | * specified event happens. 46 | * @param {Object=} opt_context The context to call the callback function 47 | * in when the event happens. 48 | * @return {number} A listener ID, which can later be used to unsubscibe. 49 | */ 50 | PubSub.prototype.subscribe = function(eventName, callback, opt_context) { 51 | if (!(eventName in this.listeners_)) { 52 | this.listeners_[eventName] = []; 53 | } 54 | 55 | var id = this.lastId_++; 56 | this.listeners_[eventName].push(id); 57 | this.listenersById_[id] = [callback, opt_context || null]; 58 | 59 | return id; 60 | }; 61 | 62 | 63 | /** 64 | * Unsubscribe for notifications we previously subscribed to. 65 | * 66 | * @param {number} subscriptionId The subscription ID returned by subscribe(). 67 | */ 68 | PubSub.prototype.unsubscribe = function(subscriptionId) { 69 | delete this.listenersById_[subscriptionId]; 70 | }; 71 | 72 | 73 | /** 74 | * Publish an event by name. Listeners previously added by calling subscribe() 75 | * will be notified. 76 | * 77 | * @param {string} eventName The event name to notify subscribers about. 78 | * @param {...Object} var_args The arguments to pass to the subscriber callbacks. 79 | */ 80 | PubSub.prototype.publish = function(eventName, var_args) { 81 | var args = Array.prototype.slice.call(arguments, 1); 82 | var listeners = this.listeners_[eventName]; 83 | if (!listeners || !listeners.length) { 84 | return; 85 | } 86 | 87 | var deletedListeners = {}; 88 | iter.forEach(listeners, function(subscriptionId) { 89 | var listenerAndContext = this.listenersById_[subscriptionId]; 90 | if (!listenerAndContext) { 91 | deletedListeners[subscriptionId] = 1; 92 | return; 93 | } 94 | 95 | var listener = listenerAndContext[0]; 96 | var context = listenerAndContext[1]; 97 | listener.apply(context, args); 98 | }, this); 99 | 100 | this.listeners_[eventName] = iter.filter(this.listeners_[eventName], function(subscriptionId) { 101 | return !(subscriptionId in deletedListeners); 102 | }); 103 | }; 104 | 105 | 106 | module.exports = PubSub; -------------------------------------------------------------------------------- /lib/skit/platform/cookies.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module 5 | * @license 6 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 7 | * License: MIT 8 | */ 9 | 10 | /** @ignore */ 11 | var browser = __module__.browser; 12 | /** @ignore */ 13 | var server = __module__.server; 14 | 15 | 16 | /** 17 | * Options passed to cookies.set(name, value, options). 18 | * 19 | * @class 20 | * @name CookieOptions 21 | * @property {Date} expires The date when the cookie should expire. 22 | * @property {string} path The path to set the cookie on. 23 | * @property {string} domain The domain to set the cookie on. 24 | * @property {boolean} secure Whether the cookie should only be sent over SSL. 25 | * @property {boolean} httpOnly Whether the cookie should only be available on 26 | * the server side. This option is ignored when using cookies.set in the 27 | * browser, so be careful. 28 | */ 29 | 30 | 31 | /** 32 | * @param {string} name Cookie name. 33 | * @return {string?} The cookie value, if it was present. 34 | */ 35 | module.exports.get = function(name) { 36 | // Dummy function filled in by cookies_browser.js or cookies_server.js. 37 | }; 38 | 39 | 40 | /** 41 | * Set a cookie. 42 | * 43 | * @param {string} name The name of the cookie to set. 44 | * @param {string} value The value of the cookie to set. 45 | * @param {CookieOptions=} opt_options The cookie objects object to set along with the cookie. 46 | */ 47 | module.exports.set = function(name, value, opt_options) { 48 | // Dummy function filled in by cookies_browser.js or cookies_server.js. 49 | }; 50 | 51 | 52 | /* JSDoc, plz to ignore this. */ 53 | module.exports = browser || server; 54 | -------------------------------------------------------------------------------- /lib/skit/platform/cookies_browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'browser-only'; 3 | 4 | /** 5 | * @module 6 | * @ignore 7 | * @license 8 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 9 | * License: MIT 10 | */ 11 | 12 | var cookies = skit.thirdparty.cookies; 13 | 14 | 15 | module.exports = { 16 | get: cookies.get, 17 | set: cookies.set 18 | }; 19 | -------------------------------------------------------------------------------- /lib/skit/platform/cookies_server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'server-only'; 3 | 4 | /** 5 | * @module 6 | * @ignore 7 | * @license 8 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 9 | * License: MIT 10 | */ 11 | 12 | var util = skit.platform.util; 13 | 14 | var _get, _set; 15 | 16 | 17 | module.exports = { 18 | __setGetSet__: function(get, set) { 19 | _get = get; 20 | _set = set; 21 | }, 22 | 23 | get: function() { 24 | var value = _get.apply(this, arguments); 25 | if (value) { 26 | return decodeURIComponent(value); 27 | } 28 | return value; 29 | }, 30 | 31 | set: function(name, value, opt_options) { 32 | var options = opt_options || {}; 33 | for (var k in options) { 34 | if (['path', 'domain', 'expires', 'secure', 'httpOnly'].indexOf(k) < 0) { 35 | throw new Error('Unsupported cookies.set option: ' + k); 36 | } 37 | } 38 | if (typeof options.httpOnly == 'undefined') { 39 | // To match client- and server-side behavior, unless specified, 40 | // don't default to "httponly" cookies. This is less secure, 41 | // but also way less confusing. 42 | options.httpOnly = false; 43 | } 44 | if (value) { 45 | // Cookies library on the client side URI-encodes values, 46 | // so copy that behavior here. 47 | value = (value + '').replace(/[^!#$&-+\--:<-\[\]-~]/g, encodeURIComponent); 48 | } 49 | return _set.call(this, name, value, options); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /lib/skit/platform/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module 5 | * @license 6 | * (c) 2016 Cluster Labs, Inc. https://cluster.co/ 7 | * License: MIT 8 | */ 9 | 10 | /** @ignore */ 11 | var browser = __module__.browser; 12 | /** @ignore */ 13 | var server = __module__.server; 14 | 15 | 16 | 17 | /** 18 | * @param {string} name Environment key name. 19 | * @return {object?} The env value, if present, or null. 20 | */ 21 | module.exports.get = function(name) { 22 | // Dummy function filled in by env_browser.js or env_server.js. 23 | }; 24 | 25 | 26 | /* JSDoc, plz to ignore this. */ 27 | module.exports = browser || server; 28 | -------------------------------------------------------------------------------- /lib/skit/platform/env_browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'browser-only'; 3 | 4 | 5 | module.exports = { 6 | get: function(key) { 7 | if (window.skit && window.skit.env && window.skit.env[key]) { 8 | return skit.env[key]; 9 | } 10 | return null; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /lib/skit/platform/env_server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'server-only'; 3 | 4 | 5 | var env = {}; 6 | 7 | 8 | module.exports = { 9 | get: function(key) { 10 | if (env[key]) { 11 | return env[key]; 12 | } 13 | return null; 14 | }, 15 | 16 | __setEnv__: function(requestEnv) { 17 | env = requestEnv; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /lib/skit/platform/iter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module 5 | * @license 6 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 7 | * License: MIT 8 | */ 9 | 10 | 11 | 12 | /** 13 | * An object with a length property that is subscriptable, like an array, 14 | * but which might not be derived from the Array prototype. 15 | * 16 | * @class 17 | * @name ArrayLike 18 | * @property {number} length The length of the array-like object. 19 | */ 20 | 21 | 22 | /** 23 | * Iterate over an array-like object with optional context. 24 | * 25 | * @param {ArrayLike} items The array-like object to iterate over. 26 | * @param {Function} fn The function to call with each item from {items}. 27 | * @param {Object=} opt_context The context for {this} inside {fn}. 28 | */ 29 | module.exports.forEach = function forEach(items, fn, opt_context) { 30 | var length = items.length || 0; 31 | var stop = false; 32 | var stopFn = function() { 33 | stop = true; 34 | }; 35 | 36 | for (var i = 0; i < length && !stop; i++) { 37 | fn.call(opt_context, items[i], i, stopFn); 38 | } 39 | }; 40 | var forEach = module.exports.forEach; 41 | 42 | 43 | /** 44 | * Filter objects in {items} based on the result of {opt_fn}. 45 | * 46 | * @param {ArrayLike} items The array-like object to iterate over. 47 | * @param {Function=} opt_fn The function to call with each item from {items}, 48 | * which should return a boolean value. If not present, the truthiness of 49 | * the item itself will be used. 50 | * @param {Object=} opt_context The context for {this} inside {opt_fn}. 51 | * @return {Array} A filtered array of items from {items}. 52 | */ 53 | module.exports.filter = function filter(items, opt_fn, opt_context) { 54 | var fn = opt_fn || function(item) { return !!item }; 55 | var array = []; 56 | forEach(items, function(item, i) { 57 | if (fn.call(opt_context, item, i)) { 58 | array.push(item); 59 | } 60 | }); 61 | return array; 62 | }; 63 | 64 | 65 | /** 66 | * Map objects in {items} to new values supplied by {fn}. 67 | * 68 | * @param {ArrayLike} items The array-like object to iterate over. 69 | * @param {Function} fn The function to call with each item from {items}, 70 | * which should return a new object. 71 | * @param {Object=} opt_context The context for {this} inside {fn}. 72 | * @return {Array} The mapped values from {items}. 73 | */ 74 | module.exports.map = function map(items, fn, opt_context) { 75 | var array = []; 76 | var skip = false; 77 | var shouldSkip = function() { 78 | skip = true; 79 | }; 80 | forEach(items, function(item, i) { 81 | var mapped = fn.call(opt_context, item, i, shouldSkip); 82 | if (!skip) { 83 | array.push(mapped); 84 | } 85 | skip = false; 86 | }); 87 | return array; 88 | }; 89 | 90 | 91 | /** 92 | * @param {ArrayLike} array The array to iterate over. 93 | * @param {Object} item The object to find. 94 | * @return {boolean} Whether {array} contains {item}, using == to compare objects. 95 | */ 96 | module.exports.contains = function contains(array, item) { 97 | if (!array || !array.length) { 98 | return false; 99 | } 100 | 101 | for (var i = 0; i < array.length; i++) { 102 | if (array[i] == item) { 103 | return true; 104 | } 105 | } 106 | return false; 107 | }; 108 | 109 | 110 | /** 111 | * @param {ArrayLike} array The array to iterate over. 112 | * @param {Function} fn The function to call with each item from {items}, 113 | * which should return whether the item matches. 114 | * @param {Object=} opt_context The context for {this} inside {fn}. 115 | * @return {number} The index of the item inside {array} if {fn} returned true 116 | * for any of the elements, or -1. 117 | */ 118 | module.exports.indexOf = function indexOf(array, fn, opt_context) { 119 | for (var i = 0; i < array.length; i++) { 120 | var item = array[i]; 121 | if (fn.call(opt_context, item)) { 122 | return i; 123 | } 124 | } 125 | return -1; 126 | }; 127 | 128 | 129 | /** 130 | * @param {ArrayLike} array The array to iterate over. 131 | * @param {Function} fn The function to call with each item from {items}, 132 | * which should return whether the item matches. 133 | * @param {Object=} opt_context The context for {this} inside {fn}. 134 | * @return {Object?} The item that {fn} returned true for, if any. 135 | */ 136 | module.exports.find = function find(array, fn, opt_context) { 137 | return array[module.exports.indexOf(array, fn, opt_context)]; 138 | }; 139 | 140 | 141 | /** 142 | * Convert an array-like object to an Array. 143 | * 144 | * @param {ArrayLike} nonArray The non-array to convert. 145 | * @return {Array} The nonArray object as an Array. 146 | */ 147 | module.exports.toArray = function toArray(nonArray) { 148 | var result = []; 149 | var length = nonArray.length || 0; 150 | for (var i = 0; i < length; i++) { 151 | result.push(nonArray[i]); 152 | } 153 | return result; 154 | }; 155 | -------------------------------------------------------------------------------- /lib/skit/platform/json.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module 5 | * @license 6 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 7 | * License: MIT 8 | */ 9 | 10 | var string = skit.platform.string; 11 | 12 | 13 | // Borrowed from jQuery:parseJSON. 14 | var rvalidchars = /^[\],:{}\s]*$/; 15 | var rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g; 16 | var rvalidescape = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g; 17 | var rvalidtokens = /"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g; 18 | 19 | 20 | /** 21 | * An implementation of JSON.parse() that works on all browsers, borrowed 22 | * from jQuery. Thanks jQuery! 23 | * 24 | * @param {string} data The JSON-encoded string to parse. 25 | * @return {Object} The parsed JavaScript object. 26 | */ 27 | module.exports.parse = function(data) { 28 | if (typeof JSON !== 'undefined' && JSON.parse) { 29 | return JSON.parse(data); 30 | } 31 | 32 | if (!data || typeof data !== 'string') { 33 | return data; 34 | } 35 | 36 | data = string.trim(data); 37 | if (!data) { 38 | return null; 39 | } 40 | 41 | var data = data.replace(rvalidescape, '@') 42 | .replace(rvalidtokens, ']') 43 | .replace(rvalidbraces, ''); 44 | 45 | if (!rvalidchars.test(data)) { 46 | throw new Error('Invalid JSON string: ' + data); 47 | } 48 | return (new Function('return ' + data))(); 49 | }; -------------------------------------------------------------------------------- /lib/skit/platform/navigation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module 5 | * @license 6 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 7 | * License: MIT 8 | */ 9 | 10 | var urls = skit.platform.urls; 11 | 12 | /** @ignore */ 13 | var browser = __module__.browser; 14 | /** @ignore */ 15 | var server = __module__.server; 16 | /** @ignore */ 17 | var navigation = browser || server; 18 | 19 | 20 | /** 21 | * Called during __preload__ to indicate that a given URL should return a 22 | * 404 not found rather than going on to load the full page. 23 | */ 24 | module.exports.notFound = function() { 25 | return navigation.notFound(); 26 | }; 27 | 28 | 29 | /** 30 | * Called during __preload__ or anywhere in the page lifecycle to indicate 31 | * that we should navigate to a new URL. Use this instead of calling 32 | * document.location.href = to make server-side 302's and client-side 33 | * navigation work. 34 | * 35 | * @param {string} url The URL to navigate to. 36 | * @param {boolean=} opt_permanent Whether to issue a permanent redirect, 37 | * ie. a 301, rather than a temporary redirect. 38 | */ 39 | module.exports.navigate = function(url, opt_permanent) { 40 | return navigation.navigate(url, opt_permanent); 41 | }; 42 | 43 | 44 | /** 45 | * @return {string} The current User-Agent, corresponding to the "User-Agent" 46 | * header on the server side or window.navigator.userAgent in the browser. 47 | */ 48 | module.exports.userAgent = function() { 49 | return navigation.userAgent(); 50 | }; 51 | 52 | 53 | /** 54 | * @return {string} The URL of this page's referer. 55 | */ 56 | module.exports.referer = function() { 57 | return navigation.referer(); 58 | }; 59 | 60 | 61 | /** 62 | * @return {string} The current URL, eg. "http://foobar.com/index.html?foo=bar#baz". 63 | */ 64 | module.exports.url = function() { 65 | return navigation.url(); 66 | }; 67 | 68 | 69 | /** 70 | * @return {string} The URL of this page's referer. 71 | */ 72 | module.exports.host = function() { 73 | return urls.parse(navigation.url()).host; 74 | }; 75 | 76 | 77 | /** 78 | * @return {boolean} Whether the current URL is HTTPS. 79 | */ 80 | module.exports.isSecure = function() { 81 | return urls.parse(navigation.url()).scheme == 'https'; 82 | }; 83 | 84 | 85 | /** 86 | * @return {string} The current URL, eg. "/index.html?foo=bar#baz". 87 | */ 88 | module.exports.relativeUrl = function() { 89 | var fullUrl = navigation.url(); 90 | var parsed = urls.parse(fullUrl); 91 | var relativeUrl = urls.appendParams(parsed.path, parsed.params) 92 | if (parsed.hash) { 93 | relativeUrl += '#' + parsed.hash; 94 | } 95 | return relativeUrl; 96 | }; 97 | 98 | 99 | /** 100 | * @return {Object} The URL query parsed into an Object, eg. {'foo': 'bar'}. 101 | */ 102 | module.exports.query = function() { 103 | return urls.parse(navigation.url()).params; 104 | }; 105 | -------------------------------------------------------------------------------- /lib/skit/platform/navigation_browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'browser-only'; 3 | 4 | /** 5 | * @module 6 | * @ignore 7 | * @license 8 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 9 | * License: MIT 10 | */ 11 | 12 | 13 | module.exports = { 14 | notFound: function() { 15 | // TODO(taylor): Revisit this API. 16 | throw new Error('Not found cannot be called after page load.'); 17 | }, 18 | 19 | navigate: function(url, opt_permanent) { 20 | document.body.className += ' navigating'; 21 | window.location.href = url; 22 | }, 23 | 24 | url: function() { 25 | return window.location.href; 26 | }, 27 | 28 | userAgent: function() { 29 | return window.navigator.userAgent; 30 | }, 31 | 32 | referer: function() { 33 | return document.referrer; 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /lib/skit/platform/navigation_server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'server-only'; 3 | 4 | /** 5 | * @module 6 | * @ignore 7 | * @license 8 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 9 | * License: MIT 10 | */ 11 | 12 | var urls = skit.platform.urls; 13 | 14 | var lastUrl = null; 15 | var redirects = null; 16 | var notFound = false; 17 | var userAgent = false; 18 | var referer = null; 19 | 20 | 21 | module.exports = { 22 | __reset__: function(_currentUrl, _userAgent, _referer) { 23 | lastUrl = _currentUrl; 24 | userAgent = _userAgent; 25 | referer = _referer; 26 | 27 | notFound = false; 28 | redirects = []; 29 | }, 30 | 31 | __redirects__: function() { 32 | return redirects; 33 | }, 34 | 35 | __notfound__: function() { 36 | return notFound; 37 | }, 38 | 39 | notFound: function() { 40 | notFound = true; 41 | }, 42 | 43 | navigate: function(url, opt_permanent) { 44 | if (!lastUrl) { 45 | throw new Error('Cannot navigate before the request has begun.'); 46 | } 47 | 48 | var newFullUrl; 49 | var parsed = urls.parse(lastUrl); 50 | var newParsed = urls.parse(url); 51 | if (newParsed.scheme) { 52 | newFullUrl = url; 53 | } else { 54 | var newPathParts = newParsed.path.split('/'); 55 | if (newPathParts[0] != '') { 56 | var oldPathParts = parsed.path.split('/'); 57 | newPathParts = oldPathParts.slice(0, oldPathParts.length - 1).concat(newPathParts); 58 | } 59 | newFullUrl = parsed.scheme + '://' + parsed.host + newPathParts.join('/'); 60 | if (newParsed.params) { 61 | newFullUrl = urls.appendParams(newFullUrl, newParsed.params); 62 | } 63 | if (newParsed.hash) { 64 | newFullUrl += '#' + newParsed.hash; 65 | } 66 | } 67 | 68 | if (lastUrl != newFullUrl) { 69 | redirects.push({ 70 | url: newFullUrl, 71 | permanent: !!opt_permanent 72 | }); 73 | } 74 | 75 | lastUrl = newFullUrl; 76 | }, 77 | 78 | url: function() { 79 | if (!lastUrl) { 80 | throw new Error('Cannot access the current URL before the request has begun.'); 81 | } 82 | 83 | return lastUrl; 84 | }, 85 | 86 | userAgent: function() { 87 | return userAgent; 88 | }, 89 | 90 | referer: function() { 91 | return referer; 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /lib/skit/platform/net.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module 5 | * @license 6 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 7 | * License: MIT 8 | */ 9 | 10 | 11 | /** @ignore */ 12 | var urls = skit.platform.urls; 13 | /** @ignore */ 14 | var util = skit.platform.util; 15 | 16 | /** @ignore */ 17 | var server = __module__.server; 18 | /** @ignore */ 19 | var browser = __module__.browser; 20 | /** @ignore */ 21 | var Response = __module__.Response; 22 | 23 | /** @ignore */ 24 | var environment = server || browser; 25 | 26 | 27 | /** 28 | * Issue an HTTP request to the given URL. 29 | * @param {string} url The URL where to send the request. 30 | * @param {SendOptions=} opt_options Options for the request, see the 31 | * {SendOptions} documentation for more details. 32 | */ 33 | module.exports.send = function(url, opt_options) { 34 | var options = opt_options || {}; 35 | 36 | var startTime = +(new Date()); 37 | var method = (options.method || 'GET').toUpperCase(); 38 | var body = options.body || ''; 39 | var contentType = ''; 40 | if (options.params) { 41 | if (method == 'GET') { 42 | url = urls.appendParams(url, options.params); 43 | } else if (options.params) { 44 | if (typeof options.params == 'string') { 45 | body = options.params; 46 | } else { 47 | body = urls.toFormEncodedString(options.params); 48 | } 49 | contentType = 'application/x-www-form-urlencoded'; 50 | } 51 | } 52 | 53 | var headers = options.headers || {}; 54 | if (contentType) { 55 | headers['Content-Type'] = contentType; 56 | } 57 | 58 | var sender = options.proxy || environment.send; 59 | sender(method, url, headers, body, function(status, headers, body) { 60 | // Don't log proxied requests because they'll get logged twice. 61 | if (options.noisy) { 62 | util.log('[skit.platform.net] ' + method + ' ' + url + ' - ' + status + ' - ' + (+(new Date()) - startTime) + 'ms'); 63 | } 64 | 65 | var response = new Response(status, headers, body); 66 | 67 | if (response.status == 200) { 68 | if (options.success) { 69 | options.success.call(options.context, response); 70 | } 71 | } else { 72 | if (options.error) { 73 | options.error.call(options.context, response); 74 | } 75 | } 76 | 77 | if (options.complete) { 78 | options.complete.call(options.context, response); 79 | } 80 | }); 81 | }; 82 | -------------------------------------------------------------------------------- /lib/skit/platform/net_Response.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @class 5 | * @license 6 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 7 | * License: MIT 8 | */ 9 | 10 | /** @ignore */ 11 | var json = skit.platform.json; 12 | /** @ignore */ 13 | var iter = skit.platform.iter; 14 | /** @ignore */ 15 | var string = skit.platform.string; 16 | 17 | 18 | function headerName(str) { 19 | return iter.map(str.split('-'), string.capitalize).join('-'); 20 | } 21 | 22 | 23 | /** 24 | * A response from the net.send() function, passed to all callbacks. 25 | * 26 | * @param {number} statusCode The status code. 27 | * @param {Object} headers The response headers. 28 | * @param {string} bodyText The body text. 29 | * @constructor 30 | */ 31 | function Response(statusCode, headers, bodyText) { 32 | this.status = statusCode; 33 | this.headers = {}; 34 | for (var key in headers) { 35 | this.headers[headerName(key)] = headers[key]; 36 | } 37 | 38 | var contentType = this.headers['Content-Type'] || ''; 39 | // Various public APIs return JSON stuff as all kinds of things. 40 | var isMaybeJSON = contentType.indexOf('/json') > -1 || 41 | contentType.indexOf('/javascript') > -1 || 42 | contentType.indexOf('/x-javascript') > -1; 43 | 44 | this.bodyText = bodyText; 45 | this.body = bodyText; 46 | 47 | if (isMaybeJSON && typeof bodyText === 'string') { 48 | try { 49 | this.body = json.parse(bodyText); 50 | } catch (e) {} 51 | } 52 | }; 53 | 54 | 55 | /** 56 | * @property {number} The response status, eg. 200. 57 | */ 58 | Response.prototype.status; 59 | 60 | 61 | /** 62 | * @property {Object} The response body. If the response's content-type 63 | * indicates that this is probably JSON, this property will be an Object. 64 | * Otherwise it will be a string. 65 | */ 66 | Response.prototype.body; 67 | 68 | 69 | /** 70 | * @property {string} The response body. Regardless of {body}, this will 71 | * be a raw string. 72 | */ 73 | Response.prototype.bodyText; 74 | 75 | 76 | /** 77 | * @property {Object} The response headers, 78 | * eg. {'Content-Type': 'application/json'}. Header names are guaranteed to 79 | * be capitalized in the form 'Content-Type'. 80 | */ 81 | Response.prototype.headers; 82 | 83 | 84 | module.exports = Response; -------------------------------------------------------------------------------- /lib/skit/platform/net_SendOptions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @license 5 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 6 | * License: MIT 7 | */ 8 | 9 | 10 | /** 11 | * @class 12 | * @name SendOptions 13 | * @property {string=} method GET or POST, default GET. 14 | * @property {Object=} params Params to encode and include on the URL in a GET 15 | * request, or in the POSTbody as a form-encoded string if a POST request. 16 | * @property {Object=} headers Headers to append to the request, eg. 17 | * {'X-Foobar': 'The foobar you requested.'}. 18 | * @property {string=} body POSTbody to send. If params are also specified, 19 | * there's no telling what will happen. 20 | * @property {SendOptions~callback=} success A callback to call when a request 21 | * is successful. This is called on 200-coded results. If success is 22 | * called, error will not be called. 23 | * @property {SendOptions~callback=} error A callback to call when a request 24 | * is not successful. This is called on non-200 results. If error is 25 | * called, success will not be called. 26 | * @property {SendOptions~callback=} complete A callback to call when a request 27 | * is complete, regardless of responseCode. 28 | */ 29 | var SendOptions; // This exists purely for JSDoc. 30 | 31 | 32 | /** 33 | * This callback is called as a result of requests made by send(). 34 | * @callback SendOptions~callback 35 | * @param {Response} response The response from the server. 36 | */ 37 | var SendOptionsCallback; // This exists purely for JSDoc. 38 | -------------------------------------------------------------------------------- /lib/skit/platform/net_browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'browser-only'; 3 | 4 | /** 5 | * @module 6 | * @ignore 7 | * @license 8 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 9 | * License: MIT 10 | */ 11 | 12 | 13 | var iter = skit.platform.iter; 14 | 15 | 16 | function createXHR() { 17 | var xhr; 18 | if (window.ActiveXObject) { 19 | try { 20 | xhr = new ActiveXObject('Microsoft.XMLHTTP'); 21 | } catch(e) { 22 | xhr = null; 23 | } 24 | } else { 25 | xhr = new XMLHttpRequest(); 26 | } 27 | 28 | return xhr; 29 | } 30 | 31 | 32 | function send(method, url, headers, body, onComplete) { 33 | var xhr = createXHR(); 34 | xhr.onreadystatechange = function() { 35 | if (xhr.readyState != 4) { 36 | return; 37 | } 38 | 39 | var headersText = xhr.getAllResponseHeaders(); 40 | var headers = {}; 41 | 42 | iter.forEach(headersText.split(/[\n\r]+/), function(line) { 43 | var result = /^([\w-]+):\s*(.+)$/.exec(line); 44 | if (result) { 45 | headers[result[1]] = result[2]; 46 | } 47 | }); 48 | 49 | onComplete(xhr.status, headers, xhr.responseText); 50 | }; 51 | 52 | xhr.open(method, url, true); 53 | for (var key in headers) { 54 | xhr.setRequestHeader(key, headers[key]); 55 | } 56 | xhr.send(body); 57 | } 58 | 59 | 60 | module.exports = { 61 | send: send 62 | }; -------------------------------------------------------------------------------- /lib/skit/platform/net_server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'server-only'; 3 | 4 | /** 5 | * @module 6 | * @ignore 7 | * @license 8 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 9 | * License: MIT 10 | */ 11 | 12 | // Server-only code so we can hack it a bit and reach into local 13 | // dependencies. I know, I feel dirty doing this too. 14 | var request = require('request'); 15 | 16 | var __errorHandler__ = function(e) { 17 | throw e; 18 | }; 19 | 20 | function send(method, url, headers, body, onComplete) { 21 | var handleResponse = function(err, nodeResponse, body) { 22 | var status = nodeResponse && nodeResponse.statusCode; 23 | var headers = nodeResponse && nodeResponse.headers; 24 | 25 | try { 26 | onComplete(status, headers, body); 27 | } catch(e) { 28 | __errorHandler__(e); 29 | } 30 | }; 31 | 32 | var hasUA = false; 33 | for (var k in (headers || {})) { 34 | if (k.toLowerCase() == 'user-agent') { 35 | hasUA = true; 36 | break; 37 | } 38 | } 39 | 40 | if (!hasUA) { 41 | headers['User-Agent'] = 'Skit Backend (XMLHTTPRequest proxy)'; 42 | } 43 | 44 | var requestOptions = {method: method, url: url, body: body, headers: headers}; 45 | request(requestOptions, handleResponse); 46 | } 47 | 48 | 49 | module.exports = { 50 | send: send, 51 | __setErrorHandler__: function(fn) { 52 | __errorHandler__ = fn; 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /lib/skit/platform/netproxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module 5 | * @ignore 6 | * @license 7 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 8 | * License: MIT 9 | */ 10 | 11 | var urls = skit.platform.urls; 12 | 13 | var server = __module__.server; 14 | var browser = __module__.browser; 15 | 16 | var PROXIES = {}; 17 | 18 | var environment = server || browser; 19 | var sendProxied = environment.sendProxied; 20 | 21 | 22 | function __register__(name, proxyObject) { 23 | PROXIES[name] = proxyObject; 24 | } 25 | 26 | 27 | function getProxyNamed(name) { 28 | var proxyObject = PROXIES[name]; 29 | if (!proxyObject) { 30 | throw new Error('Improperly configured: no proxy named ' + name); 31 | } 32 | 33 | return function() { 34 | // The arguments here are the same as skit.platform.net:send(), 35 | // we are adding the given proxy object to the front. 36 | var args = Array.prototype.slice.apply(arguments); 37 | args.unshift(proxyObject); 38 | sendProxied.apply(null, args); 39 | } 40 | } 41 | 42 | 43 | module.exports = { 44 | __register__: __register__, 45 | getProxyNamed: getProxyNamed 46 | }; 47 | -------------------------------------------------------------------------------- /lib/skit/platform/netproxy_browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'browser-only'; 3 | 4 | /** 5 | * @module 6 | * @ignore 7 | * @license 8 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 9 | * License: MIT 10 | */ 11 | 12 | var net = skit.platform.net; 13 | var urls = skit.platform.urls; 14 | 15 | 16 | function sendProxied(proxyObject, method, url, headers, body, onComplete) { 17 | net.send('/__proxy__/' + proxyObject.name, { 18 | method: 'POST', 19 | params: { 20 | url: url, 21 | method: method, 22 | headers: urls.toFormEncodedString(headers || {}), 23 | body: body, 24 | csrfToken: proxyObject.csrfToken 25 | }, 26 | complete: function(response) { 27 | // unpack the response from the proxy endpoint, if it exists. 28 | var parsed = response.body || {}; 29 | var status = parsed['status'] || -1; 30 | var headers = parsed['headers'] || {}; 31 | var body = parsed['body'] || ''; 32 | 33 | onComplete(status, headers, body); 34 | } 35 | }); 36 | } 37 | 38 | 39 | module.exports = { 40 | sendProxied: sendProxied 41 | }; -------------------------------------------------------------------------------- /lib/skit/platform/netproxy_server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'server-only'; 3 | 4 | /** 5 | * @module 6 | * @ignore 7 | * @license 8 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 9 | * License: MIT 10 | */ 11 | 12 | var net = skit.platform.net; 13 | 14 | 15 | function sendProxied(proxyObject, method, url, headers, body, onComplete) { 16 | var apiRequest = {method: method, url: url, headers: headers, body: body}; 17 | proxyObject.modifyRequestInternal(apiRequest); 18 | 19 | net.send(apiRequest.url, { 20 | method: apiRequest.method, 21 | body: apiRequest.body, 22 | headers: apiRequest.headers, 23 | complete: function(response) { 24 | proxyObject.modifyResponseInternal(response); 25 | onComplete(response.status, response.headers, response.body); 26 | } 27 | }); 28 | } 29 | 30 | 31 | module.exports = { 32 | sendProxied: sendProxied 33 | }; 34 | -------------------------------------------------------------------------------- /lib/skit/platform/object.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module 5 | * @license 6 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 7 | * License: MIT 8 | */ 9 | 10 | 11 | /** 12 | * Shallow copy an Object's keys to a new object. 13 | * 14 | * @param {Object} obj The object to copy, eg. {'a': 'b'}. 15 | * @return {Object} A new object containing the same keys, eg. {'a': 'b'}. 16 | */ 17 | module.exports.copy = function copy(obj) { 18 | var newObj = {}; 19 | for (var k in obj) { 20 | newObj[k] = obj[k]; 21 | } 22 | return newObj; 23 | }; 24 | -------------------------------------------------------------------------------- /lib/skit/platform/string.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module 5 | * @license 6 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 7 | * License: MIT 8 | */ 9 | 10 | var iter = skit.platform.iter; 11 | 12 | 13 | /** 14 | * @param {string} str A string with whitespace, eg. " abc ". 15 | * @return {string} A trimmed string, eg. "abc". 16 | */ 17 | module.exports.trim = function trim(str) { 18 | return (str || '').replace(/^\s+|\s+$/g, ''); 19 | }; 20 | 21 | 22 | /** 23 | * @param {string} str A string, eg. "abc". 24 | * @return {string} The string with the first letter capitalized, eg. "Abc". 25 | */ 26 | module.exports.capitalize = function capitalize(str) { 27 | return str.charAt(0).toUpperCase() + str.slice(1); 28 | }; 29 | 30 | 31 | /** 32 | * @param {string} str A hyphenated string, eg. "abc-def". 33 | * @return {string} The string converted to camel case, eg. "abcDef". 34 | */ 35 | module.exports.camelCase = function camelCase(str) { 36 | var substrs = str.split('-'); 37 | var first = substrs[0]; 38 | substrs = iter.map(substrs.slice(1), function(substr) { 39 | return module.exports.capitalize(substr); 40 | }); 41 | return [first].concat(substrs).join(''); 42 | }; 43 | 44 | 45 | var replaceHtmlChars = /[&<>"'`]/g; 46 | var charToHtml = { 47 | "&": "&", 48 | "<": "<", 49 | ">": ">", 50 | '"': """, 51 | "'": "'", 52 | "`": "`" 53 | }; 54 | 55 | 56 | /** 57 | * @param {string} str A string potentially containing HTML. 58 | * @return {string} The string with meaningful HTML characters (&, <, >, ", ', `) escaped. 59 | */ 60 | module.exports.escapeHtml = function escapeHtml(str) { 61 | if (!str) { 62 | return str; 63 | } 64 | return str.replace(replaceHtmlChars, function(c) { 65 | return charToHtml[c]; 66 | }); 67 | }; 68 | 69 | 70 | /** 71 | * @param {string} str A string potentially containing RegExp special chars. 72 | * @return {string} The string with any RegExp special characters escaped. 73 | */ 74 | module.exports.escapeRegex = function escapeRegex(str) { 75 | if (!str) { return str; } 76 | return str.replace(/[\[\]\/\\{}()*+?.^$|-]/g, '\\$&'); 77 | }; 78 | 79 | 80 | /** 81 | * @param {string} str A string not suitable for a URL or variable name, eg. "Señor Sisig" 82 | * @param {string=} opt_hyphen The space replacement character, defaults to "-". 83 | * @return {string} A slugified string, eg. "senor-sisig". 84 | */ 85 | module.exports.slugify = function slugify(str, opt_hyphen) { 86 | var hyphen = opt_hyphen || '-'; 87 | var accents = "àáäâèéëêìíïîòóöôùúüûñç"; 88 | var without = "aaaaeeeeiiiioooouuuunc"; 89 | 90 | return (str.toLowerCase() 91 | .replace( 92 | new RegExp('[' + accents + ']', 'g'), 93 | function (c) { return without.charAt(accents.indexOf(c)); }) 94 | .replace(/[^a-z0-9]+/g, hyphen) 95 | .replace(new RegExp('^' + hyphen + '|' + hyphen + '$', 'g'), '') 96 | ); 97 | }; -------------------------------------------------------------------------------- /lib/skit/platform/urls.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module 5 | * @license 6 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 7 | * License: MIT 8 | */ 9 | 10 | 11 | /** 12 | * @param {string} str A querystring component, eg. "foo%20bar" or "foo+bar". 13 | * @return {string} An unescaped string, eg. "foo bar" 14 | */ 15 | function decodeQuerystringComponent(str) { 16 | if (!str) { 17 | return str; 18 | } 19 | 20 | return decodeURIComponent(str.replace(/\+/g, ' ')); 21 | } 22 | 23 | 24 | /** 25 | * Convert an object of keys/values to a form-encoded string, eg. 26 | * {'a': 'b=c', 'd': 'e'} => 'a=b%26c&d=e'. 27 | * 28 | * @param {Object} params The object of keys/values to encode. Nesting of 29 | * objects is not supported and will have unexpected results. 30 | * @return {string} The form-encoded string. 31 | */ 32 | module.exports.toFormEncodedString = function toFormEncodedString(params) { 33 | var pairs = []; 34 | for (var key in params) { 35 | pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent('' + params[key])); 36 | } 37 | return pairs.join('&'); 38 | }; 39 | 40 | 41 | /** 42 | * The result returned by urls.parse(). 43 | * @class 44 | * @name UriInformation 45 | * @property {string} scheme "http" for "http://www.ex.com:80/abc?d=e#f=g" 46 | * @property {string} host "www.ex.com" for "http://www.ex.com:80/abc?d=e#f=g" 47 | * @property {number?} port 80 for "http://www.ex.com:80/abc?d=e#f=g", null 48 | * if not specified. 49 | * @property {string} path "/abc" for "http://www.ex.com:80/abc?d=e#f=g" 50 | * @property {string?} hash "f=g" for "http://www.ex.com:80/abc?d=e#f=g" 51 | * @property {Object} params {d: 'e'} for "http://www.ex.com:80/abc?d=e#f=g" 52 | */ 53 | 54 | 55 | /** 56 | * Parses a URL into its component parts. 57 | * 58 | * @param {string} url The URL to parse. 59 | * @return {UriInformation} The parsed result. 60 | */ 61 | module.exports.parse = function parse(url) { 62 | var preAndPostHash = url.split('#'); 63 | var hash = preAndPostHash[1] || null; 64 | var pathAndQuerystring = preAndPostHash[0].split('?'); 65 | var querystring = pathAndQuerystring[1] || ''; 66 | var path = pathAndQuerystring[0]; 67 | 68 | var scheme = null; 69 | var host = null; 70 | var port = null; 71 | if (path.indexOf('/') > 0) { 72 | var schemeHostAndPath = path.match(/^([A-Za-z]+):\/\/([^\/:]+(?:\:(\d+))?)(\/.*)?$/); 73 | if (schemeHostAndPath) { 74 | scheme = schemeHostAndPath[1]; 75 | host = schemeHostAndPath[2]; 76 | port = schemeHostAndPath[3] || null; 77 | if (port) { 78 | port = parseInt(port, 10); 79 | } 80 | path = schemeHostAndPath[4]; 81 | } 82 | } 83 | 84 | var existingPairs = querystring.split('&'); 85 | var params = {}; 86 | for (var i = 0; i < existingPairs.length; i++) { 87 | var split = existingPairs[i].split('='); 88 | if (split[0].length) { 89 | params[decodeQuerystringComponent(split[0])] = decodeQuerystringComponent(split[1]); 90 | } 91 | } 92 | 93 | return {scheme: scheme, host: host, port: port, path: path, hash: hash, params: params}; 94 | }; 95 | 96 | 97 | /** 98 | * Given a base URL, append the parameters to the end of the URL. Updates 99 | * existing params to the new values specified in {params}. 100 | * 101 | * @param {string} url A URL, eg. "/index.html?a=b" 102 | * @param {Object} params The params to append, eg. {'a': 'c'}. 103 | * @return {string} The URL with the params appended, eg. "/index.html?a=c" 104 | */ 105 | module.exports.appendParams = function appendParams(url, params) { 106 | var parsed = module.exports.parse(url); 107 | 108 | var newParams = parsed.params; 109 | for (var key in params) { 110 | if (params[key] === null) { 111 | delete newParams[key]; 112 | } else { 113 | newParams[key] = params[key]; 114 | } 115 | } 116 | 117 | var newPairs = []; 118 | for (var key in newParams) { 119 | var value = newParams[key]; 120 | newPairs.push(encodeURIComponent(key) + (typeof value != 'undefined' ? '=' + encodeURIComponent(value) : '')); 121 | } 122 | var newQuerystring = newPairs.join('&'); 123 | 124 | var newUrl = parsed.path; 125 | if (newQuerystring.length) { 126 | newUrl += '?' + newQuerystring; 127 | } 128 | if (parsed.hash && parsed.hash.length) { 129 | newUrl += '#' + parsed.hash; 130 | } 131 | if (parsed.scheme && parsed.host) { 132 | newUrl = parsed.scheme + '://' + parsed.host + newUrl; 133 | } 134 | 135 | return newUrl; 136 | }; 137 | -------------------------------------------------------------------------------- /lib/skit/platform/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @module 5 | * @license 6 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 7 | * License: MIT 8 | */ 9 | 10 | 11 | 12 | /** 13 | * Setup a class to inherit from another class. 14 | * 15 | * @param {Function} childCtor The child constructor function. 16 | * @param {Function} parentCtor The parent constructor function. 17 | */ 18 | module.exports.inherits = function inherits(childCtor, parentCtor) { 19 | function tempCtor() {}; 20 | tempCtor.prototype = parentCtor.prototype; 21 | childCtor.superClass_ = parentCtor.prototype; 22 | childCtor.prototype = new tempCtor(); 23 | childCtor.prototype.constructor = childCtor; 24 | }; 25 | 26 | 27 | /** 28 | * Create a constructor function from the given object keys. If the first 29 | * parameter is a Function, it will become the parent constructor for this 30 | * class. If the __init__ member exists in the given object, it will be called 31 | * when the object is created. 32 | * 33 | * @param {Function} parent The parent class constructor. If the first 34 | * parameter is not a function, it will be used as {object}. 35 | * @param {Object} object The object whose member properties will become 36 | * the prototype members of the resulting class. 37 | */ 38 | module.exports.createClass = function createClass() { 39 | var parent = null; 40 | var object; 41 | var parentOrObject = arguments[0]; 42 | if (typeof parentOrObject == 'function') { 43 | parent = parentOrObject; 44 | object = arguments[1]; 45 | } else { 46 | object = parentOrObject; 47 | } 48 | 49 | if (!object) { 50 | throw new Error('Supply an object that optionally defines __init__.'); 51 | } 52 | 53 | var f = function() { 54 | if (this.__init__) { 55 | this.__init__.apply(this, arguments); 56 | } 57 | }; 58 | if (parent) { 59 | module.exports.inherits(f, parent); 60 | } 61 | for (var k in object) { 62 | f.prototype[k] = object[k]; 63 | } 64 | return f; 65 | }; 66 | 67 | 68 | /** 69 | * Bind {this} to the given context inside {fn}. 70 | * 71 | * @param {Function} fn The function to bind. 72 | * @param {Object} context The object to bind as {this} inside {fn}. 73 | * @param {...Object} var_args The arguments to bind after {this}. 74 | */ 75 | module.exports.bind = function bind(fn, context, var_args) { 76 | var args = Array.prototype.slice.call(arguments, 2); 77 | return function() { 78 | var moreArgs = Array.prototype.slice.call(arguments); 79 | return fn.apply(context, args.concat(moreArgs)); 80 | }; 81 | }; 82 | 83 | 84 | var hasConsoleLog = false; 85 | try { 86 | hasConsoleLog = !!(typeof console != 'undefined' && console.log && console.log.apply); 87 | } catch (e) {} 88 | 89 | 90 | /** 91 | * In environments that support console.log(), call it with the given 92 | * arguments. Otherwise, do nothing. 93 | * 94 | * @param {...Object} var_args The arguments to pass to console.log(). 95 | */ 96 | module.exports.log = function log(var_args) { 97 | if (hasConsoleLog) { 98 | console.log.apply(console, arguments); 99 | } 100 | }; 101 | 102 | 103 | /** 104 | * Call setTimeout with an optional opt_context. 105 | * 106 | * @param {Function} fn The function to call after the timeout. 107 | * @param {number} time The time in milliseconds to wait before calling {fn}. 108 | * @param {Object=} opt_context The context for {this} inside {fn}. 109 | */ 110 | module.exports.setTimeout = function(fn, time, opt_context) { 111 | return setTimeout(function() { 112 | fn.apply(opt_context, arguments); 113 | }, time); 114 | }; 115 | 116 | 117 | /** 118 | * Call a function as soon as possible in the given opt_context. 119 | * 120 | * @param {Function} fn The function to call after the timeout. 121 | * @param {Object=} opt_context The context for {this} inside {fn}. 122 | */ 123 | module.exports.nextTick = function nextTick(fn, opt_context) { 124 | return module.exports.setTimeout(fn, 0, opt_context); 125 | }; 126 | -------------------------------------------------------------------------------- /lib/skit/thirdparty/cookies.js: -------------------------------------------------------------------------------- 1 | 'browser-only'; 2 | 3 | /*! 4 | * Cookies.js - 0.3.1 5 | * Wednesday, April 24 2013 @ 2:28 AM EST 6 | * 7 | * Copyright (c) 2013, Scott Hamper 8 | * Licensed under the MIT license, 9 | * http://www.opensource.org/licenses/MIT 10 | */ 11 | define([], function () { 12 | 'use strict'; 13 | 14 | var Cookies = function (key, value, options) { 15 | return arguments.length === 1 ? 16 | Cookies.get(key) : Cookies.set(key, value, options); 17 | }; 18 | 19 | // Allows for setter injection in unit tests 20 | Cookies._document = document; 21 | Cookies._navigator = navigator; 22 | 23 | Cookies.defaults = { 24 | path: '/' 25 | }; 26 | 27 | Cookies.get = function (key) { 28 | if (Cookies._cachedDocumentCookie !== Cookies._document.cookie) { 29 | Cookies._renewCache(); 30 | } 31 | 32 | return Cookies._cache[key]; 33 | }; 34 | 35 | Cookies.set = function (key, value, options) { 36 | options = Cookies._getExtendedOptions(options); 37 | options.expires = Cookies._getExpiresDate(value === undefined ? -1 : options.expires); 38 | 39 | Cookies._document.cookie = Cookies._generateCookieString(key, value, options); 40 | 41 | return Cookies; 42 | }; 43 | 44 | Cookies.expire = function (key, options) { 45 | return Cookies.set(key, undefined, options); 46 | }; 47 | 48 | Cookies._getExtendedOptions = function (options) { 49 | return { 50 | path: options && options.path || Cookies.defaults.path, 51 | domain: options && options.domain || Cookies.defaults.domain, 52 | expires: options && options.expires || Cookies.defaults.expires, 53 | secure: options && options.secure !== undefined ? options.secure : Cookies.defaults.secure 54 | }; 55 | }; 56 | 57 | Cookies._isValidDate = function (date) { 58 | return Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date.getTime()); 59 | }; 60 | 61 | Cookies._getExpiresDate = function (expires, now) { 62 | now = now || new Date(); 63 | switch (typeof expires) { 64 | case 'number': expires = new Date(now.getTime() + expires * 1000); break; 65 | case 'string': expires = new Date(expires); break; 66 | } 67 | 68 | if (expires && !Cookies._isValidDate(expires)) { 69 | throw new Error('`expires` parameter cannot be converted to a valid Date instance'); 70 | } 71 | 72 | return expires; 73 | }; 74 | 75 | Cookies._generateCookieString = function (key, value, options) { 76 | key = encodeURIComponent(key); 77 | value = (value + '').replace(/[^!#$&-+\--:<-\[\]-~]/g, encodeURIComponent); 78 | options = options || {}; 79 | 80 | var cookieString = key + '=' + value; 81 | cookieString += options.path ? ';path=' + options.path : ''; 82 | cookieString += options.domain ? ';domain=' + options.domain : ''; 83 | cookieString += options.expires ? ';expires=' + options.expires.toUTCString() : ''; 84 | cookieString += options.secure ? ';secure' : ''; 85 | 86 | return cookieString; 87 | }; 88 | 89 | Cookies._getCookieObjectFromString = function (documentCookie) { 90 | var cookieObject = {}; 91 | var cookiesArray = documentCookie ? documentCookie.split('; ') : []; 92 | 93 | for (var i = 0; i < cookiesArray.length; i++) { 94 | var cookieKvp = Cookies._getKeyValuePairFromCookieString(cookiesArray[i]); 95 | 96 | if (cookieObject[cookieKvp.key] === undefined) { 97 | cookieObject[cookieKvp.key] = cookieKvp.value; 98 | } 99 | } 100 | 101 | return cookieObject; 102 | }; 103 | 104 | Cookies._getKeyValuePairFromCookieString = function (cookieString) { 105 | // "=" is a valid character in a cookie value according to RFC6265, so cannot `split('=')` 106 | var separatorIndex = cookieString.indexOf('='); 107 | 108 | // IE omits the "=" when the cookie value is an empty string 109 | separatorIndex = separatorIndex < 0 ? cookieString.length : separatorIndex; 110 | 111 | return { 112 | key: decodeURIComponent(cookieString.substr(0, separatorIndex)), 113 | value: decodeURIComponent(cookieString.substr(separatorIndex + 1)) 114 | }; 115 | }; 116 | 117 | Cookies._renewCache = function () { 118 | Cookies._cache = Cookies._getCookieObjectFromString(Cookies._document.cookie); 119 | Cookies._cachedDocumentCookie = Cookies._document.cookie; 120 | }; 121 | 122 | Cookies._areEnabled = function () { 123 | return Cookies.set('cookies.js', 1).get('cookies.js') === '1'; 124 | }; 125 | 126 | Cookies.enabled = Cookies._areEnabled(); 127 | 128 | return Cookies; 129 | }); -------------------------------------------------------------------------------- /lib/skitutil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @license 5 | * (c) 2014 Cluster Labs, Inc. https://cluster.co/ 6 | * License: MIT 7 | */ 8 | 9 | 10 | function safeJSONStringify(arg, opt_pretty) { 11 | return JSON.stringify(arg, null, opt_pretty && ' ').replace(/[<>'&\u2028\u2029]/g, function(char) { 12 | var str = char.charCodeAt(0).toString(16); 13 | return '\\u0000'.substring(0, 2 + (4 - str.length)) + str; 14 | }); 15 | } 16 | 17 | function escapeHtml(str) { 18 | return str && str.replace(/&/g,'&').replace(//g,'>'); 19 | } 20 | 21 | function escapeRegex(str) { 22 | if (!str) { return str; } 23 | return str.replace(/[\[\]\/\\{}()*+?.^$|-]/g, '\\$&'); 24 | } 25 | 26 | function unique(array) { 27 | var added = {}; 28 | return array.filter(function(item) { 29 | if (item in added) { 30 | return false; 31 | } 32 | added[item] = true; 33 | return true; 34 | }); 35 | }; 36 | 37 | module.exports = { 38 | safeJSONStringify: safeJSONStringify, 39 | escapeHtml: escapeHtml, 40 | escapeRegex: escapeRegex, 41 | unique: unique, 42 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "skit", 3 | "description": "A pure JavaScript frontend for building better web clients.", 4 | "version": "0.3.8", 5 | "engines": { 6 | "node": ">=12.0" 7 | }, 8 | "bin": "./bin/skit", 9 | "dependencies": { 10 | "acorn": "3.0.x", 11 | "body-parser": "1.20.x", 12 | "compression": "1.7.x", 13 | "connect": "3.7.x", 14 | "cookies": "0.6.1", 15 | "handlebars": "4.7.x", 16 | "http-proxy": "1.18.x", 17 | "minimist": "1.2.x", 18 | "request": "2.88.x", 19 | "send": "0.18.x", 20 | "uglify-js": "2.6.1" 21 | }, 22 | "main": "lib/skit", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/clusterinc/skit.git" 26 | }, 27 | "bugs": { 28 | "url": "http://github.com/clusterinc/skit/issues" 29 | }, 30 | "homepage": "http://skitjs.com/", 31 | "license": "MIT" 32 | } 33 | --------------------------------------------------------------------------------