├── .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 | Reload
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 | No results. Terrible demo! (Start over )
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 |
--------------------------------------------------------------------------------
/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 |
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 |
61 |
62 | A module system for building components that consist of templates,
63 | stylesheets and JavaScript together.
64 |
65 |
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 |
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 |
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 |
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 | Skit parses the URL structure to find the current skit controller
186 | Skit instantiates your controller module
187 |
188 | Your controller: Example.js
189 |
190 | preload()
— Loads data from the backend API
191 | load()
— Sets up state after data is loaded
192 | render()
— Generate <title> text and <body> HTML for the resulting page
193 |
194 |
195 | Skit stores the state loaded in preload
196 | Skit outputs the HTML rendered during render
197 | Skit outputs a bunch of extra JavaScript to take over in the client
198 |
199 |
200 |
201 |
↓ HTTP transport ↓
202 |
203 |
204 |
Client side
205 |
206 | Skit reloads the same server-side modules in the client
207 | Skit restores the state loaded in preload
, serialized as JSON
208 |
209 | Your controller: Example.js
210 |
211 | load()
— Sets up state after data is loaded (now in the client)
212 | ready()
— Sets up client-side event handlers for clicks, scrolling, etc.
213 | … whatever else your client does in the browser.
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
Default configuration
222 |
223 |
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 |
228 |
229 | CSS is just plain CSS. CSS files defined in modules are always
230 | included when the module is required.
231 |
232 |
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 |
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 |
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 |
275 |
276 | In order to automatically include stylesheets and template partials
277 | used by other templates.
278 |
279 |
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 |
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