├── .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 | Stability 8 | 9 | 10 | NPM version 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 | --------------------------------------------------------------------------------