├── .npmrc
├── .gitignore
├── example
├── test.txt
└── build.js
├── .npmignore
├── test
└── index.js
├── lib
├── utils.js
├── methods.js
├── document.js
├── middlewares.js
└── file-writer.js
├── package.json
├── docs
└── transforms.md
├── transforms
└── index.js
├── index.js
└── readme.md
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | public
3 |
--------------------------------------------------------------------------------
/example/test.txt:
--------------------------------------------------------------------------------
1 | test file to copy
2 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | example
2 | node_modules
3 | public
4 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | var test = require('tape')
2 |
3 | test('write tests', function (t) {
4 | t.plan(1)
5 | t.fail('to do')
6 | })
7 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | newFileStream
3 | }
4 |
5 | function newFileStream (destination, stream) {
6 | return {
7 | destination,
8 | stream
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stakit",
3 | "version": "0.0.2",
4 | "description": "A modular toolkit for building static websites",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "npm run test:unit && standard",
8 | "test:unit": "tape test/**/*.js"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "github.com/stakit/stakit"
13 | },
14 | "author": "kodedninja",
15 | "license": "ISC",
16 | "devDependencies": {
17 | "standard": "^14.0.2",
18 | "tape": "^4.11.0"
19 | },
20 | "dependencies": {
21 | "documentify": "^3.2.2",
22 | "hstream": "^1.2.0",
23 | "mkdirp": "^0.5.1",
24 | "rimraf": "^3.0.0",
25 | "through2": "^3.0.1"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/example/build.js:
--------------------------------------------------------------------------------
1 | var stakit = require('..')
2 | var { lang, collect } = require('../transforms')
3 |
4 | var content = {
5 | '/': { title: 'index' },
6 | '/about': { title: 'about' }
7 | }
8 |
9 | var kit = stakit()
10 | .use(stakit.state({ content: content }))
11 | .use(stakit.copy({
12 | [`${__dirname}/test.txt`]: 'asd/test.txt'
13 | }))
14 | .routes(function (state) {
15 | return Object.keys(state.content)
16 | })
17 | .render(function (route, state) {
18 | return {
19 | html: `
${state.content[route].title}`,
20 | state: { title: state.content[route].title }
21 | }
22 | })
23 | .transform(lang, 'en')
24 | .transform(collect, function (ctx, html) {
25 | console.log(ctx.route)
26 | console.log(html)
27 | })
28 |
29 | kit.output(stakit.writeFiles('./public'))
30 |
--------------------------------------------------------------------------------
/lib/methods.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 |
3 | module.exports = {
4 | html,
5 | use,
6 | transform
7 | }
8 |
9 | // Sets custom template html and selector
10 | function html (_ctx, html, selector) {
11 | assert(typeof html === 'string', 'stakit.html: html must be a string')
12 | assert(typeof select === 'string', 'stakit.html: selector must be a string')
13 |
14 | _ctx._html = html
15 | _ctx._selector = selector
16 | }
17 |
18 | // Pushes a middleware.
19 | function use (_ctx, fn) {
20 | assert(typeof fn === 'function', 'stakit.use: fn must be a function')
21 | _ctx._middlewares.push(fn)
22 | }
23 |
24 | // Pushes a transform to the documentify transform list
25 | function transform (_ctx, fn, opts) {
26 | assert(typeof fn === 'function', 'stakit.transform: fn must be a function')
27 | _ctx._transforms.push({
28 | fn: fn,
29 | opts: opts
30 | })
31 | }
32 |
--------------------------------------------------------------------------------
/lib/document.js:
--------------------------------------------------------------------------------
1 | var documentify = require('documentify')
2 | var hyperstream = require('hstream')
3 | var { prependToHead } = require('../transforms')
4 |
5 | module.exports = document
6 |
7 | function document (renderedHtml, context) {
8 | var d = documentify(context.html)
9 |
10 | // inject rendered content
11 | d.transform(function () {
12 | return hyperstream({
13 | [context._selector]: { _replaceHtml: renderedHtml }
14 | })
15 | })
16 |
17 | // go through all transforms
18 | context._transforms.forEach(function (t) {
19 | // pass the context to the transform generator
20 | var trans = t.fn(context)
21 | d.transform(trans, t.opts)
22 | })
23 |
24 | // set the title returned by the renderer
25 | if (context.state.title) {
26 | var title = context.state.title.trim().replace(/\n/g, '')
27 | d.transform(prependToHead(context), `${title}`)
28 | }
29 |
30 | // useful metas
31 | d.transform(prependToHead(context), `
32 |
33 |
34 | `)
35 |
36 | return d.bundle()
37 | }
38 |
--------------------------------------------------------------------------------
/lib/middlewares.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 | var fs = require('fs')
3 | var path = require('path')
4 | var writeFiles = require('./file-writer')
5 | var utils = require('./utils')
6 |
7 | module.exports = {
8 | copy,
9 | state,
10 | writeFiles
11 | }
12 |
13 | // Appends the files to the output list as streams.
14 | function copy (files) {
15 | assert(typeof files === 'object', 'stakit.copy: files must be an array or object')
16 |
17 | return function (ctx) {
18 | var fromTo = {}
19 |
20 | // resolve from and to pairs
21 | if (Array.isArray(files)) {
22 | files.forEach(function (path) {
23 | fromTo[path] = path
24 | })
25 | } else {
26 | fromTo = files
27 | }
28 |
29 | Object.keys(fromTo).forEach(function (from) {
30 | var stream = fs.createReadStream(resolve(from))
31 | ctx._files.push(utils.newFileStream(fromTo[from], stream))
32 | })
33 | }
34 | }
35 |
36 | // Middleware to help assigning values to the state
37 | function state (extendState) {
38 | assert(typeof extendState === 'object', 'stakit.state: extendState must be an object')
39 | return function (ctx) {
40 | ctx.state = Object.assign(ctx.state, extendState)
41 | }
42 | }
43 |
44 | function resolve (str) {
45 | return path.isAbsolute(str) ? str : path.resolve(str)
46 | }
47 |
--------------------------------------------------------------------------------
/lib/file-writer.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs')
2 | var rmrf = promisify(require('rimraf'))
3 | var mkdirp = require('mkdirp').sync
4 | var path = require('path')
5 | var through = require('through2')
6 |
7 | module.exports = fileWriter
8 |
9 | function fileWriter (outputPath) {
10 | var promise = reset()
11 |
12 | async function reset () {
13 | // clean and ensure
14 | await rmrf(outputPath)
15 | mkdirp(outputPath)
16 | }
17 |
18 | function write (file) {
19 | return new Promise(function (resolve) {
20 | // wait for the reset
21 | promise.then(function () {
22 | var dir = path.join(outputPath, path.dirname(file.destination))
23 | var basename = path.basename(file.destination)
24 |
25 | // ensure the directory exists
26 | mkdirp(dir)
27 |
28 | file.stream
29 | .pipe(through(write, end))
30 | .pipe(fs.createWriteStream(path.join(dir, basename)))
31 |
32 | // noop
33 | function write (chunk, enc, cb) {
34 | cb(null, chunk)
35 | }
36 |
37 | // callback on flush
38 | function end (cb) {
39 | resolve()
40 | cb()
41 | }
42 | }, onError)
43 | })
44 | }
45 |
46 | return { write }
47 | }
48 |
49 | function onError (err) {
50 | console.error(err)
51 | }
52 |
53 | // (fn) -> fn
54 | function promisify (fn) {
55 | return function (...args) {
56 | return new Promise(function (resolve, reject) {
57 | fn(...args, function (err) {
58 | if (err) {
59 | reject(err)
60 | return
61 | }
62 | resolve()
63 | })
64 | })
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/docs/transforms.md:
--------------------------------------------------------------------------------
1 | ## Built-in Transforms
2 |
3 | ```javascript
4 | var transform = require('stakit/transforms')
5 | var { lang, meta } = require('stakit/transforms')
6 | ```
7 |
8 | Stakit includes the following built-in transforms:
9 |
10 | ### `lang`
11 | `transform(lang, str)`
12 |
13 | Sets the language property of the `` element to `str`.
14 |
15 | ---
16 |
17 | ### `meta`
18 | `transform(meta, obj)`
19 |
20 | Appends `` tags to the ``.
21 |
22 | ---
23 |
24 | ### `collect`
25 | `transform(collect, fn(ctx, html))`
26 |
27 | Collects the complete HTML from the stream and passes it to `fn` along with the full context. Put it as the last transform, in order to have the correct HTML.
28 |
29 | ---
30 |
31 | ### `includeScript`
32 | `transform(includeScipt, src)`
33 |
34 | Appends a `` } })
62 | }
63 | }
64 |
65 | function includeStyle () {
66 | return function (path) {
67 | return hyperstream({ head: { _appendHtml: `` } })
68 | }
69 | }
70 |
71 | function prependToHead () {
72 | return function (str) {
73 | return hyperstream({ head: { _prependHtml: str } })
74 | }
75 | }
76 | function appendToHead () {
77 | return function (str) {
78 | return hyperstream({ head: { _appendHtml: str } })
79 | }
80 | }
81 |
82 | function prependToBody () {
83 | return function (str) {
84 | return hyperstream({ body: { _prependHtml: str } })
85 | }
86 | }
87 | function appendToBody () {
88 | return function (str) {
89 | return hyperstream({ body: { _appendHtml: str } })
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 | var path = require('path')
3 | var methods = require('./lib/methods')
4 | var middlewares = require('./lib/middlewares')
5 | var document = require('./lib/document')
6 | var utils = require('./lib/utils')
7 |
8 | module.exports = Stakit
9 |
10 | var REQUIRED_VALUES = ['_routesReducer', '_renderer']
11 |
12 | var TEMPLATE = `
13 |
14 |
15 |
16 |
17 |
18 | `
19 |
20 | function Stakit () {
21 | if (!(this instanceof Stakit)) return new Stakit()
22 |
23 | this._context = {
24 | state: {}, // state forwarded for the render method
25 | _files: [],
26 | _middlewares: [],
27 | _transforms: [],
28 | _html: TEMPLATE,
29 | _selector: 'body'
30 | }
31 |
32 | this._routesReducer = null
33 | this._renderer = null
34 | }
35 |
36 | Stakit.prototype.routes = function (reducer) {
37 | assert(typeof reducer === 'function', 'stakit.routes: reducer must be a function')
38 | this._routesReducer = reducer
39 | return this
40 | }
41 |
42 | Stakit.prototype.render = function (renderer) {
43 | assert(typeof renderer === 'function', 'stakit.render: renderer must be a function')
44 | this._renderer = renderer
45 | return this
46 | }
47 |
48 | Stakit.prototype.output = async function (writer) {
49 | writer = writer || middlewares.writeFiles(path.join(process.cwd(), 'public'))
50 |
51 | var self = this
52 | // check all required values
53 | REQUIRED_VALUES.forEach(function (key) {
54 | if (self[key] === null) {
55 | throw new Error(`stakit.output: ${key} was not set, but it's required`)
56 | }
57 | })
58 |
59 | // run through all the middlewares
60 | this._context._middlewares.forEach(function (fn) {
61 | fn(self._context)
62 | })
63 |
64 | // the state is already filled up, get the routes
65 | var routes = this._routesReducer(this._context.state)
66 |
67 | // wait until all the pages are written
68 | await Promise.all(
69 | routes.map(async function (route) {
70 | // get rendered view
71 | var view = self._renderer(route, self._context.state)
72 |
73 | // clone and update the context with the new state
74 | var context = Object.assign({}, self._context)
75 | context = Object.assign(context, {
76 | state: view.state ? Object.assign(self._context.state, view.state) : self._context.state,
77 | route: route
78 | })
79 |
80 | // documentify + handle transforms
81 | var stream = document(view.html, context)
82 |
83 | await writer.write(utils.newFileStream(path.join(route, 'index.html'), stream))
84 | })
85 | )
86 |
87 | // wait until all the files are written
88 | await Promise.all(
89 | this._context._files.map(async function (file) {
90 | await writer.write(file)
91 | })
92 | )
93 | }
94 |
95 | // dynamically add public methods
96 | Object.keys(methods).forEach(function (key) {
97 | Stakit.prototype[key] = function (...args) {
98 | methods[key](this._context, ...args)
99 | return this
100 | }
101 | })
102 |
103 | // dynamically add middlewares as static methods
104 | Object.keys(middlewares).forEach(function (key) {
105 | Stakit[key] = middlewares[key]
106 | })
107 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # stakit
2 | A modular toolkit for building static websites.
3 |
4 | **Currently in early WIP / planning state.**
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | ## Installation
14 | ```
15 | npm i stakit
16 | ```
17 |
18 | ## Example
19 | ```javascript
20 | var stakit = require('stakit')
21 | var { appendToHead } = require('stakit/transforms')
22 | var { render, hydrate } = require('@stakit/choo')
23 |
24 | var app = require('.')
25 |
26 | var kit = stakit()
27 | .routes(function (state) {
28 | return [ '/' ]
29 | })
30 | .render(render(app))
31 | .transform(hydrate)
32 |
33 | kit.output(stakit.writeFiles('./public'))
34 | ```
35 |
36 | ## Why?
37 | Generally, you do 2 things when generating a static site:
38 |
39 | - fill your **app** with some **content**
40 | - copy static **files**
41 |
42 | There are many modular (and lovely) tools for bundling Javascript or transforming CSS, Stakit is something similar, but for the full site,
43 | and especially focuses on HTML files.
44 |
45 | You'll have to handle the bundling of your app and including the bundle if that's what you need. Following [Choo](https://github.com/choojs/choo#philosophy)'s philosophy, it's small, understandable and easy to use. It was designed to work mainly with Choo, but it should work with other isomorphic frameworks too.
46 |
47 | ## Usage
48 | Stakit is called programmatically, not from the command-line, therefore you'll need a Javascript file (like `build.js`), where you require it. Afterwards you can initialize the kit with `stakit()` and then chain a couple of methods.
49 |
50 | Two methods must appear in the chain:
51 | - [`routes(fn)`](#kitroutesroutereducerstate)
52 | - [`render(fn)`](#kitrenderrendererroute-state)
53 |
54 | All other methods are optional and called in the following order:
55 |
56 | 1. all the middlewares applied by `kit.use()`
57 | 2. the applied [`routesReducer`](#kitroutesroutereducerstate) function
58 | 3. for every route:
59 | 1. a single call to the applied [`renderer`](#kitrenderrendererroute-state)
60 | 2. all `transform` calls
61 |
62 | End the chain with `kit.output()`.
63 |
64 | ## API
65 | This section provides documentation on how each function in Stakit works. It's intended to be a technical reference.
66 |
67 | ### `kit = stakit()`
68 | Initialize a new `kit` instance.
69 |
70 | ### `kit.html(template, selector)`
71 | Sets the starting HTML template and selector.
72 |
73 | ### `kit.use(fn(context))`
74 | Pushes a middleware / plugin to the middlewares list, general purpose functions ran before the route generation. You can modify the context any way you want, from altering the `state` to installing `transform`s.
75 |
76 | ```javascript
77 | kit.use(function (ctx) {
78 | ctx._transforms.push(transform)
79 | })
80 | ```
81 |
82 | See [Middlewares](#middlewares) for more information.
83 |
84 | ### `kit.routes(routeReducer(state))`
85 | The `routeReducer` is a function that gets `context.state` as a parameter and returns an `Array` of strings / routes. These are the routes on which Stakit will call `render`.
86 |
87 | ```javascript
88 | kit.routes(function (state) {
89 | return Object.keys(state.content)
90 | // or statically
91 | return [ '/', '/about', '/blog' ]
92 | })
93 | ```
94 |
95 | ### `kit.render(renderer(route, state))`
96 | Sets the renderer of the build. This is where the magic happens. The `renderer` will be called for every route returned by `routes`.
97 |
98 | It has to return an object with the following values:
99 |
100 | ```javascript
101 | {
102 | html: string, // the result of the render
103 | state: object // the state after the render (optional)
104 | }
105 | ```
106 |
107 | Transforms will receive the updated state returned here.
108 |
109 | ### `kit.transform(transformFn, opts)`
110 | Pushes a transform to the list of transforms. Stakit uses [`documentify`](https://github.com/stackhtml/documentify) and streams to build up the HTML.
111 |
112 | They're called after the rendered content has been replaced in the HTML.
113 |
114 | See [Transforms](#transforms) for more information.
115 |
116 | ### `kit.output(writerObject)`
117 | Starts the build chain and ends it with passing all the routes to `writerObject.write({ destination, stream })`. Returns a `Promise` that waits until all files (routes and static) has been completely written.
118 |
119 | By default it uses a Writer that outputs the site to the ``./public`` directory.
120 |
121 | See [Writers](#writers) for more information.
122 |
123 | ## Middlewares
124 | Built-in middlewares:
125 |
126 | ### `stakit.state(extendState)`
127 | Utility to help you with adding values to `context.state`
128 |
129 | ```javascript
130 | kit.use(stakit.state({ message: 'good morning!' }))
131 | ```
132 |
133 | ### `stakit.copy(files)`
134 | Middleware for copying files to the output directory.
135 |
136 | ```javascript
137 | // Copy files to the same location
138 | kit.use(stakit.copy([ 'robots.txt' ]))
139 |
140 | // Copy files to a different location within the output path
141 | kit.use(stakit.copy({
142 | 'robots.txt': 'robots.txt',
143 | 'sitemap.xml': 'sitemaps/sitemap.xml'
144 | }))
145 | ```
146 |
147 | ## Transforms
148 | [`Documentify`](https://github.com/stackhtml/documentify) is very powerful and can easily be modulized. The general format of a Stakit transform is:
149 |
150 | ```javascript
151 | // wrapped in a function
152 | function lang (context) {
153 | // return the documentify transform
154 | return function (lang) {
155 | // return a transform stream
156 | return hstream({ html: { lang: lang } })
157 | }
158 | }
159 | ```
160 |
161 | Note: [`hstream`](https://github.com/stackhtml/hstream) is a very good friend!
162 |
163 | The `documentify` transform is wrapped in a function, so we can get the `context` when we need it, without messing with `documentify`'s API.
164 |
165 | See what transforms come with Stakit in [`docs/transforms.md`](https://github.com/stakit/stakit/blob/master/docs/transforms.md).
166 |
167 | ## Writers
168 | Writers output the generated, transformed static files. This can vary from outputting to the file-system, to putting them into a [Dat](https://github.com/datproject/dat) archive.
169 |
170 | A writer must implement a method: `write`. For every file, including the generated pages + the files added to `context._files`, `writer.write` will be called with a file object. It should return a `Promise` that returns after the pipe was flushed (the file was completely written).
171 |
172 | A file object looks like this:
173 |
174 | ```
175 | {
176 | destination: string,
177 | stream: Stream
178 | }
179 | ```
180 |
181 | It's recommended to clean up the output directory before every build.
182 |
183 | Have a look at the built-in [`stakit.writeFiles`](https://github.com/stakit/stakit/blob/master/lib/file-writer.js) method as an example.
184 |
185 | That's all about writers.
186 |
187 | ## See Also
188 | - [jalla](https://github.com/jalljs/jalla) - Lightning fast web compiler and server in one (also thanks for a lot of code snippets!)
189 | - [documentify](https://github.com/stackhtml/documentify) - Modular HTML bundler
190 |
--------------------------------------------------------------------------------