pipe-dom

import { query, addClass, append, on } from 'pipe-dom';

query('ul')
  |> addClass('important-list')
  |> append(
    document.createElement('li')
      |> addClass('important-item')
      |> on('click', () => console.log('item clicked'))
)

Tagged Unions & Loading State

type State = {
  items: Item[] | null;
  error: Error | null;
};

const initialState: State = {
  items: null,
  error: null
};

const successState: State = {
  items: response.data,
  error: null
};

const errorState: State = {
  items: null,
  error: response.error
};
if (state.items === null && state.error === null) {
  return ;
}

if (state.items !== null) {
  return ;
}

// We can assume we're in the error state now
return ;
type State = {
  status: 'unloaded' | 'loading' | 'loaded' | 'error';
  items: Item[] | null;
  error: Error | null;
};

const defaultState: State = {
  status: 'unloaded',
  items: null,
  error: null
};
type State = {
  loading: boolean;
  items: Item[] | null;
  error: Error | null;
};

const defaultState: State = {
  loading: false,
  items: null,
  error: null
};
if (state.loading) {
  return ;
}

if (state.items === null && state.error === null) {
  return ;
}

if (state.items !== null) {
  return ;
}

return ;
switch (state.status) {
  case 'unloaded':
    return ;
  case 'loading':
    return ;
  case 'loaded':
    return ;
  case 'error':
    return ;
}
type First = {
  tag: 'first';
  prop: number;
};

type Second = {
  tag: 'second';
  value: string;
}

type FirstOrSecond = First | Second;

declare function getFirstOrSecond(): FirstOrSecond;

const thing: FirstOrSecond = getFirstOrSecond();

switch (thing.tag) {
  case 'first':
    // TypeScript knows prop exists and is a number.
    // Accessing thing.value would cause an error.
    return thing.prop + 1;
  case 'second':
    // And value exists and is a string here
    return thing.value + ' came in second';
}
type First = {
  prop: number;
};

type Second = {
  value: string;
};

type FirstOrSecond = First | Second;

declare function getFirstOrSecond(): FirstOrSecond;

const thing: FirstOrSecond = getFirstOrSecond();

// TypeScript complains about Second not having a `prop` property.
if (typeof thing.prop === 'number') {
  // TypeScript does not know you have a First
  // without an explicit cast
  const prop = (thing as First).prop;
} else {
  const value = (thing as Second).value;
}
type UnloadedState = {
  status: 'unloaded';
};

type LoadingState = {
  status: 'loading';
};

type LoadedState = {
  status: 'loaded';
  items: Item[];
};

type ErrorState = {
  status: 'error';
  error: Error;
};

type State = UnloadedState | LoadingState | LoadedState | ErrorState;
type State = {
  items: Item[];
};

const defaultState: State = {
  items: []
};

useOnEnter

import React, { useContext, useEffect } from 'react';
import { StdinContext } from 'ink';

export default function useOnEnter(onEnter) {
  const { stdin } = useContext(StdinContext);

  useEffect(() => {
    const onData = data => {
      const s = data.toString();

      if (s === '\r') {
        onEnter();
      }
    };

    stdin.on('data', onData);

    return () => {
      stdin.off('data', onData);
    };
  });
};

Removed from brookjs

const child$s = new WeakMap();

const getChildStream = (stream$, id) => {
    let childs = child$s.get(stream$);

    if (!childs) {
        child$s.set(stream$, childs = {});
    }

    if (childs[id]) {
        return childs[id];
    }

    return childs[id] = stream$.map(props => props.dict[id]);
};

const orderMatches = (prev, next) => prev.order === next.order;
const id = x => x;

export default function loop(mapper, callback) {
    if (callback == null) {
        callback = mapper;
        mapper = id;
    }

    return stream$ => {
        const src$ = stream$.map(mapper);

        return src$.skipDuplicates(orderMatches)
            .map(props => props.order.map(id => callback(getChildStream(src$, id), id)));
    };
}
import Kefir from 'kefir';
import * as PropTypes from 'prop-types';

