├── 404.html ├── CNAME ├── README.md ├── index.html ├── index.js ├── make.js ├── package.json ├── posts.json ├── posts ├── csz.md ├── es-react.md ├── ijk.md ├── oceanwind.md ├── perflink.md ├── react-slack-clone.md └── servor.md └── runtime.js /404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | lukejacksonn | blog 5 | 6 | 11 | 14 | 15 | 16 | 17 |               18 |               19 |               20 |          21 | 22 | 23 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | blog.lukejacksonn.com -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ghatsby 2 | 3 | > forkable static personal blogging solution 4 | 5 | A clientside only web app with the sole purpose of indexing, fetching and rendering markdown file at runtime. A lightweight alternative to static site generators such as gatsby and next. Intended to be self hosted for free using GitHub pages. 6 | 7 | Both the content and the application code is completely static which means no lengthy build steps ever; handling hundreds of posts efforlessly. Because all compilation and linking happens in the browser at runtime, any changes are rendered almost instantly. Deployment to GitHub pages happens after `git push` with no CI required. It is even possible to edit existing posts file via the GitHub UI alone. 8 | 9 | ## Features 10 | 11 | - 🗂 Automatically generated index 12 | - 🔍 Searchable meta data and content 13 | - 🖥 Lazy loading of full render previews 14 | - ⏱ Almost instant rebuilds and deploys 15 | - 🌍 Intended to be hosted on GitHub pages 16 | 17 | ## Usage 18 | 19 | The product consists of two fundamental parts; a small node script `make.js` which generates the index for all posts, along with a lightweight clientside application `index.js` which renders the index and posts. 20 | 21 | > To get started fork then clone this repository to your local machine 22 | 23 | ### Creating a New Post 24 | 25 | This can be done by creating an new file inside of the `posts` directory. There is no restriction or convention for naming post files but note that the chosen name maps directly to the url that the post will be made available at. All posts must have the extension `.md`. 26 | 27 | For example, if a file exists in the posts directory named `first-post.md` then it will be accessible locally via the url `localhost:8080/first-post` and in production at `user.github.io/blog/first-post`. 28 | 29 | ### Generating an Index 30 | 31 | This should be done after adding or removing a post from the `posts` directory. To generate an index that includes the new file (or excludes and removed file) run the following command from the project root: 32 | 33 | ```bash 34 | node make 35 | ``` 36 | 37 | This outputs the file `post.json` which is contains meta data (like name, size and modified date) for all posts. For the UI to work properly it is important to keep the index in sync with te posts that exist. 38 | 39 | ### Running Locally 40 | 41 | This can be done trivially as both the posts and the application code themselves are static and do not require building at all. Feel free to use any local dev server that supports history API fallback (for clientside routing) or run the following command from the project root: 42 | 43 | ```bash 44 | npx servor --reload 45 | ``` 46 | 47 | By default this will start a server on `http://localhost:8080` with live reload enabled. 48 | 49 | ### Deploying the Blog 50 | 51 | This should happen after any merge to master so long as GitHub Pages has been enabled for the master branch of the repository (which can be done from the Settings tab). Given the nature if this setup it is also possible to trigger a deploy by editing and committing a change to a post directly from GitHub using the WYSIWYG editor. 52 | 53 | Given that there is no build step or continuous integration required, deploys usually happen almost immedietly and can be verified by visiting `https://.github.io/blog` and hard refreshing. 54 | 55 | ## Contributions 56 | 57 | If there is a feature missing from this setup that you would like to see implemented then feel free to create a pull request or an issue! 58 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | lukejacksonn | blog 4 | 5 | 16 | 109 | 110 | 111 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { 2 | render, 3 | useReducer, 4 | useState, 5 | useEffect, 6 | useRef, 7 | html, 8 | css, 9 | } from './runtime.js'; 10 | 11 | const getPost = (file) => 12 | fetch(`./posts/${file}.md`) 13 | .then((res) => res.text()) 14 | .then(marked); 15 | 16 | const getPosts = () => 17 | fetch('./posts.json') 18 | .then((res) => res.json()) 19 | .catch(() => ({})); 20 | 21 | const header = ({ dispatch }) => { 22 | return html` 23 |
24 | { 26 | e.preventDefault(); 27 | window.history.pushState(null, null, '/'); 28 | }} 29 | > 30 | 36 | 37 | dispatch({ searchTerm: e.target.value })} 41 | /> 42 | 43 | 47 | 48 |
49 | `; 50 | }; 51 | 52 | const linkToArticle = ({ data: [url, meta], dispatch }) => { 53 | const ref = useRef(); 54 | const [post, setPost] = useState(''); 55 | 56 | useEffect(() => { 57 | let observer = new IntersectionObserver( 58 | (container) => { 59 | if (container[0].intersectionRatio > 0.1 && post === '') 60 | getPost(url).then(setPost); 61 | }, 62 | { 63 | root: null, 64 | rootMargin: '0px', 65 | threshold: 0.1, 66 | } 67 | ); 68 | observer.observe(ref.current); 69 | return () => observer.unobserve(ref.current); 70 | }, [post]); 71 | 72 | return html` 73 | { 77 | e.preventDefault(); 78 | dispatch({ post: '' }); 79 | window.history.pushState(null, null, url); 80 | }} 81 | > 82 |
83 |
84 | `; 85 | }; 86 | 87 | const index = ({ state, dispatch }) => { 88 | const { posts, searchTerm } = state; 89 | 90 | useEffect(() => { 91 | getPosts() 92 | .then((data) => Object.entries(data)) 93 | .then((posts) => dispatch({ posts })); 94 | }, []); 95 | 96 | return html` 97 |
98 |
99 | ${posts && 100 | (posts.length === 0 101 | ? html` 102 |
103 | 109 |

110 | Add a markdown file to the posts directory and run ${' '}node make${' '} from the project root. 113 |

114 |
115 | ` 116 | : posts 117 | .filter(([k, v]) => 118 | k.toLowerCase().match(searchTerm.toLowerCase()) 119 | ) 120 | .sort(([k, v], [k1, v1]) => 121 | +new Date(v.mtime) > +new Date(v1.mtime) ? -1 : 0 122 | ) 123 | .map( 124 | (x) => 125 | html` 126 | <${linkToArticle} 127 | data=${x} 128 | key=${x[0]} 129 | dispatch=${dispatch} 130 | /> 131 | ` 132 | ))} 133 |
134 |
135 | `; 136 | }; 137 | 138 | const article = ({ state, dispatch }) => { 139 | useEffect(() => { 140 | getPost(state.route).then((post) => dispatch({ post })); 141 | }, []); 142 | 143 | return html` 144 |
145 |
146 |
147 | `; 148 | }; 149 | 150 | const style = { 151 | gettingStarted: css` 152 | width: 50ch; 153 | max-width: 100%; 154 | margin: auto; 155 | text-align: center; 156 | line-height: 150%; 157 | font-size: 1.38rem; 158 | color: rgba(0, 0, 0, 0.38); 159 | font-weight: bold; 160 | padding: 1rem; 161 | svg { 162 | width: 10ch; 163 | fill: rgba(0, 0, 0, 0.38); 164 | } 165 | code { 166 | background: rgba(0, 0, 0, 0.1); 167 | padding: 0 0.5ch; 168 | } 169 | > * + * { 170 | margin-top: 2rem; 171 | } 172 | `, 173 | header: css` 174 | display: flex; 175 | align-items: center; 176 | background: #191919; 177 | padding: 1rem; 178 | 179 | > * + * { 180 | margin-left: 1rem; 181 | } 182 | `, 183 | logo: css` 184 | width: 3.2rem; 185 | height: 3.2rem; 186 | fill: #333; 187 | transform: translateY(5%); 188 | `, 189 | avatar: css` 190 | width: 3rem; 191 | height: 3rem; 192 | border-radius: 50%; 193 | `, 194 | searchInput: css` 195 | background: #111; 196 | display: block; 197 | font-size: 1.2rem; 198 | padding: 1rem; 199 | border: 1px solid #222; 200 | border-radius: 1rem; 201 | flex: 1 1 100%; 202 | color: rgba(255, 255, 255, 0.8); 203 | min-width: 0; 204 | margin-right: auto; 205 | max-width: 20rem; 206 | `, 207 | index: css` 208 | display: flex; 209 | width: 100%; 210 | height: 100%; 211 | overflow: hidden; 212 | overflow-x: scroll; 213 | -webkit-overflow-scrolling: touch; 214 | flex: 0 1 100%; 215 | 216 | > div { 217 | padding: 2rem; 218 | display: flex; 219 | height: 100%; 220 | } 221 | > div > * + * { 222 | margin-left: 2rem; 223 | } 224 | > div > a { 225 | display: block; 226 | flex: none; 227 | width: 22rem; 228 | position: relative; 229 | text-decoration: none; 230 | height: 100%; 231 | border: 0; 232 | box-shadow: 0 0 1rem rgba(0, 0, 0, 0.2); 233 | opacity: 0.8; 234 | transition: transform 0.3s; 235 | border-radius: 1rem; 236 | overflow: hidden; 237 | font-size: 0.8rem; 238 | padding: 2rem 2rem 4rem; 239 | 240 | p { 241 | line-height: 162%; 242 | } 243 | border: 2px solid #333; 244 | &::before { 245 | content: ''; 246 | position: absolute; 247 | top: 0; 248 | left: 0; 249 | width: 100%; 250 | height: 100%; 251 | z-index: 1; 252 | } 253 | &:hover { 254 | opacity: 1; 255 | transform: scale(1.038); 256 | box-shadow: 0 0 2rem rgba(0, 0, 0, 0.2); 257 | } 258 | } 259 | `, 260 | post: css` 261 | width: 100%; 262 | margin: 0 auto; 263 | padding: 2rem 2rem 4rem; 264 | overflow-y: auto; 265 | height: 100%; 266 | flex: 0 1 100%; 267 | -webkit-overflow-scrolling: touch; 268 | 269 | @media (min-width: 110ch) { 270 | padding: 5vw 2rem 4rem; 271 | } 272 | `, 273 | article: css` 274 | max-width: 80ch; 275 | color: #fff; 276 | line-height: 2; 277 | word-wrap: break-word; 278 | width: 100%; 279 | margin: 0 auto; 280 | color: rgba(255, 255, 255, 0.8); 281 | 282 | > * + * { 283 | margin-top: 2em; 284 | } 285 | 286 | hr { 287 | opacity: 0.38; 288 | } 289 | 290 | li { 291 | list-style: disc; 292 | list-style-position: inside; 293 | } 294 | 295 | h1, 296 | h2, 297 | h3, 298 | h4, 299 | strong { 300 | font-weight: bold; 301 | } 302 | 303 | h1 { 304 | text-align: left; 305 | font-size: 2em; 306 | border-bottom: 1px solid rgba(255, 255, 255, 0.2); 307 | padding-bottom: 0.62em; 308 | line-height: 1.38; 309 | } 310 | 311 | h2 { 312 | font-size: 1.62em; 313 | border-bottom: 1px solid rgba(255, 255, 255, 0.2); 314 | padding-bottom: 0.62em; 315 | margin-bottom: 1em; 316 | } 317 | 318 | h3 { 319 | font-size: 1em; 320 | border-bottom: 1px solid rgba(255, 255, 255, 0.2); 321 | padding-bottom: 0.62em; 322 | margin-bottom: 1em; 323 | } 324 | 325 | img { 326 | display: block; 327 | width: 100%; 328 | max-width: 100%; 329 | } 330 | 331 | pre { 332 | background: rgba(0, 0, 0, 0.2); 333 | padding: 1em; 334 | border-radius: 0.38em; 335 | overflow-x: scroll; 336 | } 337 | 338 | a { 339 | display: inline-block; 340 | color: #aaa; 341 | margin-top: 0; 342 | } 343 | 344 | table { 345 | border-collapse: collapse; 346 | color: inherit; 347 | } 348 | 349 | td, 350 | th, 351 | tr { 352 | border: 1px solid rgba(255, 255, 255, 0.38); 353 | padding: 0.62em; 354 | } 355 | 356 | blockquote { 357 | border-left: 2px solid rgba(255, 255, 255, 0.38); 358 | padding: 0.38em 1em; 359 | font-style: italic; 360 | } 361 | 362 | :not(pre) > code { 363 | padding: 0 0.38em; 364 | background: #333; 365 | } 366 | `, 367 | }; 368 | 369 | const reducer = (state, update) => ({ 370 | ...state, 371 | ...(typeof update === 'function' ? update(state) : update), 372 | }); 373 | 374 | const initialState = { 375 | route: null, 376 | posts: null, 377 | post: '', 378 | searchTerm: '', 379 | }; 380 | 381 | const app = () => { 382 | const [state, dispatch] = useReducer(reducer, initialState); 383 | 384 | useEffect(() => { 385 | const updateRoute = () => 386 | dispatch(() => ({ 387 | route: location.pathname, 388 | })); 389 | addEventListener('popstate', updateRoute); 390 | const pushState = window.history.pushState; 391 | window.history.pushState = function () { 392 | pushState.apply(history, arguments); 393 | updateRoute(); 394 | }; 395 | updateRoute(); 396 | }, []); 397 | 398 | return ( 399 | state.route && 400 | html` 401 | <${header} state=${state} dispatch=${dispatch} /> 402 | <${state.route === '/' || state.searchTerm ? index : article} 403 | state=${state} 404 | dispatch=${dispatch} 405 | /> 406 | ` 407 | ); 408 | }; 409 | 410 | render(html`<${app} />`, document.body); 411 | -------------------------------------------------------------------------------- /make.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const details = (file) => ({ 4 | size: fs.readFileSync(`./posts/${file}`).length, 5 | mtime: fs.statSync(`./posts/${file}`).mtime, 6 | }); 7 | 8 | const posts = fs 9 | .readdirSync('./posts') 10 | .reduce((a, b) => ({ ...a, [b.replace('.md', '')]: details(b) }), {}); 11 | 12 | fs.writeFileSync('./posts.json', JSON.stringify(posts)); 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "npm run build && npx servor --reload --browse --editor", 4 | "build": "node make.js" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /posts.json: -------------------------------------------------------------------------------- 1 | {"csz":{"size":4683,"mtime":"2023-06-06T11:07:03.713Z"},"es-react":{"size":4366,"mtime":"2023-06-06T11:07:03.713Z"},"ijk":{"size":3965,"mtime":"2023-06-06T11:07:03.713Z"},"oceanwind":{"size":37949,"mtime":"2023-06-06T11:07:03.713Z"},"perflink":{"size":3965,"mtime":"2023-06-06T11:07:03.713Z"},"react-slack-clone":{"size":3321,"mtime":"2023-06-06T11:07:03.713Z"},"servor":{"size":4988,"mtime":"2023-06-06T11:07:03.713Z"}} -------------------------------------------------------------------------------- /posts/csz.md: -------------------------------------------------------------------------------- 1 | # csz 2 | 3 | > Runtime CSS modules with SASS like preprocessing 4 | 5 | A framework agnostic css-in-js solution that uses [stylis](https://github.com/thysultan/stylis.js) to parse styles from tagged template literals and append them to the head of the document at runtime. Loading in stylesheets dynamically – from .css files – is supported out of the box, so you can write your styles in `.css` files and import them via url without having to worry about flashes of unstyled content. 6 | 7 | ## Features 8 | 9 | - Efficient caching of styles 10 | - Import styles from regular `.css` files 11 | - Available as an ES module (from [unpkg.com](https://unpkg.com/csz)) 12 | - Styles scoped under unique namespaces `.csz-lur7p80ssnq` 13 | - Global style injection `:global(selector)` 14 | - Nested selectors `a { &:hover {} }` 15 | - Vendor prefixing `-moz-placeholder` 16 | - Flat stylesheets `color: red; h1 { color: red; }` 17 | - Minification of appended styles 18 | - Keyframe and animation namespacing 19 | 20 | ## Usage 21 | 22 | The package is designed to be used as an ES module. You can import it directly from [unpkg.com](https://unpkg.com/csz/): 23 | 24 | ```js 25 | import css from 'https://unpkg.com/csz'; 26 | 27 | // static 28 | const inlined = css` 29 | background: blue; 30 | `; // generate class name for ruleset 31 | 32 | // dynamic (from stylesheet) 33 | const relative = css`/index.css`; // generate class name for file contents 34 | const absolute = css` 35 | https: ; //example.com/index.css 36 | `; // generate class name for file contents 37 | ``` 38 | 39 | Both variations (static and dynamic) are sync and return a string in a format similar to `csz-b60d61b8`. If a ruleset is provided as a string then it is processed immediately but if a filepath is provided then processing is deferred until the contents of the file has been fetched and parsed. 40 | 41 | > **NOTE:** File paths starting with `/` must be relative to the current hostname, so if you are running your app on `example.com` and require `/styles/index.css` then csz will try fetch it from `example.com/styles/index.css`. 42 | 43 | Styles imported from a file are inevitably going to take some amount of time to download. Whilst the stylesheet is being downloaded a temporary ruleset is applied to the element which hides it (using `display: none`) until the fetched files have been processed. This was implemented to prevent flashes of unstyled content. 44 | 45 | See below for an example of what a raw ruleset might look like and how it looks like after processing. 46 | 47 |
48 | Example stylesheet (unprocessed) 49 | 50 | ```scss 51 | font-size: 2em; 52 | 53 | // line comments 54 | /_ block comments _/ 55 | 56 | :global(body) {background:red} 57 | 58 | h1 { 59 | h2 { 60 | h3 { 61 | content:'nesting' 62 | } 63 | } 64 | } 65 | 66 | @media (max-width: 600px) { 67 | & {display:none} 68 | } 69 | 70 | &:before { 71 | animation: slide 3s ease infinite 72 | } 73 | 74 | @keyframes slide { 75 | from { opacity: 0} 76 | to { opacity: 1} 77 | } 78 | 79 | & { 80 | display: flex 81 | } 82 | 83 | &::placeholder { 84 | color:red 85 | } 86 | 87 | ```` 88 | 89 |
90 | 91 |
92 | Example stylesheet (processed) 93 | 94 | ```scss 95 | .csz-a4B7ccH9 {font-size: 2em;} 96 | 97 | body {background:red} 98 | h1 h2 h3 {content: 'nesting'} 99 | 100 | @media (max-width: 600px) { 101 | .csz-a4B7ccH9 {display:none} 102 | } 103 | 104 | .csz-a4B7ccH9:before { 105 | -webkit-animation: slide-id 3s ease infinite; 106 | animation: slide-id 3s ease infinite; 107 | } 108 | 109 | 110 | @-webkit-keyframes slide-id { 111 | from { opacity: 0} 112 | to { opacity: 1} 113 | } 114 | @keyframes slide-id { 115 | from { opacity: 0} 116 | to { opacity: 1} 117 | } 118 | 119 | .csz-a4B7ccH9 { 120 | display:-webkit-box; 121 | display:-webkit-flex; 122 | display:-ms-flexbox; 123 | display:flex; 124 | } 125 | 126 | .csz-a4B7ccH9::-webkit-input-placeholder {color:red;} 127 | .csz-a4B7ccH9::-moz-placeholder {color:red;} 128 | .csz-a4B7ccH9:-ms-input-placeholder {color:red;} 129 | .csz-a4B7ccH9::placeholder {color:red;} 130 | ```` 131 | 132 |
133 | 134 | ## Example 135 | 136 | This library is framework agnostic but here is a contrived example of how you can style a React component conditionally based upon some state; demonstrating switching between static and dynamic styles on the fly. 137 | 138 | ```jsx 139 | import css from 'https://unpkg.com/csz'; 140 | 141 | export default () => { 142 | const [toggle, setToggle] = React.useState(false); 143 | return ( 144 |
153 |

