1 | <p align="center">
|
2 | <img width="250" src="/docs/huncwot-logo.svg">
|
3 | </p>
|
4 |
|
5 | <h1 align="center">
|
6 | <a href="https://huncwot.org">Huncwot</a>
|
7 | </h1>
|
8 |
|
9 | <p align="center">
|
10 | <b>Macro framework for monolithic JavaScript applications, batteries included</b>
|
11 | </p>
|
12 |
|
13 | <br>
|
14 |
|
15 | <p align="center">
|
16 | <a href="https://landing.mailerlite.com/webforms/landing/a3k0m1"><img src="https://img.shields.io/badge/%20newsletter%20-%20subscribe%20-blue.svg?style=for-the-badge" alt="Subscribe to Huncwot Newsletter"></a>
|
17 | <a href="https://www.npmjs.com/package/huncwot"><img src="https://img.shields.io/npm/v/huncwot.svg?style=for-the-badge" alt="Huncwot Package on NPM"></a>
|
18 | <a href="https://www.npmjs.com/package/huncwot"><img src="https://img.shields.io/npm/dm/huncwot.svg?style=for-the-badge" alt="Huncwot Package on NPM"></a>
|
19 | <a href="https://discord.gg/fdqx3BD"><img src="https://img.shields.io/badge/Discord-join%20chat-738bd7.svg?style=for-the-badge" Alt="Huncwot channel on Discord"></a>
|
20 | </a>
|
21 |
|
22 | Huncwot is a **macro framework** for **monolithic** web applications built for modern JavaScript with « batteries included » approach. It is an **integrated** solution that optimizes for programmers productivity by reducing choices and incorporating community conventions.
|
23 |
|
24 | [Website](https://huncwot.org) |
|
25 | [Twitter](http://twitter.com/huncwot)
|
26 |
|
27 | > The documentation is a bit outdated. Check the [Huncwot Example App](https://github.com/zaiste/huncwot-example-app) for inspiration and guidance.
|
28 |
|
29 | ## Table of Contents
|
30 |
|
31 | * [Features In A Nutshell](#features-in-a-nutshell)
|
32 | * [Rationale](#rationale)
|
33 | * [Getting Started](#getting-started)
|
34 | * [Usage](#usage)
|
35 | * [Server-Side](#server-side)
|
36 | * [Component-based](#component-based)
|
37 | * [Concepts](#concepts)
|
38 | * [Database](#databse)
|
39 | * [Handlers](#handlers)
|
40 | * [View](#view)
|
41 | * [Routes](#routes)
|
42 | * [Parameters](#parameters)
|
43 | * [GraphQL](#graphql)
|
44 | * [Modules](#modules)
|
45 | * [Auth](#auth)
|
46 | * [Static](#static)
|
47 | * [Examples](#examples)
|
48 |
|
49 |
|
50 | ## Features In A Nutshell
|
51 |
|
52 | Huncwot bridges client-side (*frontend*) and server-side (*backend*) development
|
53 | by using [a single programming language - JavaScript - across the
|
54 | board](https://cdb.reacttraining.com/universal-javascript-4761051b7ae9).
|
55 |
|
56 | The framework draws inspiration from Rails while trying to be less *magical*, if
|
57 | any at all. In Huncwot, you write your applications using
|
58 | [TypeScript](https://www.typescriptlang.org/). It also comes with a convenient
|
59 | command toolkit (CLI) which wraps over `npm scripts`
|
60 |
|
61 | As a secondary goal, Huncwot tries to minimize the dependencies. It uses
|
62 | external packages only if absolutely necessary (e.g. security, OS abstractions
|
63 | etc).
|
64 |
|
65 | ### :gear: Server-side / Backend
|
66 |
|
67 | * Huncwot is as a replacement for Express & Koa to build server-side applications.
|
68 | * Huncwot provides a simpler than Express/Koa, data-driven HTTP handler abstraction.
|
69 | * Huncwot comes with a built-in REST endpoint.
|
70 | * Huncwot comes with a built-in GraphQL endpoint.
|
71 | * Huncwot can collocate [GraphQL](http://graphql.org/) queries with Vue.js components.
|
72 | * Huncwot provides a data-driven router
|
73 |
|
74 | ### :bar_chart: Client-side / Frontend
|
75 |
|
76 | * ... uses [Vue.js](https://vuejs.org/)
|
77 | * ... uses [Vuex](https://vuex.vuejs.org/en/) for state management
|
78 | * ... uses [Webpack 4](https://webpack.js.org/) for bundling assets
|
79 | * ... favors class-style Vue.js components using [vue-class-component](https://github.com/vuejs/vue-class-component)
|
80 | * ... can be used as a convenient boilerplate for Vue.js to build client-side applications
|
81 | * ... uses [vue-i18n](https://github.com/kazupon/vue-i18n) for internationalization
|
82 |
|
83 | ### :closed_lock_with_key: Security
|
84 |
|
85 | * Huncwot favors [Argon2](https://en.wikipedia.org/wiki/Argon2) as a hash
|
86 | function for storing passwords over bcrypt and scrypt. bcrypt lacks memory
|
87 | hardness while in scrypt both, memory hardness and iteration count are tied to
|
88 | a single cost factor. On top of that, Argon2 won the Password Hashing
|
89 | Competition in 2015. It is build around AES ciphers, is resistant to ranking
|
90 | tradeoff attacks and more...
|
91 | * Huncwot uses the [node-argon2](https://github.com/ranisalt/node-argon2/) package
|
92 |
|
93 | ### :computer: Command Toolkit
|
94 |
|
95 | ### :minidisc: Storage
|
96 |
|
97 | * Huncwot favors plain (old?) SQL queries over ORMs as [SQL is one of the most valuable skills](http://www.craigkerstiens.com/2019/02/12/sql-most-valuable-skill/)
|
98 | * Huncwot provides a SQL-like, data-driven abstractions for the database integration
|
99 | * ... uses [Sqorn](https://sqorn.org/) for the database integration which provides a SQL-like abstractions right inside JavaScript
|
100 | * ... supports only PostgreSQL
|
101 | * ... is unwilling to support NoSQL ([Thank you for your help NoSQL, but we got it from here](https://www.memsql.com/blog/why-nosql-databases-wrong-tool-for-modern-application/))
|
102 |
|
103 | ### :cake: Conventions & Conveniences
|
104 |
|
105 | * Huncwot enforces the [Folder-By-Feature Directory Structure](#folder-by-feature-directory-structure)
|
106 |
|
107 | ## Rationale
|
108 |
|
109 | Huncwot is being built with *battery included* approach in mind, i.e. it comes with a (eventually large) library of useful modules which are developped in a coherent way. This stands in direct opposition to Koa approach. Huncwot tries to formalize conventions and eliminate valueless choices by providing solid defaults for building web applications that increase the programmers productivity.
|
110 |
|
111 | ## Getting Started
|
112 |
|
113 | Install `huncwot` globally to use its CLI commands which simplify frequent operations. You also need to install [yarn](https://yarnpkg.com/en/).
|
114 |
|
115 | ```
|
116 | npm install -g huncwot
|
117 | ```
|
118 |
|
119 | Generate new application
|
120 |
|
121 | ```
|
122 | huncwot new my-project
|
123 | cd my-project
|
124 | ```
|
125 |
|
126 | Start the application using `huncwot`, `hc` (alias) or `npm`
|
127 |
|
128 | ```
|
129 | huncwot start
|
130 | ```
|
131 |
|
132 | or
|
133 |
|
134 | ```
|
135 | hc start
|
136 | ```
|
137 |
|
138 | or
|
139 |
|
140 | ```
|
141 | npm start
|
142 | ```
|
143 |
|
144 | Visit `https://localhost:8080`
|
145 |
|
146 | ![Huncwot Init](https://raw.githubusercontent.com/zaiste/huncwot/master/docs/huncwot-start.png)
|
147 |
|
148 | ## Features In Detail
|
149 |
|
150 | ### Folder-By-Feature Directory Structure
|
151 |
|
152 | The directory structure in Huncwot is organized around your application
|
153 | **features**, and not **by type**. This means that artifacts, either client-side or
|
154 | server-side are kept togheter. In other words, this approach groups together
|
155 | entities (classes, functions) that actually work together. This leads to high
|
156 | modularity of your application and better cohesion.
|
157 |
|
158 | The *Folder-By-Feature* approach makes it easier to find files in your
|
159 | application directory. It is especially visible once your project grows -
|
160 | folder-by-feature is a better long-term approach due its scalability.
|
161 |
|
162 | To some extend, the *Folder-By-Feature* approach is similar to how recent
|
163 | frontend libraries and frameworks (React, Vue, etc) group together HTML,
|
164 | JavaScript and Stylesheets. In Huncwot, this simply goes one step further by
|
165 | applying a similar technique to the entire application so that it covers both
|
166 | frontend and backend.
|
167 |
|
168 |
|
169 | ## Usage
|
170 |
|
171 | Huncwot can be used as a replacement for Express or Koa, but it also goes beyond that by providing opinionated choices to other layers in the stack (view, ORM, etc) required to build a fully functional web application.
|
172 |
|
173 | There are two essential ways in Huncwot to constract a web application: traditional server-side or modern component-based. Nonenthless, those two approaches can be combined in any proportion.
|
174 |
|
175 | ### Server-side
|
176 |
|
177 | This is an example of a basic server-side application in Huncwot. Save it to a file e.g. `server.js`, run it with `node server.js` and visit the application `https://localhost:5544`.
|
178 |
|
179 | > *Note* Don't forget to install `huncwot` by adding it to `package.json` in your project directory followed by `npm install`. If you're starting from scratch, use `npm init` or (better) `huncwot new` described below.
|
180 |
|
181 | ```js
|
182 | const Huncwot = require('huncwot');
|
183 | const { OK } = require('huncwot/response');
|
184 |
|
185 | const app = new Huncwot();
|
186 |
|
187 | // implicit `return` with a `text/plain` response
|
188 | app.get('/', _ => 'Hello Huncwot')
|
189 |
|
190 | // explicit `return` with a 200 response of `application/json` type
|
191 | app.get('/json', _ => {
|
192 | return OK({ a: 1, b: 2 });
|
193 | })
|
194 |
|
195 | // set your own headers
|
196 | app.get('/headers', _ => {
|
197 | return { body: 'Hello B', statusCode: 201, headers: { 'Authorization': 'PASS' } }
|
198 | })
|
199 |
|
200 | // request body is parsed in `params` by default
|
201 | app.post('/bim', request => {
|
202 | return `Hello POST! ${request.params.name}`;
|
203 | })
|
204 |
|
205 | app.listen(5544);
|
206 | ```
|
207 |
|
208 | This example shows a regular, server-side application in the style of Express or Koa, e.g. you define various routes as a combination of paths and functions attached to it i.e. route handlers. In contrast to Express, Huncwot handlers only take HTTP `request` as input and always return an HTTP response: either defined explicitly as an object with `body`, `status`, etc keys, or implicitly with an inferred type e.g. `text/plain` or as a wrapping function e.g. `OK()` for `200`, or `created()` for `201`.
|
209 |
|
210 |
|
211 | ### Component-based
|
212 |
|
213 | Component-based means that *pages* are built by combining components: an independant chunks of HTML with their own styling and behaviour defined in JavaScript. There is usually only a single *page* (rendered on the server) to which components are being attached - this happens in the browser (client-side). Routing is usually performed in the browser with paths corresponding to components.
|
214 |
|
215 | ![component](https://raw.githubusercontent.com/zaiste/huncwot/master/docs/component-approach.png)
|
216 |
|
217 |
|
218 | Here's an example of a Vue.js component
|
219 |
|
220 | ```js
|
221 | <template>
|
222 | <div class="content">
|
223 | <h2>Counter</h2>
|
224 |
|
225 | <div>
|
226 | <span class="counter">{{ $store.state.count }}</span>
|
227 | <span class="notice">(count is: {{ evenOrOdd }})</span>
|
228 | </div>
|
229 |
|
230 | <a @click="increment">Increment</a>
|
231 | <a @click="decrement">Decrement</a>
|
232 | <a @click="incrementIfOdd">Increment if odd</a>
|
233 | <a @click="incrementAsync">Increment async</a>
|
234 | </div>
|
235 | </template>
|
236 |
|
237 | <script>
|
238 | import { mapGetters, mapActions } from 'vuex'
|
239 |
|
240 | export default {
|
241 | computed: mapGetters([
|
242 | 'evenOrOdd'
|
243 | ]),
|
244 | methods: mapActions([
|
245 | 'increment',
|
246 | 'decrement',
|
247 | 'incrementIfOdd',
|
248 | 'incrementAsync'
|
249 | ])
|
250 | }
|
251 | </script>
|
252 |
|
253 | <style scoped>
|
254 | .counter {
|
255 | font-size: 5rem;
|
256 | }
|
257 |
|
258 | .notice {
|
259 | color: #666;
|
260 | font-weight: 500;
|
261 | }
|
262 | </style>
|
263 | ```
|
264 |
|
265 | ## Concepts
|
266 |
|
267 | ### Database
|
268 |
|
269 | An ORM is at times too much to get data out of the database. Huncwot provides an
|
270 | thin layer of integration with various RDMBS systems using
|
271 | [Sqorn](https://sqorn.org/). Thanks to this library you can
|
272 | write usual SQL queries, yet fully integrated with the regular JavaScript data
|
273 | structures.
|
274 |
|
275 | The database configuration is stored `config/database.json` as a JSON document.
|
276 |
|
277 | In order to start using the database integration you only need to require `huncwot/db`:
|
278 |
|
279 | ```
|
280 | const db = require('huncwot/db');
|
281 | ```
|
282 |
|
283 | Let's see how we can perform some basic and frequent SQL queries in Huncwot
|
284 |
|
285 | #### Select
|
286 |
|
287 | Get all elements with all columns from `widgets` table; equivalent to `select * from widgets`:
|
288 |
|
289 | ```
|
290 | const results = await db`widgets`;
|
291 | ```
|
292 |
|
293 | Get all elements with all some columns from `widgets` table; equivalent to `select id, name from widgets`:
|
294 |
|
295 | ```
|
296 | const results = await db`widgets`.return('id', 'name');
|
297 | ```
|
298 |
|
299 | Get a single element from `widgets` table by `id`:
|
300 |
|
301 | ```
|
302 | const result = await db`widgets`.where({ id })
|
303 | ```
|
304 |
|
305 | #### Insert/Update
|
306 |
|
307 | Insert a single element into `widgets` table:
|
308 |
|
309 | ```
|
310 | await db`widgets`.insert({ name: 'Widget 1', amount: 2 })
|
311 | ```
|
312 |
|
313 | Insert few elements at once into `widgets` table:
|
314 |
|
315 | ```
|
316 | await db`widgets`.insert([
|
317 | { name: 'Widget 1', amount: 2 },
|
318 | { name: 'Widget 2', amount: 7 },
|
319 | { name: 'Widget 3', amount: 4 }
|
320 | ])
|
321 | ```
|
322 |
|
323 | Update an existing element (identified by `id`) in `widgets` table:
|
324 |
|
325 | ```
|
326 | await db`widgets`.where({ id: 2 }).set({ name: 'Widget 22' })
|
327 | ```
|
328 |
|
329 | ### Handlers
|
330 |
|
331 | A handler is a module which groups actions. Actions are functions operating in the context of a single route, i.e. actions defined in `handlers/widgets/` handle the `/widgets` route.
|
332 |
|
333 | Each action defined in a handler is responsible to connect the information received from the incoming request to underlaying data in your application (i.e. fetching/saving/updating) in order to produce a corresponding view e.g. a HTML page or a JSON payload.
|
334 |
|
335 | Handlers may define up to five action. Each action is placed in a separate file i.e. `browse`, `read`, `edit`, `add`, `delete` - in short **BREAD** (which is a kind of extension of CRUD approach). Each of those functions is triggered by a corresponding HTTP method i.e. `browse()` and `read()` by `GET`, `edit()` by `PUT`, `add()` by `POST` and finally `destroy()` by `DELETE`.
|
336 |
|
337 | Here's an example of a handler with five actions defined in `handlers/widgets/` directory.
|
338 |
|
339 | Inside `handlers/widgets/browse.js`:
|
340 |
|
341 | ```js
|
342 | const { OK } = require('huncwot/response');
|
343 |
|
344 | async function browse(request) {
|
345 | const results = ...
|
346 | return OK(results);
|
347 | }
|
348 |
|
349 | module.exports = browse
|
350 | ```
|
351 |
|
352 | Inside `handlers/widgets/read.js`
|
353 |
|
354 | ```js
|
355 | const { OK } = require('huncwot/response');
|
356 |
|
357 | async function read(request) {
|
358 | const { id } = request.params;
|
359 | const result = ...
|
360 | return OK(result);
|
361 | }
|
362 |
|
363 | module.exports = read
|
364 | ```
|
365 |
|
366 | Inside `handlers/widgets/edit.js`:
|
367 |
|
368 | ```js
|
369 | const { OK } = require('huncwot/response');
|
370 |
|
371 | async function edit(request) {
|
372 | const { id, name } = request.params;
|
373 | ...
|
374 | return OK({ status: `success: ${id} changed to ${name}` });
|
375 | }
|
376 |
|
377 | module.exports = edit
|
378 | ```
|
379 |
|
380 | Inisde `handlers/widgets/add.js`:
|
381 |
|
382 | ```js
|
383 | const { created } = require('huncwot/response');
|
384 |
|
385 | async function add(request) {
|
386 | const { name } = request.params;
|
387 | ...
|
388 | return created({ status: `success: ${name} created` });
|
389 | }
|
390 |
|
391 | module.exports = add
|
392 | ```
|
393 |
|
394 | Inside `handlers/widgets/destroy.js`:
|
395 |
|
396 | ```js
|
397 | const { OK } = require('huncwot/response');
|
398 |
|
399 | async function destroy(request) {
|
400 | const { id } = request.params;
|
401 | ...
|
402 | return OK({ status: `success: ${id} destroyed` });
|
403 | }
|
404 |
|
405 | module.exports = destroy
|
406 | ```
|
407 |
|
408 | By default, Huncwot will make those actions available under `/widgets` route through HTTP methods and in accordance to **BREAD** principle.
|
409 |
|
410 | ### View
|
411 |
|
412 | Huncwot uses [Vue.js](https://vuejs.org/) in the view layer to create and mangage components.
|
413 |
|
414 | ### Routes
|
415 |
|
416 | You can define a route using one of HTTP verbs e.g. `.get()`, `.post()`, `.put()`, `.patch()` or `.delete()` - it takes a string which defines a desired path and a function that defines a action which will be exectued once the route is hit. The action takes the incoming `request` as its parameter and returns a `response` that will be send to the client. The response is represented as a JavaScript object which must have at least `body` and `statusCode` keys. By conventions, a return of string value is considered to be a `200` response of type `plain/text` with `body` set to that string. There is also a `reply` helper function which allows to create responses with `application/json` type out of JavaScript objects.
|
417 |
|
418 | ### Parameters
|
419 |
|
420 | There are two kinds of parameters possible in a web application: the ones that are sent as part of the URL after `?`, called *query string* parameters; and the ones that are sent as part of the request `body`, referred to as POST data (usually comes from an HTML form or as JSON). Huncwot does not make any distinction between query string parameters and POST parameters, both are available in the request `params` object.
|
421 |
|
422 | ### GraphQL
|
423 |
|
424 | Huncwot uses [Apollo](http://dev.apollodata.com/) on the client and on the server. Type definitions are stored at the top level in `graphql/`. Resolvers may be grouped in modules underneath `graphql/` directory. GraphQL service endpoint is `/graphql`.
|
425 |
|
426 | Both schema and resolvers can be auto-generated with placeholder data using `demo` template of `huncwot new`:
|
427 |
|
428 | ```
|
429 | huncwot new --template demo
|
430 | ```
|
431 |
|
432 | Here's an example of a Vue.js component with a collocated GraphQL query that communicates with the built-in `/graphql` endpoint.
|
433 |
|
434 | ```js
|
435 | <template>
|
436 | <div class="content">
|
437 | <h2>Widgets</h2>
|
438 | <ul>
|
439 | <li v-for="widget in widgets">{{ widget.name }}</li>
|
440 | </ul>
|
441 | </div>
|
442 | </template>
|
443 |
|
444 | <script>
|
445 | import gql from 'graphql-tag';
|
446 |
|
447 | const query = gql`
|
448 | query {
|
449 | widgets {
|
450 | name
|
451 | }
|
452 | }
|
453 | `
|
454 |
|
455 | export default {
|
456 | data() {
|
457 | return {
|
458 | widgets: []
|
459 | }
|
460 | },
|
461 |
|
462 | apollo: {
|
463 | widgets: {
|
464 | query
|
465 | }
|
466 | }
|
467 | }
|
468 | </script>
|
469 | ```
|
470 |
|
471 | ## Modules
|
472 |
|
473 | Huncwot comes with a set of modules that enable common functionalities
|
474 |
|
475 | ### Auth
|
476 |
|
477 | ```js
|
478 | app.use(auth({ users: { 'admin': 'secret' }}))
|
479 | ```
|
480 |
|
481 | ### Static
|
482 |
|
483 | ```js
|
484 | app.use(static('./public'))
|
485 | ```
|
486 |
|
487 | ## Examples
|
488 |
|
489 | * [Huncwot Example App](https://github.com/zaiste/huncwot-example-app)
|
490 | * [ToDo with Vue.js (Vuex, Vue Router), Node.js and TypeScript](https://github.com/zaiste/huncwot-component-app)
|
491 |
|
492 | ## 3-rd Party Integrations
|
493 |
|
494 | ### [nunjucks](https://mozilla.github.io/nunjucks/) integration
|
495 |
|
496 | ```js
|
497 | const Huncwot = require('huncwot');
|
498 | const { html } = require('huncwot/response');
|
499 | const nunjucks = require('nunjucks');
|
500 |
|
501 | const app = new Huncwot();
|
502 |
|
503 | nunjucks.configure('views', { autoescape: true });
|
504 |
|
505 | app.get('/', request => {
|
506 | return html(nunjucks.render('index.html', { username: 'Zaiste' }));
|
507 | })
|
508 |
|
509 | app.listen(3000);
|
510 | ```
|
511 |
|
512 | In your project create `views/` directory with the following `index.html`
|
513 |
|
514 | ```html
|
515 | <!DOCTYPE html>
|
516 | <html>
|
517 | <head>
|
518 | <title>Nunjucks Example</title>
|
519 | </head>
|
520 | <body>
|
521 | <h1>Hello {{ username }}</h1>
|
522 | </body>
|
523 | </html>
|
524 | ```
|
525 |
|
526 | ## Roadmap
|
527 |
|
528 | Huncwot keeps track of the upcoming fixes and features on GitHub Projects: [Huncwot Roadmap](https://github.com/zaiste/huncwot/projects/1)
|
529 |
|
530 | ## Bug reports
|
531 |
|
532 | We use *Github Issues* for managing bug reports and feature requests. If you run
|
533 | into problems, please search the *issues* or submit a new one here:
|
534 | https://github.com/zaiste/huncwot/issues
|
535 |
|
536 | Detailed bug reports are always great; it's event better if you are able to
|
537 | include test cases.
|
538 |
|