const spyOn = (s$, validator, propName, componentName, name) => {
    if (s$._alive) {
        const handler = (event) => {
            if (event.type === 'value') {
                PropTypes.checkPropTypes(validator, event.value, 'prop', componentName);
            }
        };
        if (!s$._spyHandlers) {
            s$._spyHandlers = [];
        }
        s$._spyHandlers.push({ name, handler });
        s$._dispatcher.addSpy(handler);

        if (s$._currentEvent) {
            handler(s$._currentEvent);
        }
    }
};

export default function observableOfValidator(valueValidator, name = 'observableOf') {
    const validator = function observableOf(props, propName, componentName) {
        const propValue = props[propName];
        if (propValue == null) {
            return null;
        }

        if (!(propValue instanceof Kefir.Observable)) {
            return new TypeError(`${componentName}: ${propName} must be an Observable, got "${typeof propValue}"`);
        }

        spyOn(propValue, valueValidator, propName, componentName, name);

        return null;
    };

    validator.isRequired = function andIsRequired(props, propName, componentName) {
        const propValue = props[propName];

        if (!(propValue instanceof Kefir.Observable)) {
            return new TypeError(`${componentName}: ${propName} must be an Observable, got "${typeof propValue}"`);
        }

        spyOn(propValue, valueValidator, propName, componentName, name);

        return null;
    };

    return wrapValidator(validator, name);
}

function wrapValidator(validator, typeName, typeChecker = null) {
    return Object.assign(validator.bind(), {
        typeName,
        typeChecker,
        isRequired: Object.assign(validator.isRequired.bind(), {
            typeName,
            typeChecker,
            typeRequired: true,
        }),
    });
}

Sync ownCloud bookmarks to Pinboard

import os
import requests
from bs4 import BeautifulSoup
from termcolor import cprint
from dotenv import load_dotenv


class Hydrator(object):
    def __init__(self):
        self.soups = {}

    def title(self, url):
        soup = self._get_soup(url)
        title = soup.title.string

        if title == "":
            cprint(f"Url provided as title for {url}", "white", "on_blue")
            title = url

        return title

    def description(self, url):
        soup = self._get_soup(url)
        meta = soup.find_all("meta")

        for tag in meta:
            if (
                "name" in tag.attrs.keys()
                and tag.attrs["name"].strip().lower() == "description"
            ):
                content = tag.attrs.get("content")

                if content is None:
                    content = tag.attrs.get("value")

                if content is None:
                    cprint(f"No content found", "red")
                    print(tag.attrs.keys())
                else:
                    return content

        cprint(f"No description found for {url}", "white", "on_blue")

        return ""

    def _get_soup(self, url):
        soup = self.soups.get(url)

        if soup is None:
            response = requests.get(url)
            soup = self.soups[url] = BeautifulSoup(response.content, "lxml")

        return soup


hydrate = Hydrator()


def get_bookmark_from_pinboard(url):
    response = requests.get(
        url="https://api.pinboard.in/v1/posts/get",
        params={
            "url": url,
            "auth_token": os.getenv("PINBOARD_AUTH_TOKEN"),
            "format": "json",
        },
    )

    posts = response.json()["posts"]

    return posts[0] if len(posts) == 1 else None


def hydrate_bookmark(bookmark):
    title = bookmark["title"]
    url = bookmark["url"]
    description = bookmark["description"]

    if title == url or title == "":
        title = hydrate.title(url)

    if description == url or description == "":
        description = hydrate.description(url)

    return {
        "url": url,
        "title": title,
        "description": description,
        "tags": bookmark["tags"],
    }


def add_bookmark_to_pinboard(bookmark):
    response = requests.get(
        url="https://api.pinboard.in/v1/posts/add",
        params={
            "auth_token": os.getenv("PINBOARD_AUTH_TOKEN"),
            "format": "json",
            "url": bookmark["url"],
            # actually title
            "description": bookmark["title"],
            # actually description
            "extended": bookmark["description"],
            "tags": ",".join(bookmark["tags"]),
        },
    )

    body = response.json()

    return body["result_code"] == "done"


def process_bookmark(bookmark):
    url = bookmark["url"]
    pinboard_bookmark = get_bookmark_from_pinboard(url)

    # Add only if we don't find the bookmark in pinboard.
    if pinboard_bookmark is None:
        hydrated = hydrate_bookmark(bookmark)
        success = add_bookmark_to_pinboard(hydrated)

        if success:
            cprint(f"Successfully added {url} to pinboard", "green")
        else:
            cprint(f"Error adding {url} to pinboard", "red")
    else:
        cprint(f"Url {url} found in Pinboard. Skipping.", "yellow")