Hello World!

154 | 155 |
156 | ); 157 | }; 158 | ``` 159 | 160 | ## Implementation 161 | 162 | I was inspired by [emotion](https://github.com/emotion-js/emotion) and [styled-components](https://github.com/styled-components/styled-components) but unfortunately neither of these packages expose an es module compatible build and come with quite a lot of extraneous functionality that isn't required when the scope of the project is restricted to runtime only class name generation and ruleset isolation. 163 | -------------------------------------------------------------------------------- /posts/es-react.md: -------------------------------------------------------------------------------- 1 | # es-react 2 | 3 | > An ES6 module exposing the latest version of react, react-dom, react-is, and prop-types 4 | 5 | Ever wanted to just import react into your project as a module **without** a build step or even script tags? It is 2019 now and native browser support for module [imports](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) is [pretty good](https://caniuse.com/#feat=es6-module) so this should be possible if we so wish! Alas, there has not been an ES6 module compatible build released yet. 6 | 7 | This package allows you import `react` and `react-dom` as ES6 modules from a CDN like [`unpkg`](https://unpkg.com): 8 | 9 | ```js 10 | import { 11 | React, 12 | ReactDOM, 13 | ReactIs, 14 | PropTypes 15 | } from 'https://unpkg.com/es-react'; 16 | 17 | ReactDOM.render( 18 | React.createElement('h1', {}, 'Hello from es-react'), 19 | document.body 20 | ); 21 | ``` 22 | 23 | By default es-react exports the production build of react. For the development build use the `/dev` subfolder: 24 | 25 | ```js 26 | import { React, ReactDOM } from 'https://unpkg.com/es-react/dev'; 27 | ``` 28 | 29 | You may also import any members of the React package directly: 30 | 31 | ```js 32 | import React, { 33 | Component, 34 | useState /* ... */ 35 | } from 'https://unpkg.com/es-react'; 36 | ``` 37 | 38 | And every package is also being provided as a separate file: 39 | 40 | - `es-react/index.js`: Exports all of `React` and exports `{ React, ReactDOM, ReactIs, PropTypes }` 41 | - `es-react/react.js`: Exports all of `React` plus a default export 42 | - `es-react/react-dom.js`: Exports all of `ReactDOM` plus a default export (but not `react-dom/server`) 43 | - `es-react/react-is.js`: Exports all of `ReactIs` plus a default export 44 | - `es-react/prop-types.js`: Exports all of `PropTypes` plus a default export 45 | 46 | All development-versions of these packages are also available under `es-react/dev/`. 47 | 48 | ## Features 49 | 50 | - All the latest React features (hooks, suspense, lazy, memo etc.) 51 | - Use React directly from any javascript file (no build step required) 52 | - Compatible with [`htm`](https://github.com/developit/htm) (for JSX compilation at runtime) 53 | 54 | ## Usage 55 | 56 | Import `React` and `ReactDOM` directly from any script with `type="module"`. The package is intended to be available from [`unpkg`](https://unpkg.com) (without having to append `?module` to the package name). 57 | 58 | ```js 59 | import { React, ReactDOM } from 'https://unpkg.com/es-react@16.12.0'; 60 | ``` 61 | 62 | It is strongly advised that you specify a version when requesting the module – this speeds up the request time and helps with caching. If you don't specify a number then unpkg will redirect and serve up the latest available version. 63 | 64 | ## Example 65 | 66 | Create a new file, copy the code below into it and then open the file in a browser – or [try online](https://codepen.io/lukejacksonn/pen/EMxVWM). 67 | 68 | > If you would like the browser to reload when you update the code, then you can use a dev server like [servor](https://github.com/lukejacksonn/servor) dependency free by running `npx servor .`. 69 | 70 | ```js 71 | 97 | ``` 98 | 99 | ## Implementation 100 | 101 | The latest versions of all packages are installed via (pinned) entries in `package.json` and built and bundled using Rollup with automatic code splitting. 102 | 103 | The exports of each package are automatically expanded and `object-assign` is stripped from the output, since all browsers that support ESM will also support `Object.assign` 104 | (See `scripts/expand-exports-plugin.js` and `scripts/replace-object-assign.js` for the Babel plugins that do this) 105 | 106 | ## Acknowledgements 107 | 108 | Barely any of the code in this repo is written by myself. It is just a wrapper for React that is written and maintained by the team at Facebook. Thanks to my employer [Formidable](https://github.com/formidablelabs) for allowing me the time to think about and work on fun and experimental projects like this. 109 | -------------------------------------------------------------------------------- /posts/ijk.md: -------------------------------------------------------------------------------- 1 | # ijk 2 | 3 | > Transforms arrays into virtual DOM trees 4 | 5 | [![Build Status](https://travis-ci.org/lukejacksonn/ijk.svg?branch=master)](https://travis-ci.org/lukejacksonn/ijk) 6 | [![codecov](https://codecov.io/gh/lukejacksonn/ijk/branch/master/graph/badge.svg)](https://codecov.io/gh/lukejacksonn/ijk) 7 | 8 | Find `h` a bit repetitive? Not a huge fan of JSX? Love LISP? Code as data and data as code? 9 | 10 | This is a tiny recursive factory function that allows you to write terse, declarative representations of virtual DOM trees. It does not try mimic HTML or JSON syntax but instead a series of nested arrays to represent user interfaces. 11 | 12 | ```js 13 | const tree = h( 14 | 'x', 15 | 'y', 16 | 'z' 17 | )([ 18 | 'main', 19 | [ 20 | ['h1', 'Hello World'], 21 | ['input', { type: 'range' }], 22 | ['button', { onclick: console.log }, 'Log Event'] 23 | ] 24 | ]); 25 | ``` 26 | 27 | The above call to `h` returns a virtual DOM tree with named attributes that respect the provided schema. Expected output here, would be of the shape `{ x: 'main', y: {}, z: [...] }`. A tree like this can be passed as a node to patch, diff and render algorithms exposed by libraries like [Hyperapp](https://github.com/hyperapp/hyperapp), [Ultradom](https://github.com/jorgebucaran/ultradom) or [Preact](https://github.com/developit/preact). 28 | 29 | ### Schemas 30 | 31 | - **Hyperapp** / **Ultradom** / **Preact:** `h('nodeName','attributes','children')` 32 | 33 | ## Signature 34 | 35 | A call to `h(x,y,z)` returns a build function that expects a node of type `[0,1,2]` where: 36 | 37 | - Index `0` contains a `string` used as the elements tag name (required) 38 | - Index `1` contains an `object` containing element attributes (optional) 39 | - Index `2` contains an `string|array` of content or children (optional) 40 | 41 | Children are flattened and falsey children are excluded. Numbers passed as children get converted to strings. 42 | 43 | ## Installation 44 | 45 | ```bash 46 | npm i ijk 47 | ``` 48 | 49 | ## Usage 50 | 51 | Here is a demo with [Hyperapp](https://codepen.io/lukejacksonn/pen/BJvXvg?editors=0010) and [Preact](https://codepen.io/lukejacksonn/pen/ZvwKva?editors=0010). 52 | 53 | ```js 54 | import { h } from 'ijk'; 55 | 56 | const tree = h( 57 | 'nodeName', 58 | 'attributes', 59 | 'children' 60 | )([ 61 | 'main', 62 | [ 63 | ['h1', 'Hello World'], 64 | ['input', { type: 'range' }], 65 | ['button', { onclick: console.log }, 'Log Event'], 66 | [ 67 | 'ul', 68 | [ 69 | ['li', 1], 70 | ['li', 2], 71 | ['li', 3] 72 | ] 73 | ], 74 | false && ['span', 'Hidden'] 75 | ] 76 | ]); 77 | ``` 78 | 79 | ## Comparison 80 | 81 | ijk is essentially `h` but with optional props and you only have to call `h` once; not every time you want to represent an element in the DOM. This generally means less repetition and one less import in your view files. 82 | 83 | ```js 84 | const h = h('main', {}, [ 85 | h('h1', {}, 'Hello World'), 86 | h('input', { type: 'range' }), 87 | h('button', { onclick: console.log }, 'Log Event'), 88 | h('ul', {}, [h('li', {}, 1), h('li', {}, 2), h('li', {}, 3)]), 89 | false && h('span', {}, 'Hidden') 90 | ]); 91 | ``` 92 | 93 | The main advantages over using JSX is less repetition of tag names and no build step is required. 94 | 95 | ```jsx 96 | const jsx = ( 97 |
98 |

Hello World

99 | 100 | 101 | 106 | {false && 'Hidden'} 107 |
108 | ); 109 | ``` 110 | 111 | ## Advanced 112 | 113 | Here is an example that takes advantage of most features and demonstrates components. 114 | 115 | ```js 116 | import { h } from 'ijk'; 117 | 118 | const Item = data => ['li', data]; 119 | const Article = ({ title, story, related }) => [ 120 | 'article', 121 | [['h2', title], ['hr'], ['p', story], related.map(Item)] 122 | ]; 123 | 124 | const Main = [ 125 | 'main', 126 | [ 127 | ['h1', 'Hello World'], 128 | ['input', { type: 'range' }], 129 | [ 130 | 'ul', 131 | [ 132 | ['li', 1], 133 | ['li', 2], 134 | ['li', 3] 135 | ] 136 | ], 137 | ['button', { onclick: console.log }, 'Log Event'], 138 | false && ['span', 'Hidden'], 139 | Article({ 140 | title: 'Some News', 141 | story: 'lorem ipsum dolor sit amet', 142 | related: [4, 5] 143 | }) 144 | ] 145 | ]; 146 | 147 | const tree = h('nodeName', 'attributes', 'children')(Main); 148 | ``` 149 | -------------------------------------------------------------------------------- /posts/oceanwind.md: -------------------------------------------------------------------------------- 1 | # Tailwind the switch statement 2 | 3 | > How and why I wrote a library that converts tailwind shorthand into css at runtime 4 | 5 | For the benefit of you that do not know already, [Tailwind](https://tailwindcss.com) is a utility-first CSS framework built for rapidly building custom UI on the web. It is very similar to the once popular and somewhat ubiquitous Bootstrap framework but with a much more functional twist. 6 | 7 | 8 | 9 | > ⚡️ Check out the [live and interactive demo](https://esm.codes/#Ly8gT2NlYW53aW5kIGRlbW8gYnkgQGx1a2VqYWNrc29ubgovLyAtLS0tLS0tLS0tLS0tLS0tCiAgICAKaW1wb3J0IHsgcmVuZGVyLCBoIH0gZnJvbSAnaHR0cHM6Ly91bnBrZy5jb20vcHJlYWN0P21vZHVsZSc7CmltcG9ydCBodG0gZnJvbSAnaHR0cHM6Ly91bnBrZy5jb20vaHRtP21vZHVsZSc7CmltcG9ydCBvdyBmcm9tICdodHRwczovL3VucGtnLmNvbS9vY2VhbndpbmQnOwoKY29uc3QgaHRtbCA9IGh0bS5iaW5kKGgpOwoKcmVuZGVyKAogIGh0bWxgCiAgICA8ZGl2IGNsYXNzTmFtZT0ke293YAogICAgICBoLWZ1bGwKICAgICAgYmctcHVycGxlLTUwMAogICAgICBmbGV4CiAgICAgIGl0ZW1zLWNlbnRlcgogICAgICBqdXN0aWZ5LWNlbnRlcgogICAgYH0+CiAgICAgIDxoMSBjbGFzc05hbWU9JHtvd2AKICAgICAgICB0ZXh0LXdoaXRlCiAgICAgICAgZm9udC1ib2xkCiAgICAgICAgZm9udC1zYW5zCiAgICAgICAgaG92ZXI6cm90YXRlLTMKICAgICAgICBob3ZlcjpzY2FsZS0xNTAKICAgICAgICBob3ZlcjpjdXJzb3ItcG9pbnRlcgogICAgICBgfT5IZWxsbyBXb3JsZDwvaDE+CiAgICA8L2Rpdj4KICBgLAogIGRvY3VtZW50LmJvZHkKKTs=) 10 | 11 | This is a post about how I went about making [Oceanwind](https://github.com/lukejacksonn/oceanwind); my very own runtime implementation of Tailwind. If you are looking more for documentation rather than a story then please stop right here and go [checkout the README](https://github.com/lukejacksonn/oceanwind). 12 | 13 | ## What is atomic CSS exactly 14 | 15 | Tailwind (like [Tachyons](https://tachyons.io) before it) takes advantage of _atomic styles_. An approach that is becoming more and more talked about lately. The idea, generally, is that instead of using class names like `btn-primary` which might add a multitude of style rules to a given element, we'd use more granular class names like, for example `p-10 bg-blue border-1 font-bold` which are often more self explanitory and usually map to a single CSS rule. 16 | 17 | There are many well-written articles out there that define this philosophy. They go into more depth explaining the pros and cons of atomic CSS and/or compare it to various other approaches like BEM for example; see [In Defense of Utility-First CSS](https://frontstuff.io/in-defense-of-utility-first-css), [CSS Utility Classes and "Separation of Concerns"](https://adamwathan.me/css-utility-classes-and-separation-of-concerns) and [A year of Utility Classes](https://css-irl.info/a-year-of-utility-classes). I highly recommend reading some of these resources for a bit of context here! 18 | 19 | Like most things, some people love the idea of atomic css, others hate it. So I will not be advocating for or against it here. What I will be explaning, is how I went about flipping everything on its head and developed – from the top down – a library that mitigates what I think are the biggest drawbacks of the current Tailwind implementation, whilst maintaining the same wonderfully thought out API. 20 | 21 | ## A little knowledge is a dangerous thing 22 | 23 | If you have ever checked out any of [my repositories on GitHub](https://github.com/lukejacksonn) or read any of my previous articles, you will know that I am an advocate of "simple". If something isn't easy for me to grasp, remix or maintain then I will usually side step it completely, or try solve the problem myself in order to better my understanding of a specific domain. 24 | 25 | Luckily for me the Tailwind API is amazingly simple to grasp. As mentioned previously, it is essentially a one-to-one mapping of some custom shorthand syntax to CSS rules. It is a language abstraction. All you have to do to be _good_ at Tailwind, much like CSS, is to learn the vocabulary. Colour me interested. 26 | 27 | So I started digging around looking for the easiest way to integrate Tailwind into my project. The quick start guide suggests the following steps: 28 | 29 | 1. Install Tailwind via npm 30 | 2. Add Tailwind to your CSS 31 | 3. Create your Tailwind config file 32 | 4. Process your CSS with Tailwind 33 | 34 | This all sounded pretty straight forward.. except for the last step. Why do I need to process my CSS after using Tailwind? I thought it was just a collection of useful class names! 35 | 36 | It turns out that the recommended way of adding all the class names you might need in your project, is to use custome css directives which look something like this: 37 | 38 | ```css 39 | @tailwind base; 40 | @tailwind components; 41 | @tailwind utilities; 42 | ``` 43 | 44 | Tailwind will swap these directives out at build time with all of its generated CSS. As you might have noticed, this is not _normal_ or valid CSS. Which is why step 4 in the getting started guide is required. For most people this is probably not a big deal, but you see, I [don't use a build step](https://formidable.com/blog/2019/no-build-step) in the majority of my projects these days. This realisation suddenly made Tailwind a non-starter for me! 45 | 46 | I was pretty gutted but continued scrolling down the getting started document when I saw the title **Using Tailwind via CDN**. Now, I'm a big fan of CDNs, they are fast and they are simple. I got all excited again. Apparently all it actually takes to add all the tailwind class names to your project, is this line of HTML: 47 | 48 | ```html 49 | 53 | ``` 54 | 55 | This actually works great and ended up inspiring one developer to make a [PR to czs](https://github.com/lukejacksonn/csz/pull/9) (a small CSS-in-JS library I made a while back) which allows you to import CSS files from absolute URLs like this, from within JS modules. I was now up and running with Tailwind in my project! 56 | 57 | OK. So why'd you go and rewrite a perfectly good stylesheet.. in JavaScript? Well, using the CDN version of Tailwind comes with a few downsides. these are outlined quite clearly in their documentation: 58 | 59 | - You can't customize Tailwind's default theme 60 | - You can't use any directives like @apply, @variants, etc. 61 | - You can't enable features like group-hover 62 | - You can't install third-party plugins 63 | - You can't tree-shake unused styles 64 | 65 | Ohh no! The lack of these features really takes the wind out the Tailwind sails. Not only that but the CDN build is large. Around 348kb of raw CSS. Admittedly it compresses pretty well (down to 27kb) as it consists of _a lot_ of repetition which gzip and brotli love. But this is still quite a hefty dependency to be adding to a brand new project, especially considering we don't get any of the more powerful features like variants (which allow us to scope styles under responsive and pseudo selectors) and we haven't actually styled anything yet! 66 | 67 | ## A Program to Utilize & Reduce Gross Excesses 68 | 69 | So what's the craic? Why are Tailwind setting such a high baseline here? Well, this is mainly down to the optimize through purgation approach that has been taken in the library design. To use all of Tailwind, first you need to generate all of Tailwind. By that I mean, for the class `bg-white` to work then the class `.bg-white { background: #fff; }` must be defined. Tailwind doesn't know what class names you are going to use ahead of time, so it has to assume you want to use them all, which means it has to generate every possible permutation of every directive and variant. 70 | 71 | When you start to work out about all the possible combinations of all class names and variants then you end up with some very big numbers. Imagine for example the `bg-` directive. It is suffixed by a color (like `white` in the example above) and there are 10 hues in the Tailwind base theme. Whatsmore there are 9 shades of each hue. That means there are `10 * 9` rules that need to be generated in order to cover all possible permutations of the `bg-` directive. There are more directives like this (that accept a color after them), like `text-` and `border-` for example, which, by the same logic as `bg-` will require 90 rules each to be generated. Wowzer. 72 | 73 | If you are sat there thinking, well that isn't _so_ bad. Then lets talk about variants. Variants like `sm:` which essentially wraps a directive in a media query. So `sm:bg-white` translates to `@media (min-width: 640px) { .sm\:bg-white { background: white; } }` for example. There are 4 such responsive variants like this (`sm`, `md`, `lg`, `xl`). So you can probably see where this is going by now, this means that to cover all possible permutations of the `bg-` directive for example, we now need to generate `10 * 9 * 4` rules. That's 360 rules for one directive, then 360 more for `text-` and again for `border-` which is 1080 rules for 3 directives over 4 responsive variants! If this is starting to sound like a lot by now, wait until you hear about the pseudo variants like `:hover`, `:disabled`, `:active` etc. of which there are currently 16 documented. That's a bug number. 74 | 75 | For those **not so** mathematically inclined like myself, the result of such a mega combinatory explosion becomes apparent when using the development build of Tailwind: 76 | 77 | > Using the default configuration, the development build of Tailwind CSS is 2365.4kB uncompressed, 185.0kB minified and compressed with Gzip, and 44.6kB when compressed with Brotli. 78 | 79 | That is **2.3 megabytes** of CSS.. most of which you probably won't use! How are you supposed to get all this output down to just what you need? Well, as outlined in the [controlling file size](https://tailwindcss.com/docs/controlling-file-size) guide: 80 | 81 | > When building for production, you should always use Tailwind's purge option to tree-shake unused styles and optimize your final build size. When removing unused styles with Tailwind, it's very hard to end up with more than 10kb of compressed CSS. 82 | 83 | That sounds more like it! Less than 10kb sounds like a very reasonable amount of CSS. So, long story short; Tailwind employs [PurgeCSS](https://purgecss.com) which scans your project files looking for any string matching the regular expression `/[^<>"'`\s]\*[^<>"'`\s:]/g` then looks through any CSS files removing any styles that were never used. This approach has been around for a while and is not novel to Tailwind but it obviously works quite well for them. The downside here, in my opinion, is that: 84 | 85 | - It is another step required to get everything working efficiently 86 | - It requires tentative and quite involved configuration to get right 87 | - It doesn't _just work_ work at runtime (obviously) 88 | - It is non-deterministic and quite error prone 89 | 90 | All things considered, suddenly Tailwind was looking like a non-starter for me again; especially for an application that was going to make it beyond development and into production without a build step. 91 | 92 | ## Can't we have our cake and eat it? 93 | 94 | After the realisation that you have to choose between a build step or masses of redundancy I took a step back from the idea. I had this niggling feeling that there was surely a better way to get what I wanted. So what did I actually want? Well, put simply, I needed a function that, when given a term like `bg-white` returns me `background-color: white`, when given `mt-1` it should return `margin-top: 0.25rem` as specified in the Tailwind API. 95 | 96 | This got me thinking, could I make a regular expression then use string replace to turn `mt` into `margin-top` and `bg` into `background` and so on? It seemed trivial enough, so I started implementing such a function. I soon realised that with all the directives in the API, and with all the variants, that a regex would quickly become unweildly (not to mention slow)... back to the drawing board! 97 | 98 | My next approach was to create a dictionary type structure, in the form of an object (which in python is aptly call a dictionary). The idea here was to split the key from the value for each shorthand then lookup the key and execute a transform function which gets passed the value. For example: 99 | 100 | ```js 101 | const dictionary = { 102 | bg: val => ({ background: val }), 103 | mt: val => ({ margin-top: theme.unit[val] }), 104 | ... 105 | } 106 | ``` 107 | 108 | It became apparent that I would need as well, a theme file. This file would consist of some names constants that could be used by differet directives; for example things like font sizes, widths, heights and colors. Tailwind already have a notion of a config file like this, so I pieced together from the documentation, a JSON object with every possible property in there. 109 | 110 | This worked pretty well actually, at least for a start. The simple cases were simple but because this approach was founded on the quite a naieve assumption that each shorthand directive adhered to the `key-value` grammar, it started to get a bit messy when cases like `key-key-value` or `key-value-value` came along (like `overflow-x-hidden` or `bg-red-500` for example). Then there were cases that sometimes were in one form and sometimes in others. It became obvious that is was not going to be possible to generalise the full Tailwind API like this. 111 | 112 | So I ended up reaching out to my colleague [Phil Pluckthun](https://github.com/kitten), seriously one of the smartest people I know! Having vast knowledge and experience with programming languages, lexing and parsing of grammars, he immediately started to break down the problem in a much more granular way than I had been doing. The first thing we did was to define all the grammar. This means going through all cases that we wanted to support, noting down any common patterns and coming up with our own notation for "valid" directives. The idea was that, once this was complete, we should have everything we needed to start generating a solution automatically via codegen. 113 | 114 | This grammar file had entries that looked something like this: 115 | 116 | ```js 117 | // .float-right => float: right; 118 | // .float-left => float: left; 119 | // .float-none => float: none; 120 | [['float', '$any'], ['$0', '$1']], 121 | ``` 122 | 123 | First we start with some comments which were copied almost verbatim from the tailwind documentation. This gives us a mapping of Tailwind shorthand to CSS output that is desired for a particular directive; our given input and desire output. Then we go ahead and try generalise all the cases, making a rule. We decided quite early on to use a nested tuple style syntax to denote these rules. The outermost tuple contains two more tuples. On the left we have the input definition and on the right, the output definition. 124 | 125 | We devised some custom notation pretty quickly. Thing like `$any` and `$0` or `$1`. This allowed us to reduce many directives, into just one generic rule. Perhaps you have already worked out the pattern here but in case you haven't: 126 | 127 | - The `$any` covers the terms `right`, `left` and `none` 128 | - The `$0` relates to `float` the first element in the input 129 | - The `$1` relates to the second element in the input 130 | 131 | Just to demonstrate the principle here, if the directive `float-cats` was suddenly to exist, then based on this grammar, you could infer the output would be `float: cats`. Pretty neat right! It turned out that this was quite a trivial case, some were even easier others weren't quite so straight forward. Eventually we were able to generalise them all. It became quite fun, but there were a _lot_! It was satisfying when you came across a directive that had many permutations but that could be reduced down to a single grammar. 132 | 133 | ```js 134 | // .z-0 => z-index: 0; 135 | // .z-10 => z-index: 10; 136 | // .z-20 => z-index: 20; 137 | // .z-30 => z-index: 30; 138 | // .z-40 => z-index: 40; 139 | // .z-50 => z-index: 50; 140 | // .z-auto => z-index: auto; 141 | [['z', '$any'], ['z-index', '$1']], 142 | ``` 143 | 144 | It also became apparent that our solution would be able to handle more than just the values prescribed in the Tailwind API. For example, the case above would produce `z-index: 10` when given `z-10` but similarly it would produce `z-index: 69` if given `z-69`. This was exciting. We still hadn't actually generated any code to do the translating, and we had no real idea whether it would actually end up being small enough to be worthwhile. But noticing how expressive we could be using this approach gave us a glimmer hope which was enough to make us persist. Going through every directive listed in the Tailwind documentation. 145 | 146 | ## Writing code that writes code 147 | 148 | So now we had this big file containing (almost) all possible grammars – there were some real edgy edge cases that we decided to defer on in fear that it would have a drastic effect on the complexity of generating a translator function automatically. I'm not ashamed to admit, the next 5 or so hours of development were kind of a blur for me. Phil proceeded to import `@babel/types` and `@babel/generator` then got to work writing code that writes code. 149 | 150 | I got the general gist of what was going on (and helped out by playing the role of duck pointing out syntactic errors and unscrambling some cryptic intermediate outputs) but by now I was way out of my depth. Luckily, the code that was being generated, although verbose, wasn't too hard to grok at all. It was essentially a big switch statement and those, I am familiar with! 151 | 152 | There were a few key learnings and tricks that were applied to make the output not only work, but be as compact and consistent as possible. To demonstrate how the generated solution worked let's consider the first case we looked at which was `bg-white`. Now, at first this seems like a very trivial case that could be denoted by the grammar `[['bg', '$color'], ['background-color', '$1']]` but what we did not consider before is that `bg-red-500` is also a valid Tailwind shorthand but which has the grammar `[['bg', '$any', '$shade'], ['background-color', '$2']]`. So how did we go about covering both cases? Well it goes something like this: 153 | 154 | ```js 155 | const out = {} 156 | const input = 'bg-red-500'.split('-') 157 | 158 | switch(input.length) { 159 | case 1: ... 160 | case 2: 161 | switch (input[0]) { 162 | case 'bg': 163 | out['background-color'] = input[1]; 164 | break; 165 | ... 166 | } 167 | break; 168 | case 3: 169 | switch (input[0]) { 170 | case 'bg': 171 | out['background-color'] = theme.colors[input[1]][input[2]]; 172 | break; 173 | ... 174 | } 175 | break; 176 | case 4: ... 177 | } 178 | 179 | return out; 180 | ``` 181 | 182 | For a start we split the input into parts at every `-` character. We then, somewhat unintuitively count how many parts we have ands switch on that; `bg-white` has two parts, whilst `bg-red-500` has three parts, and so on. This helps keep the translation operation for directives with different lengths (but a common first part) nice and straight forward. We then switch on the first part of the directive, in this instance looking for a `bg` case. It just so happens that we know (because it was defined in our grammar file) that, if a `bg` directive has just two parts then we can go ahead and use the second part as the CSS value. So `bg-rebeccapurple` for example, returns `{ background-color: rebeccapurple }` and we are done. If, however, the directive has three parts then we have to assume that a color from the _theme_ file is being requested. In the case of `bg-red-500` the `red-500` parts get converted to the value defined in the theme file, which just so happens to be `#F56565`, so `{ background-color: #F56565 }` is returned. 183 | 184 | I have slightly trivialised the cases and the code here for the sakes of demonstration, but hopefully you get the gist. This really isn't rocket science. Rather a slightly fancy lookup table. Regardless, we were quite happy with ourselves and I personally, couldn't wait to try it out. 185 | 186 | It was late by this point, perhaps 1am in the morning, but there was no way I was going to sleep without a quick play around to see if what we had made was going to work. It did... kind of! I tested some simple cases at first, they worked as expected and per the Tailwind spec. I tested a few directives with more parts and that required values be looked up from the theme file (like `bg-red-500`) and to my delight they worked too! 187 | 188 | However the more I tried, the more edge cases I was finding that were either not covered or not returning the expected output. For fucks sake, that was a lot of work and all for nothing, I remember thinking. If it wasn't _complete_ then what was the point really. But I guess that is the nature of experiments, some you win, some you lose. If it was easy then everyone would be doing it. Nevermind, it's late. I'll go to sleep and pick it up again in the morning. Then I didn't... 189 | 190 | ## So can we have our cake and eat it 191 | 192 | We were in the midst of lockdown at this point, my motivation and passion were dwindling. I woke up the next day and for some reason I couldn't face [the code we had written](https://gist.github.com/kitten/219ddda1db5df4ad42a05abf0f2738dd) despite it being pretty damn good for a nights work! 193 | 194 | It had been such an ordeal getting everything together and to be quite honest I didn't really understand the codegen element of the project. That was Phil's department and I felt like I'd already used up a lot of his precious time. So I proceeded, quite solemnly to get on with my "real work" for the client I was assigned to at the time, whilst this project lay dorment on my hard drive for the next month. Phil made a few amendments and probably hoped that would inspire me to pick it back up but my brain was being stubborn. 195 | 196 | > I realised that this was quite uncharacteristic of me and that my body was trying to tell me something. I needed some time off. I filed a request for a month long vacation which my employer was kind enough to grant me. 197 | 198 | During my month off I hardly opened my laptop at all but this project was still lingering in the back of my mind. I knew there was still so much to do for it to be a proper proof of concept, no matter a production ready piece of software. Then one day (some time during week 3) I thought screw it, let's do it. I opened up my code editor and started writing some tests. 199 | 200 | Writing tests were the logical next thing to do. The last thing I'd done is to identify that we had indeed missed some cases and/or got something wrong in the grammars or codegen. That said, I knew that it was _mostly_ right. The only way I was going to know what was wrong exactly is to write out an example of each unique type of directive, followed by what I expected the CSS output to look like. 201 | 202 | ```js 203 | const tests = { 204 | 'hidden': { display: 'none' }, 205 | 'bg-white': { background: 'white' }, 206 | 'float-left': { float: 'left' }, 207 | ... 208 | } 209 | ``` 210 | 211 | I ended up with another quite simple structure for the tests, one which I could iterate over quite easily. I knew that if I persisted with this approach and found a directive that wasn't behaving as expected, then I could go and find that entry in the switch statement and fix it by hand. 212 | 213 | Sure, this was probably wasn't the smartest approach and the "right" thing to do would to go and fix the code that generated the switch statement, or check for incorrect grammar. But I then thought, well we only did all that codegen stuff so that we didn't have to type out thousands of lines of switch statement, but they were already written now and to be honest the edge cases we had deferred to a later date really were curveballs that would probably take muich longer to account for and generate automatically, than they would to just write by hand. 214 | 215 | So my mind was made up. I started going through the whole Tailwind API again, one directive at a time, writing test cases then optimizing or fixing up the auto-generated code where appropriate. This was all done manually. It took what felt like _forever_ but turned out to be totally worthwhile. Now I knew two things. Firstly, that we had finally covered the vast majority of the API (including those pesky edge cases), secondly that our output was correct and that at least now if we changed something in the translate function then we'd know if it broke anything. I guess that's the whole point of tests! 216 | 217 | So I was feeling much more optimistic about the project by now. There was however, still a lot to do! 218 | 219 | ## Variety is the spice of life 220 | 221 | Up until now we had really just been focussed on the translation of shorthand directives into CSS and we'd kind of proved that was possible with this big switch statement. But there is a little more to this problem than that. For a start, as we have talked about before, there are variants; both responsive and pseudo. 222 | 223 | That means when we are given `sm:bg-white` we need to generate `{ background: white }` but that only gets applied when rendered on small screens. This is an interesting problem and got me thinking (and you probably are too by now) about how these styles are going to get applied to an element. When using Tailwind there is no dynamic behaviour, you write a class name in your HTML and if the corresponding rules exists in the CSS then the styles will be applied to the element. 224 | 225 | After some quick googling I stumbled across a library called Otion, which advertised itself as a CSS-in-JS solution tailored and optimized especially for atomic CSS. This sounded perfect and it was only a couple of kilobytes! So I started experimenting with how it might work. 226 | 227 | ```js 228 | otion({ 229 | 'margin-top': '0.25rem', 230 | '@media (min-width: 640px)': { 231 | background: 'white', 232 | }, 233 | ':hover': { 234 | background: 'red', 235 | }, 236 | }); 237 | // => "od34ud adsr81" 238 | ``` 239 | 240 | Essentially what Otion does is take a CSS-in-JS object, break it up into single rules, generate a class name for each rule, then append those rules to a stylesheet in the head of the document, finally returning a string of class names you can use on your element of choice. This seemed almost too good to be true. It seemed like the ideal fit for this project. It turns out that Kristof (the author of Otion) was also inspired by Tailwind too. 241 | 242 | It was at this point that the project name came about too! 243 | 244 | As you can see in the example above, Otion makes applying responsive variants pretty trivial. You pass it a media query with the rule you want scoped to that query in the form of an object and it will do the rest; essentially unwrapping the rules from the media query, generating the class names and then wrapping them up again before appending them to the DOM. It does a similar thing for pseudo selecters like `:hover` and `:active` too. 245 | 246 | ## All together now 247 | 248 | Everything was pretty much in place. Now all that was left was to pull everything together into a function that would be exposed from the main module, then try using that function in a real life application. 249 | 250 | Let's look at how that was all going to work. 251 | 252 | ```js 253 | import oceanwind from './index.js'; 254 | document.body.className = oceanwind`mt-1 sm:bg-white`; 255 | ``` 256 | 257 | Most of the projects I work on are either preact or react based, therefore CSS-in-JS wasn't a new concept for me. I reached for a familiar variable input mechanism, a tagged template literal. In most JavaScript styling libraries there is the option to write css inbetween the template literals and so it should feel familiar to most developers. It also means that directives can be written over multiple lines as supposed to in one long line (which Tailwind is limited to). I thought that this should help improve readability in some instances. 258 | 259 | Now the input method was established I just had to write the function that processed the inputs and generated the desired output. The steps I had in my head at this point went something like this: 260 | 261 | 1. Tidy up the input, forcing it into a single space delimited string 262 | 2. Split the string on the space character to get an array of directives 263 | 3. Lookup the translation for each directive in the array 264 | 4. Apply any variants to the directive translation 265 | 5. Merge all the translations together into a single CSS-in-JS like object 266 | 6. Pass the object to otion to generate class names and append styles to the DOM 267 | 268 | This function ended up being quite compact (less that 20 meaningful lines of code). There were a couple of things that I'd overlooked but the first 4 of these steps were pretty trivial to implement given that the translation code that had already been written: 269 | 270 | ```js 271 | const styles = rules 272 | .replace(/\s\s+/g, ' ') 273 | .trim() 274 | .split(' ') 275 | .map((rule) => { 276 | // Split the rule into parts 277 | rule = rule.split(':'); 278 | // Seperate out directive from variants 279 | let directive = rule.pop(); 280 | let variants = rule; 281 | // Lookup translation for directive 282 | let translation = translate(theme)(directive); 283 | // Apply variants to the translation 284 | variants.reverse().forEach((variant) => { 285 | if (theme.screen[variant]) translation = mediaQuery(variant)(translation); 286 | else translation = { [`:${variant}`]: translation }; 287 | }); 288 | // Return translation with variants applied 289 | return translation; 290 | }); 291 | ``` 292 | 293 | This produced an array of translated directives with any variants applied. All that was left to do now was to merge the translations together to form the CSS-in-JS object that would get passed to otion. At first I thought that this would be as simple as reducing over an array and returning a object. But there was one small caveats with this approach. 294 | 295 | If the input had multiple directive with variants applied, like `sm:scale-95 sm:rotate-3`, then you would end up with an array that looked something like this: 296 | 297 | ```js 298 | [ 299 | { 300 | '@media (min-width: 640px)': { 301 | transfrom: 'scale(0.95)', 302 | }, 303 | }, 304 | { 305 | '@media (min-width: 640px)': { 306 | transform: 'rotate(3deg)', 307 | }, 308 | }, 309 | ]; 310 | ``` 311 | 312 | Merging these values in a typical fashion (using the spread operator) would result in the latter rule overriding the former. After realising this, it became quickly apparent that a _deep merge_ would be required. Being a senior developer and all I googled "deep merge objects in JavaScript" and copy pasted from the top accepted answer on StackOverflow dot com. With these 12 lines of code the issue was resolved. 313 | 314 | There was also this annoying case caused by the way CSS accepts values for the `transform` property. Most CSS rules are pretty atomic by nature. By that I mean one key maps to one value and denotes one stylistic change to an element. There are of course execptions to this rules, for example thoes that accept shorthand values like `margin: 0 0 0 0` which denote multiple stylistic changes be applied to an element. Most properties like this can be broken down into their corresponding atomic parts - instead of `margin: 1rem 0 0 0` you can write `margin-top: 1rem` – but not `transform`, it is a special snowflake. 315 | 316 | There is no `rotate` or `scale` property in CSS, they are both values of the `transform` property. I'm not actually sure why this is (and as far as I know it is unique to `transform`) but it needed fixing. I looked at how Tailwind were doing this. They were using CSS custom properties like `--transform-rotate` which I assumed meant that their tranform translation looked something like: 317 | 318 | ```js 319 | transform: rotate(--transform-rotate) scale(--transform-scale); 320 | ``` 321 | 322 | Whereby they apply all transforms but have default null values for the custom properties if none were passed in as input. This seemed smart, and admitedly was something I'd overlooked when creating translations for the transform type directives. 323 | 324 | I hadn't dealt with custom properties in this implementation yet as there just hadn't been much reason to up until now and although I knew that Otion does support custom properties, I went for a quick and dirty fix which involved tweaking slighty the deep merge function: 325 | 326 | ```js 327 | prev[key] = key === 'transform' && pVal ? [oVal, pVal].join(' ') : oVal; 328 | ``` 329 | 330 | It now checks if it is merging a transform key, and if it is, then it mergest the existing value with the new value by means of joining with a space character, as prescribed by the CSS specification. 331 | 332 | This was far from ideal and still makes me feel queezy but it fixed the problem so I ran with it. It would be good to look at the custom property method in the future and see if it is worth refactoring to. This exploration could lead to exciting new ways of using CSS custom properties. 333 | 334 | ## Are you quite finished already 335 | 336 | A lot of progress had been made and it was now time to test out the module for reals. I setup a template es preact project using imports directly from unpkg – my ususal way of prototyping almost anything – then imported the oceanwind module and crossed my fingers. 337 | 338 | ```js 339 | import { render, h } from 'https://unpkg.com/preact?module'; 340 | import htm from 'https://unpkg.com/htm?module'; 341 | 342 | import ow from '../index.js'; 343 | 344 | const html = htm.bind(h); 345 | 346 | render( 347 | html`
`, 348 | document.body 349 | ); 350 | ``` 351 | 352 | Somewhat to my surprise, it worked! It was doing everything I'd have expected it to do. I tried applying responsive variants, pseudo variants and various combinations of both. This was a big relief. I committed one more time, wrote a README, published to npm (thus unpkg) and pushed everything up to GitHub. 353 | 354 | Everything was nearly finished, we now have the whole Tailwind API at our disposal during runtime and we are only generating the classes we needed. This is quite literally a class generating machine; some code that writes code! What I was really curious about now though, was how big was it? That switch statement we generated all that time ago was not small. If the whole module ended up being larger than say, the CDN version of Tailwind then not much would have been gained by doing all this work. 355 | 356 | So I opened up the network tab in the chome inspector, quite aprehensively: 357 | 358 | otion-network-tap 359 | 360 | Because I'd spit the module up into a few files, tt was hard to tell the exact size of the overall thing, but it looking pretty good. The translator function was in its own file and weighed in at quite a hefty 27 kilobytes. But due to the masses of repetition in that function (terms like `switch`, `case` and `break`) gzip was able to compress it down to just 4 kilobytes. It was smaller than I'd even hoped for! 361 | 362 | This was good but I knew it could be better. Knowing that all the files were required by the module index, it could be quite trivially bundled into one file for production use. As I stated earlier on in the article, I'm not one for big ol' build tool chains and so I wasn't about to set one up. A much better way I have found of doing this kind of pre-publish build is to employ `esbuild` like this: 363 | 364 | ```json 365 | { 366 | "scripts": { 367 | "build": "esbuild --bundle index.js --outfile=index.min.js --format=esm --minify" 368 | } 369 | } 370 | ``` 371 | 372 | This line of code takes the module index file, concatinates together all the modules it depends on, then minifies the result, exposing a single set of public exports. It does all this in tens of milliseconds with no configuration. The package itself is also tiny (around ~5MB), consisting of 7 files revolving around a single GO binary. A truly incredible piece of software. 373 | 374 | The result of running the module through `esbuild` was **a single file weighing 22.9 kilobytes. It compressed down to 7.7kb with gzip and 7.2kb with brotli.** I couldn't have been happier.. all the styles you could ever ask for in less than the average purged Tailwind output file (which according to the documentation was around 10kb)! 375 | 376 | It was now possible to do: 377 | 378 | ```js 379 | import ow from 'https://unpkg.com/oceanwind/index.min.js'; 380 | ``` 381 | 382 | I believe this really makes getting started with Tailwind _considerably_ less effort. Whats more is that there isn't much reason that it can't scale just like any other CSS-in-JS solution. In fact, because the Tailwind API constrants the developer into using only a small subset of all possible CSS values and because otion is optimized for this precise scenario, then I'd be surprised if it doesn't scale even better than most CSS-in-JS solutions. 383 | 384 | Not only might you be better off technically by taking this approach, but by the nature of adhering to the Tailwind API you are going to be producing much more consistent designs. This makes it what I call a win win, win win; better for your developers, designers, product and users. I kind of brushed over the benefits of Tailwind in regards to this axiom at the start of the article but what Adam Wathan et al. has done to prove and popularise the philosophy of constrained yet composable design on the web is absolutely ground breaking, opening up doors to interesting and exciting futures. 385 | 386 | Some people reading this will be thinking, this is at runtime is all well and good but now the client has to do more work, and these people would be correct. Deferring any kind of compilation like this to runtime will certainly require more processor cycles. Thankfully the transpilation step here is relatively cheap and results can be cached reliably (similar to [`htm`](https://github.com/developit/htm)) to the point where the impact on perf should be almost negligable if you are already using a frontend framework to render UI. That said, I haven't tried optimizing for performance yet so I imagine there are still some gains to be had. 387 | 388 | ## What does the future hold 389 | 390 | That concludes the journey to building oceanwind so far. In summary I'm pretty happy with the outcome and am excited to hear what people think to the approach, how you could improve on it, what you would like to see added feature wise and if there are any bugs (there probably will be.. lots). 391 | 392 | By the nature of essentially creating an abstraction on top of an abstraction, keeping the APIs of Oceanwind aligned with Tailwind will take considerable effort. I'm not sure how much time I will be able to dedicate to this task so if you notice something missing and have some free time then perhaps take the time to fork the project and make a pull request with a proposed implementation along with a test case. 393 | 394 | If you have made it this far then thank you for your time, I hope you learnt something here. Let me know on [Twitter](https://twitter.com/lukejacksonn) if you build anything with Oceanwind. I'd love to hear from you! 395 | -------------------------------------------------------------------------------- /posts/perflink.md: -------------------------------------------------------------------------------- 1 | # Perflink 2 | 3 | > JavaScript performance benchmarks that you can share via URL 4 | 5 | The motivation here was to create a single page app like [jsperf](https://jsperf.com) – which is commonly used to compare performance characteristics of different Javascript code snippets – but with improved usability and portibility of results. It is a frontend only static web app with no build step and is hosted on Github pages. 6 | 7 | ![perflink3](https://user-images.githubusercontent.com/1457604/55292888-f5ecd300-53e7-11e9-94fb-3266adaaf235.gif) 8 | 9 | ## Features 10 | 11 | - 🎨 Syntax highlighted textarea inputs 12 | - ♻️ Benchmarks run automatically when test cases change 13 | - 🌍 Serializable state encoded into shareable URLs 14 | - ⏱ Accurate timing using `performance.now()` 15 | - 🗜 Super light weight – almost no dependencies 16 | 17 | ## Usage 18 | 19 | To use the web interface simply visit https://perf.link and start typing. As you type the code will be evaluated and benchmarked – against all other test cases – the results of which will appear on graph to the right. 20 | 21 | The contents of all inputs and latest benchmark results for each test case, are stored in state which is serialised using the browsers `atob` function and set as the `location.hash`. This happens every time a benchmark is ran. That means you can share your findings with anyone by just copy pasting the window URL. 22 | 23 | ## Development 24 | 25 | If you would like to develop the project, first clone this repo then run the following command in your terminal (from the project root directory) which will open the app in your preferred browser. 26 | 27 | ``` 28 | $ npx servor 29 | ``` 30 | 31 | > Live reload is enabled by default with [servor](https://github.com/lukejacksonn/servor) so when you make changes to your code the browser will reload the tab and your changes will be reflected there. 32 | 33 | ### es-react 34 | 35 | This project uses a proprietary version of react called [`es-react`](https://github.com/lukejacksonn/es-react) which allows you to import `React` and `ReactDOM` (version 16.8.3) as an es module from within your app and component files. 36 | 37 | ```js 38 | import { React, ReactDOM } from 'https://unpkg.com/es-react'; 39 | ``` 40 | 41 | ## Implementation 42 | 43 | Benchmarking involves accurate timing. Historically this has been hard to do due to the limitations of timers in JavaScript. Recently a high resolution timer was added by the WebPerf Working Group to allow measurement in the Web Platform with much greater degree of accuracy. 44 | 45 | Here is how time of execution is calculated: 46 | 47 | ```js 48 | let time; 49 | try { 50 | time = eval(`() => { 51 | ${before} 52 | let start, end 53 | start = performance.now() 54 | ${test.code} 55 | end = performance.now() 56 | return end - start 57 | }`)(); 58 | } catch (e) {} 59 | ``` 60 | 61 | ### Measuring 62 | 63 | This timer is available through the `performance.now()` method. Numbers returned from function call are not limited to one-millisecond resolution. Instead, they represent times as floating-point numbers with up to microsecond precision and are monotonic. 64 | 65 | ### Benchmarking 66 | 67 | Currently when the benchmark is ran, each tast case will be executed execute 100 times (this is subject to becoming variable as to cover more advanced use cases) then both the total and the median execution time in milliseconds is recorded and displayed on the graph. 68 | 69 | ## Todo 70 | 71 | I would like the app to remain simple with very focussed scope but I am interested in a few features: 72 | 73 | - Look into running tests from a service worker 74 | - Support test cases with async code 75 | - More options around benchmark settings (iteration count etc.) 76 | - Offer various different graph types and legends 77 | - More interesting and demonstrative tests cases 78 | - Being able to archive URLs by putting them into localstorage 79 | - A proper logo and maybe a better color scheme 80 | - Making the user interface responsive 81 | 82 | I am not interested in: 83 | 84 | - Rewriting the project in TypeScript 85 | - Adding Babel or Webpack (or any build step for that matter) 86 | 87 | Although I am currently quite busy with work I will try take the time to review PRs that address these todos. 88 | -------------------------------------------------------------------------------- /posts/react-slack-clone.md: -------------------------------------------------------------------------------- 1 | # React Slack Clone 2 | 3 | > Slack clone powered by [Chatkit](https://pusher.com/chatkit). See it in action here https://pusher.github.io/react-slack-clone 4 | 5 | ![demo](https://user-images.githubusercontent.com/1457604/35891289-687ad6ec-0b9b-11e8-99cc-ffbad31a017e.gif) 6 | 7 | This is a static, single page web app bootstrapped with [create-react-app](https://github.com/facebookincubator/create-react-app) for ease of setup, distribution and development. It is a thin UI wrapper around the [pusher-chatkit-client](https://github.com/pusher/chatkit-client-js) library to demonstrate how different features can work together to form a compelling real-time chat client with various potential product applications. 8 | 9 | ## Features 10 | 11 | The Chatkit SDK allows you to implement features you would expect from a chat client. These include: 12 | 13 | - 📝 Public and private chat rooms 14 | - 📡 Realtime sending and receiving of messages 15 | - 📦 Rich media attachments (drag and drop) 16 | - 💬 Typing and presence indicators 17 | - 📚 Read message cursors 18 | 19 | Want to get involved? We have a bunch of [beginner-friendly GitHub issues](https://github.com/pusher/react-slack-clone/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). 20 | 21 | ## Components 22 | 23 | The demo attempts to be feature complete according to documentation [here](https://docs.pusher.com/chatkit/reference/javascript). Feature requests should be made via issues or pull requests to this repository. 24 | 25 | - CreateMessageForm - to send a message with a textual body and trigger typing indicators. 26 | - CreateRoomForm - to create a new room and join it upon creation. 27 | - FileInput - to send a message with a rich media attachment. 28 | - Message - to render out a message that potentially includes an attachment. 29 | - MessageList - to render a list of messages from a key value store. 30 | - RoomHeader - to display useful information about a given room. 31 | - RoomList - to render a list of rooms which can be subscribed to by the current user. 32 | - TypingIndicator - to signify to the user that another user is typing in a given room. 33 | - UserHeader - to display useful information about a given user. 34 | 35 | ## Usage 36 | 37 | To run the application locally; clone the repo, install dependencies and run the app. 38 | 39 | ``` 40 | $ git clone https://github.com/pusher/react-slack-clone 41 | $ cd react-slack-clone 42 | $ yarn && yarn start 43 | ``` 44 | 45 | The app starts in development mode and opens a browser window on `http://localhost:3000`. The project rebuilds and the browser reloads automatically when source files are changed. Any build or runtime errors are propagated and displayed in the browser. 46 | 47 | The app depends on GitHub authentication and a user creation endpoint that is hosted at https://chatkit-demo-server.herokuapp.com. The endpoints are `/auth` and `/token`. 48 | 49 | [github-star-badge]: https://img.shields.io/github/stars/pusher/react-slack-clone.svg?style=social 50 | [github-star]: https://github.com/pusher/react-slack-clone/stargazers 51 | [twitter-badge]: https://img.shields.io/twitter/url/https/github.com/kentcdodds/react-testing-library.svg?style=social 52 | [twitter]: https://twitter.com/intent/tweet?text=Check%20out%20this%20Slack%20clone%20using%20@pusher%20Chatkit%20%F0%9F%91%89https://github.com/pusher/react-slack-clone 53 | [travis-badge]: https://travis-ci.org/pusher/react-slack-clone.svg?branch=master 54 | [travis]: https://travis-ci.org/pusher/react-slack-clone 55 | -------------------------------------------------------------------------------- /posts/servor.md: -------------------------------------------------------------------------------- 1 | # Servør 2 | 3 | > A dependency free dev server for modern web application development 4 | 5 | The new and enhanced version of [http-server-spa](https://npmjs.com/http-server-spa). A very compact but capable static file server with https, live reloading and other useful features to support web app development on localhost and over a local network. 6 | 7 | Servør can be invoked via the command line or programmatically using the node API. 8 | 9 |
10 | 11 | servor 12 | 13 | ## Features 14 | 15 | The motivation here was to write a package from the ground up with no dependencies; using only native node and browser APIs to do a specific task with minimal code. 16 | 17 | - 🗂 Serves static content like scripts, styles, images from a given directory 18 | - 🖥 Redirects all path requests to a single file for frontend routing 19 | - ♻️ Reloads the browser when project files get added, removed or modified 20 | - 🔐 Supports https with self signed certificates added to the systems trusted store 21 | - 🔎 Discovers freely available ports to serve on if no port is specified 22 | 23 | ## CLI Usage 24 | 25 | Run as a terminal command without adding it as a dependency using `npx`: 26 | 27 | ```s 28 | npx servor 29 | ``` 30 | 31 | - `` path to serve static files from (defaults to current directory `.`) 32 | - `` the file served for all non-file requests (defaults to `index.html`) 33 | - `` what port you want to serve the files from (defaults to `8080`) 34 | 35 | Optional flags passed as non-positional arguments: 36 | 37 | - `--browse` causes the browser to open when the server starts 38 | - `--reload` causes the browser to reload when files change 39 | - `--secure` starts the server with https using generated credentials 40 | - `--silent` prevents the node process from logging to stdout 41 | 42 | Example usage with npm scripts in a `package.json` file after running `npm i servor -D`: 43 | 44 | ```json 45 | { 46 | "devDependencies": { 47 | "servor": "3.1.0" 48 | }, 49 | "scripts": { 50 | "start": "servor www index.html 8080 --reload --browse" 51 | } 52 | } 53 | ``` 54 | 55 | ### Generating Credentials 56 | 57 | > NOTE: This process depends on the `openssl` command existing (tested on macOS only) 58 | 59 | The files `servor.crt` and `servor.key` need to exist for the server to start using https. If the files do not exist when the `--secure` flag is passed, then [`certify.sh`](/certify.sh) is invoked which: 60 | 61 | - Creates a local certificate authority used to generate self signed SSL certificates 62 | - Runs the appropriate `openssl` commands to produce: 63 | - a root certificate (pem) so the system will trust the self signed certificate 64 | - a public certificate (crt) that the server sends to clients 65 | - a private key for the certificate (key) to encrypt and decrypt traffic 66 | 67 | #### Adding credentials to the trusted store 68 | 69 | > NOTE: This process depends on the `sudo` and `security` commands existing (tested on macOS only) 70 | 71 | For the browser to trust self signed certificates the root certificate must be added to the system trusted store. This can be done automatically by running `sudo servor --secure` which: 72 | 73 | - Adds the root certificate to the system Keychain Access 74 | - Prevents the "⚠️ Your connection is not private" screen 75 | - Makes the 🔒 icon appear in the browsers address bar 76 | 77 | The approach was adopted from [@kingkool68/generate-ssl-certs-for-local-development](https://github.com/kingkool68/generate-ssl-certs-for-local-development) 78 | 79 | ## API Usage 80 | 81 | Use servor programmatically with node by requiring it as a module in your script: 82 | 83 | ```js 84 | const servor = require('servor'); 85 | const instance = await servor({ 86 | root: '.', 87 | fallback: 'index.html', 88 | port: 8080, 89 | reload: false, 90 | inject: '' 91 | credentials: {}, 92 | }); 93 | ``` 94 | 95 | The `servor` function accepts a config object with optional props assigned the above default values if none are provided. Calling the `servor` function starts up a new server and returns an object describing its configuration. 96 | 97 | ```js 98 | const { url, root, protocol, port, ips } = await servor(config); 99 | ``` 100 | 101 | ### Inject 102 | 103 | The `inject` property accepts a string that gets prepended to the servers root document (which is `index.html` by default). This could be used to inject config or extend the development servers behavior and capabilities to suit specific environments. 104 | 105 | ```js 106 | const config = require('package.json'); 107 | servor({ inject: `` }); 108 | ``` 109 | 110 | ### Credentials 111 | 112 | The `credentials` property accepts an object containing the entries `cert` and `key` which must both be valid for the server to start successfully. If valid credentials are provided then the server will start serving over https. 113 | 114 | It is possible to generate the appropriate credentials using the `--secure` CLI flag. 115 | 116 | ## Notes 117 | 118 | Thanks to all the contributors to this projects so far. If you find a bug please create an issue or if you have an idea for a new feature then fork the project and create a pull request. Let me know how you are using servør [on twitter](https://twitter.com/lukejacksonn). 119 | -------------------------------------------------------------------------------- /runtime.js: -------------------------------------------------------------------------------- 1 | var e,n,t,_,r,o,u,l,i={},c=[],s=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord/i;function a(e,n){for(var t in n)e[t]=n[t];return e}function f(e){var n=e.parentNode;n&&n.removeChild(e)}function p(e,n,t){var _,r=arguments,o={};for(_ in n)"key"!==_&&"ref"!==_&&(o[_]=n[_]);if(arguments.length>3)for(t=[t],_=3;_2&&(n.children=c.slice.call(arguments,2)),t={},n)"key"!==_&&"ref"!==_&&(t[_]=n[_]);return h(e.type,t,n.key||e.key,n.ref||e.ref,null)}function W(e){var n={},t={__c:"__cC"+l++,__:e,Consumer:function(e,n){return e.children(n)},Provider:function(e){var _,r=this;return this.getChildContext||(_=[],this.getChildContext=function(){return n[t.__c]=r,n},this.shouldComponentUpdate=function(e){r.props.value!==e.value&&_.some((function(n){n.context=e.value,k(n)}))},this.sub=function(e){_.push(e);var n=e.componentWillUnmount;e.componentWillUnmount=function(){_.splice(_.indexOf(e),1),n&&n.call(e)}}),e.children}};return t.Consumer.contextType=t,t.Provider.__=t,t}e={__e:function(e,n){for(var t,_;n=n.__;)if((t=n.__c)&&!t.__)try{if(t.constructor&&null!=t.constructor.getDerivedStateFromError&&(_=!0,t.setState(t.constructor.getDerivedStateFromError(e))),null!=t.componentDidCatch&&(_=!0,t.componentDidCatch(e)),_)return k(t.__E=t)}catch(n){e=n}throw e}},n=function(e){return null!=e&&void 0===e.constructor},m.prototype.setState=function(e,n){var t;t=this.__s!==this.state?this.__s:this.__s=a({},this.state),"function"==typeof e&&(e=e(t,this.props)),e&&a(t,e),null!=e&&this.__v&&(n&&this.__h.push(n),k(this))},m.prototype.forceUpdate=function(e){this.__v&&(this.__e=!0,e&&this.__h.push(e),k(this))},m.prototype.render=v,t=[],_=0,r="function"==typeof Promise?Promise.prototype.then.bind(Promise.resolve()):setTimeout,u=i,l=0;var O,R,V,j=0,q=[],B=e.__r,I=e.diffed,$=e.__c,z=e.unmount;function G(n,t){e.__h&&e.__h(R,n,j||t),j=0;var _=R.__H||(R.__H={__:[],__h:[]});return n>=_.__.length&&_.__.push({}),_.__[n]}function J(e){return j=1,Z(ce,e)}function Z(e,n,t){var _=G(O++,2);return _.__c||(_.__c=R,_.__=[t?t(n):ce(void 0,n),function(n){var t=e(_.__[0],n);_.__[0]!==t&&(_.__[0]=t,_.__c.setState({}))}]),_.__}function K(n,t){var _=G(O++,3);!e.__s&&ie(_.__H,t)&&(_.__=n,_.__H=t,R.__H.__h.push(_))}function Q(n,t){var _=G(O++,4);!e.__s&&ie(_.__H,t)&&(_.__=n,_.__H=t,R.__h.push(_))}function X(e){return j=5,ee((function(){return{current:e}}),[])}function Y(e,n,t){j=6,Q((function(){"function"==typeof e?e(n()):e&&(e.current=n())}),null==t?t:t.concat(e))}function ee(e,n){var t=G(O++,7);return ie(t.__H,n)?(t.__H=n,t.__h=e,t.__=e()):t.__}function ne(e,n){return j=8,ee((function(){return e}),n)}function te(e){var n=R.context[e.__c],t=G(O++,9);return t.__c=e,n?(null==t.__&&(t.__=!0,n.sub(R)),n.props.value):e.__}function _e(n,t){e.useDebugValue&&e.useDebugValue(t?t(n):n)}function re(e){var n=G(O++,10),t=J();return n.__=e,R.componentDidCatch||(R.componentDidCatch=function(e){n.__&&n.__(e),t[1](e)}),[t[0],function(){t[1](void 0)}]}function oe(){q.some((function(n){if(n.__P)try{n.__H.__h.forEach(ue),n.__H.__h.forEach(le),n.__H.__h=[]}catch(t){return n.__H.__h=[],e.__e(t,n.__v),!0}})),q=[]}function ue(e){e.t&&e.t()}function le(e){var n=e.__();"function"==typeof n&&(e.t=n)}function ie(e,n){return!e||n.some((function(n,t){return n!==e[t]}))}function ce(e,n){return"function"==typeof n?n(e):n}e.__r=function(e){B&&B(e),O=0,(R=e.__c).__H&&(R.__H.__h.forEach(ue),R.__H.__h.forEach(le),R.__H.__h=[])},e.diffed=function(n){I&&I(n);var t=n.__c;if(t){var _=t.__H;_&&_.__h.length&&(1!==q.push(t)&&V===e.requestAnimationFrame||((V=e.requestAnimationFrame)||function(e){var n,t=function(){clearTimeout(_),cancelAnimationFrame(n),setTimeout(e)},_=setTimeout(t,100);"undefined"!=typeof window&&(n=requestAnimationFrame(t))})(oe))}},e.__c=function(n,t){t.some((function(n){try{n.__h.forEach(ue),n.__h=n.__h.filter((function(e){return!e.__||le(e)}))}catch(o){t.some((function(e){e.__h&&(e.__h=[])})),t=[],e.__e(o,n.__v)}})),$&&$(n,t)},e.unmount=function(n){z&&z(n);var t=n.__c;if(t){var _=t.__H;if(_)try{_.__.forEach((function(e){return e.t&&e.t()}))}catch(n){e.__e(n,t.__v)}}};var se=function(e,n,t,_){var r;n[0]=0;for(var o=1;o=5&&((r||!e&&5===_)&&(u.push(_,0,r,t),_=6),e&&(u.push(_,e,0,t),_=6)),r=""},i=0;i"===n?(_=1,r=""):r=n+r[0]:o?n===o?o="":r+=n:'"'===n||"'"===n?o=n:">"===n?(l(),_=1):_&&("="===n?(_=5,t=r,r=""):"/"===n&&(_<5||">"===e[i][c+1])?(l(),3===_&&(u=u[0]),_=u,(u=u[0]).push(2,0,_),_=0):" "===n||"\t"===n||"\n"===n||"\r"===n?(l(),_=2):r+=n),3===_&&"!--"===r&&(_=4,u=u[0])}return l(),u}(e)),n),arguments,[])).length>1?n:n[0]}var pe=fe.bind(p);var he={data:""},de=function(e){try{var n=e?e.querySelector("#_goober"):self._goober;return n||((n=(e||document.head).appendChild(document.createElement("style"))).innerHTML=" ",n.id="_goober"),n.firstChild}catch(o){}return he},ve=function(e){var n=de(e),t=n.data;return n.data="",t},me=/(?:([a-z0-9-%@]+) *:? *([^{;]+?);|([^;}{]*?) *{)|(})/gi,ye=/\/\*.*?\*\/|\s{2,}|\n/gm,ge=function(e,n,t){var _="",r="",o="";for(var u in e){var l=e[u];if("object"==typeof l){var i=n+" "+u;/&/g.test(u)&&(i=u.replace(/&/g,n)),"@"==u[0]&&(i=n,"f"==u[1]&&(i=u)),/@k/.test(u)?r+=u+"{"+ge(l,"","")+"}":r+=ge(l,i,i==n?u:t||"")}else/^@i/.test(u)?o=u+" "+l+";":_+=u.replace(/[A-Z]/g,"-$&").toLowerCase()+":"+l+";"}if(_.charCodeAt(0)){var c=n+"{"+_+"}";return t?r+t+"{"+c+"}":o+c+r}return o+r},ke={},be=function(e,n,t,_){var r=JSON.stringify(e),o=ke[r]||(ke[r]=".go"+r.split("").reduce((function(e,n){return 101*e+n.charCodeAt(0)>>>0}),11));return function(e,n,t){n.data.indexOf(e)<0&&(n.data=t?e+n.data:n.data+e)}(ke[o]||(ke[o]=ge(e[0]?function(e){for(var n,t=[{}];n=me.exec(e.replace(ye,""));)n[4]&&t.shift(),n[3]?t.unshift(t[0][n[3]]=t[0][n[3]]||{}):n[4]||(t[0][n[1]]=n[2]);return t[0]}(e):e,t?"":o)),n,_),o.slice(1)},Ce=function(e,n,t){return e.reduce((function(e,_,r){var o=n[r];if(o&&o.call){var u=o(t),l=u&&u.props&&u.props.className||/^go/.test(u)&&u;o=l?"."+l:u&&u.props?"":u}return e+_+(null==o?"":o)}),"")};function xe(e){var n=this||{},t=e.call?e(n.p):e;return be(t.map?Ce(t,[].slice.call(arguments,1),n.p):t,de(n.target),n.g,n.o)}var we,He=xe.bind({g:1}),Se=function(e){return we=e};function Ee(e){var n=this||{};return function(){var t=arguments;return function(_){var r=n.p=Object.assign({},_),o=r.className;return n.o=/\s*go[0-9]+/g.test(o),r.className=xe.apply(n,t)+(o?" "+o:""),we(e,r)}}}var Pe=36,Ne="";while(Pe--)Ne+=Pe.toString(36);function De(e){var n="",t=e||11;while(t--)n+=Ne[Math.random()*36|0];return n}export{m as Component,v as Fragment,T as _unmount,F as cloneElement,W as createContext,p as createElement,d as createRef,xe as css,ve as extractCss,He as glob,p as h,pe as html,L as hydrate,n as isValidElement,e as options,M as render,Se as setPragma,Ee as styled,x as toChildArray,De as uid,ne as useCallback,te as useContext,_e as useDebugValue,K as useEffect,re as useErrorBoundary,Y as useImperativeHandle,Q as useLayoutEffect,ee as useMemo,Z as useReducer,X as useRef,J as useState}; --------------------------------------------------------------------------------