def send_request(page=0):
    try:
        response = requests.get(
            url=f'https://{os.getenv("OWNCLOUD_DOMAIN")}/index.php/apps/bookmarks/bookmark',
            params={"type": "bookmark", "page": page},
            headers={
                "Cookie": os.getenv("OWNCLOUD_COOKIE"),
                "requesttoken": os.getenv("OWNCLOUD_REQUEST_TOKEN"),
            },
        )

        body = response.json()
        status = body["status"]

        if status == "success":
            data = body["data"]

            for bookmark in data:
                process_bookmark(bookmark)

            if len(data) != 0:
                send_request(page + 1)
    except requests.exceptions.RequestException as err:
        cprint(f"Request failed: {err.request.url}", "red")


if __name__ == "__main__":
    load_dotenv()
    send_request()
PINBOARD_AUTH_TOKEN=username:token
OWNCLOUD_DOMAIN=your.owncloud.domain.com
OWNCLOUD_COOKIE=Get this from the network tab of the bookmarks page
OWNCLOUD_REQUEST_TOKEN=Also from the network tab

Update Array Hack

const arr = [1, 2, 3];
const newArr = Array.from({ ...arr, 1: 3, length: arr.length });
console.log(newArr); // [1, 3, 3]
const arr = [1, 2, 3];
const newObj = { ...arr };
console.log(newObj); // {0: 1, 1: 2, 2: 3}
const arr = [1, 2, 3];
const newObj = { ...arr, 1: 3 };
console.log(newObj); // {0: 1, 1: 3, 2: 3}

Trellis Gatsby Configuration

diff --git a/deploy-hooks/build-before.yml b/deploy-hooks/build-before.yml
--- a/deploy-hooks/build-before.yml
+++ b/deploy-hooks/build-before.yml
+- name: Install npm dependencies
+  command: npm ci
+  connection: local
+  args:
+    chdir: "~/path/to/gatsby"
+
+- name: Compile assets for production
+  command: npm run build
+  connection: local
+  args:
+    chdir: "~/path/to/gatsby"
+
+- name: Copy production assets
+  synchronize:
+    src: "~/path/to/gatsby/public"
+    dest: "{{ deploy_helper.new_release_path }}"
+    group: no
+    owner: no
+    rsync_opts: --chmod=Du=rwx,--chmod=Dg=rx,--chmod=Do=rx,--chmod=Fu=rw,--chmod=Fg=r,--chmod=Fo=r
diff --git a/group_vars/development/wordpress_sites.yml b/group_vars/development/wordpress_sites.yml
--- a/group_vars/development/wordpress_sites.yml
+++ b/group_vars/development/wordpress_sites.yml
@@ -8,6 +8,7 @@ wordpress_sites:
       - canonical: domain.test
     local_path: ../site # path targeting local Bedrock site directory (relative to Ansible root)
     admin_email: admin@domain.test
+    nginx_wordpress_site_conf: templates/domain.com.conf.j2
     multisite:
       enabled: false
     ssl:
diff --git a/group_vars/production/wordpress_sites.yml b/group_vars/production/wordpress_sites.yml
index 48b69b9..99d775f 100644
--- a/group_vars/production/wordpress_sites.yml
+++ b/group_vars/production/wordpress_sites.yml
@@ -9,6 +9,7 @@ wordpress_sites:
     local_path: ../site # path targeting local Bedrock site directory (relative to Ansible root)
     repo: git@bitbucket.org:repo/domain.git # replace with your Git repo URL
     branch: master
+    nginx_wordpress_site_conf: templates/domain.com.conf.j2
     multisite:
       enabled: false
     ssl:
diff --git a/templates/domain.com.conf.j2 b/templates/domain.com.conf.j2
+++ b/templates/domain.com.conf.j2
+{% extends 'roles/wordpress-setup/templates/domain.com.conf.j2' %}
+
+{% block location_primary -%}
+location /wp-json {
+    try_files $uri $uri/ /index.php?$args;
+  }
+  location /wp/ {
+    try_files $uri $uri/ /wp/wp-admin/;
+  }
+  location / {
+    root       {{ www_root }}/{{ item.key }}/{{ item.value.current_path | default('current') }}/public;
+    error_page 404 /404.html;
+  }
+{% endblock %}

Get List of Commits

const { findGitRepos, getCommitsFromRepos } = require ('./gitutils');

findGitRepos(['~/Code/Valtech'], 5, (err, result) => {
    if (err) throw err;

    getCommitsFromRepos(result, 30, (err, result) => {
        if (err) throw err;

        console.log(result);
    });
});
// Stolen from here:
// https://github.com/notwaldorf/tiny-care-terminal/blob/78e038069f01c36148d7d486d7775275d3df1df8/gitbot.js
const resolve = require('resolve-dir');
const subdirs = require('subdirs');
const isGit = require('is-git');
const gitlog = require('gitlog');
const path = require('path');
const async = require("async");
const git = require('git-utils');

try {
  const gitUsername = require('git-user-name')();
} catch(err) {
  console.error(`ERROR reading git-config.
    Use e.g. 'git config --global user.name "Mona Lisa"'.
    See 'man git config' for further information.
  `);
  return process.exit(0);
}

/**
 * Go through all `repos` and look for subdirectories up to a given `depth`
 * and look for repositories.
 * Calls `callback` with array of repositories.
 */
function findGitRepos(repos, depth, callback) {
  let allRepos = [];
  async.each(repos, (repo, repoDone) => {
    repo = resolve(repo);
    subdirs(repo, depth, (err, dirs) => {
      if (err) {
        switch (err.code) {
          case 'ENOENT':
            return callback(`Could not open directory directory: ${err.path}n`, null);
          case 'EACCES':
            return; //ignore if no access
          default:
            return callback(`Error "${err.code}" doing "${err.syscall}" on directory: ${err.path}n`, null);
        }
      }
      if (dirs) dirs.push(repo);
      async.each(dirs, (dir, dirDone) => {
        isGit(dir, (err, isGit) => {
          if (err) {
            return callback(err, null);
          }
          if (!dir.includes('.git') && isGit) {
            allRepos.push(dir);
          }
          dirDone();
        });
      }, repoDone);
    });
  }, err => {
    callback(err, allRepos.sort().reverse());
  });
}

/**
 * returns all commits of the last given `days`.
 * Calls `callback` with line-seperated-strings of the formatted commits.
 */
function getCommitsFromRepos(repos, days, callback) {
  let cmts = [];
  async.each(repos, (repo, repoDone) => {
    let localGitUsername = '';
    try {
      const gitUtilsRepo = git.open(repo);
      localGitUsername = gitUtilsRepo.getConfigValue('user.name') || gitUsername;
    } catch (err) {
      localGitUsername = gitUsername;
    }
    try {
      gitlog({
        repo: repo,
        all: true,
        number: 100, //max commit count
        since: `${days} days ago`,
        fields: ['abbrevHash', 'subject', 'authorDate', 'authorName'],
        author: localGitUsername
      }, (err, logs) => {
        // Error
        if (err) {
          callback(`Oh noes­čś▒nThe repo ${repo} has failed:n${err}`, null);
        }
        // Find user commits
        let commits = [];
        logs.forEach(c => {
          // filter simple merge commits
          if (c.status && c.status.length)
            commits.push(`${c.abbrevHash} - ${c.subject} (${c.authorDate}) <${c.authorName.replace('@end@n','')}>`);
        });

        // Add repo name and commits
        if (commits.length >= 1) {
          // Repo name
          cmts.push(repo);
          cmts.push(...commits);
        }

        repoDone();
      });
    } catch(err) {
      callback(err, null);
    }
  }, err => {
    callback(err, cmts.length > 0 ? cmts.join('n') : "Nothing yet. Start small!");
  });
}

module.exports.findGitRepos = findGitRepos;
module.exports.getCommitsFromRepos = getCommitsFromRepos;

What Order Does This Execute In?

setTimeout(() => {
    console.log('inside timeout');
}, 0);

process.nextTick(() => {
    console.log('inside nextTick');
});

const p = new Promise(function(resolve, reject) {
    console.log('inside promise');

    resolve();
})
    .then(() => console.log('inside then'));

console.log('after promise');