From 339de0457cbc1cbc463c3b6a827f3febe14356d3 Mon Sep 17 00:00:00 2001 From: James Mills Date: Sun, 12 Mar 2023 04:13:53 +0000 Subject: [PATCH] Refactor a bunch of stuff adding zs serve, CI/CD workflows, fixing docs and license (#10) Fixes #5 Fixes #8 Fixes #9 Co-authored-by: James Mills <1290234+prologic@users.noreply.github.com> Reviewed-on: https://git.mills.io/prologic/zs/pulls/10 --- .dockerfiles/entrypoint.sh | 9 ++ .drone.yml | 81 +++++++++++++ .gitignore | 2 +- Dockerfile | 60 ++++++++++ LICENSE | 36 +++--- LICENSE.old | 22 ++++ Makefile | 103 ++++++++++++++-- README.md | 72 +++++++----- go.mod | 12 +- go.sum | 27 +++++ main.go | 235 ++++++++++++++++++++++++------------- preflight.sh | 145 +++++++++++++++++++++++ 12 files changed, 660 insertions(+), 144 deletions(-) create mode 100755 .dockerfiles/entrypoint.sh create mode 100644 .drone.yml create mode 100644 Dockerfile create mode 100644 LICENSE.old create mode 100755 preflight.sh diff --git a/.dockerfiles/entrypoint.sh b/.dockerfiles/entrypoint.sh new file mode 100755 index 0000000..d4da611 --- /dev/null +++ b/.dockerfiles/entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +[ -n "${PUID}" ] && usermod -u "${PUID}" zs +[ -n "${PGID}" ] && groupmod -g "${PGID}" zs + +printf "Configuring zs...\n" + +printf "Switching UID=%s and GID=%s\n" "${PUID}" "${PGID}" +exec su-exec zs:zs "$@" diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..8c43c0e --- /dev/null +++ b/.drone.yml @@ -0,0 +1,81 @@ +--- +kind: pipeline +type: exec +name: ๐Ÿš€ CI + +platform: + os: linux + arch: amd64 + +steps: + - name: ๐Ÿ› ๏ธ Build + commands: + - make build + + - name: ๐Ÿงช Test + commands: + - make test + +trigger: + branch: + - main + event: + - tag + - push + - pull_request + +--- +kind: pipeline +name: ๐Ÿณ Docker + +steps: + - name: ๐Ÿ“ฆ Image + image: plugins/kaniko + settings: + repo: prologic/saltyim + tags: latest + build_args: + - VERSION=latest + - COMMIT=${DRONE_COMMIT_SHA:0:8} + username: + from_secret: dockerhub_username + password: + from_secret: dockerhub_password + when: + branch: + - main + event: + - push + +depends_on: + - ๐Ÿš€ CI + +trigger: + branch: + - main + event: + - push + +--- +kind: pipeline +name: ๐Ÿฅณ Done + +steps: + - name: ๐Ÿ”” Notify + image: plugins/webhook + settings: + urls: + - https://msgbus.mills.io/ci.mills.io + +depends_on: + - ๐Ÿš€ CI + - ๐Ÿณ Docker + +trigger: + branch: + - main + event: + - tag + - push + - pull_request + diff --git a/.gitignore b/.gitignore index ec76799..9638f0d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ /zs /dist -/test.md +**/.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c78862d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,60 @@ +# Build +FROM golang:alpine AS build + +RUN apk add --no-cache -U build-base git + +RUN mkdir -p /src + +WORKDIR /src + +# Copy Makefile +COPY Makefile ./ + +# Install deps +RUN make deps + +# Copy go.mod and go.sum and install and cache dependencies +COPY go.mod . +COPY go.sum . + +# Download dependencies +RUN go mod download + +# Copy sources +COPY *.go ./ + +# Version/Commit (there there is no .git in Docker build context) +# NOTE: This is fairly low down in the Dockerfile instructions so +# we don't break the Docker build cache just be changing +# unrelated files that actually haven't changed but caused the +# COMMIT value to change. +ARG VERSION="0.0.0" +ARG COMMIT="HEAD" +ARG BUILD="" + +# Build cli binary +RUN make cli VERSION=$VERSION COMMIT=$COMMIT BUILD=$BUILD + +# Runtime +FROM alpine:latest + +RUN apk --no-cache -U add su-exec shadow + +ENV PUID=1000 +ENV PGID=1000 + +RUN addgroup -g "${PGID}" zs && \ + adduser -D -H -G zs -h /var/empty -u "${PUID}" zs && \ + mkdir -p /data && chown -R zs:zs /data + +VOLUME /data + +WORKDIR / + +COPY --from=build /src/zs /usr/local/bin/zs + +COPY .dockerfiles/entrypoint.sh /init + +ENTRYPOINT ["/init"] + +CMD ["zs""] diff --git a/LICENSE b/LICENSE index c68b526..bfe2bdd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,22 +1,22 @@ -The MIT License (MIT) +Copyright (C) 2021-present James Mills -Copyright (c) 2014 zserge +zs is covered by the MIT license:: -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, +and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/LICENSE.old b/LICENSE.old new file mode 100644 index 0000000..c68b526 --- /dev/null +++ b/LICENSE.old @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014 zserge + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/Makefile b/Makefile index b1b1fe8..2d3cd73 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,92 @@ -destdir ?= -prefix ?= /usr/local +-include environ.inc +.PHONY: help deps dev build install image release test clean -build: - go build -v -clean: - rm -f zs -install: - install -m0755 zs ${destdir}${prefix}/bin/zs - install -m0644 zs.1 ${destdir}${prefix}/share/man/man1/zs.1 -uninstall: - rm -f ${prefix}/bin/zs - rm -f ${prefix}/share/man/man1/zs.1 +export CGO_ENABLED=0 +VERSION=$(shell git describe --abbrev=0 --tags 2>/dev/null || echo "$VERSION") +COMMIT=$(shell git rev-parse --short HEAD || echo "$COMMIT") +BRANCH=$(shell git rev-parse --abbrev-ref HEAD) +BUILD=$(shell git show -s --pretty=format:%cI) +GOCMD=go + +DESTDIR=/usr/local/bin + +ifeq ($(LOCAL), 1) +IMAGE := r.mills.io/prologic/zs +TAG := dev +else +IMAGE := prologic/zs +TAG := latest +endif + +all: help + +help: ## Show this help message + @echo "zs - Zen Static site generator" + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[$$()% a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +preflight: ## Run preflight checks to ensure you have the right build tools + @./preflight.sh + +deps: ## Install any required dependencies + +dev : DEBUG=1 +dev : build ## Build debug version of zs (cli) + @./zs -v + +cli: ## Build the zs command-line tool +ifeq ($(DEBUG), 1) + @echo "Building in debug mode..." + @$(GOCMD) build -tags "netgo static_build" -installsuffix netgo \ + -ldflags "\ + -X $(shell go list).Version=$(VERSION) \ + -X $(shell go list).Commit=$(COMMIT) \ + -X $(shell go list).Build=$(BUILD)" \ + . +else + @$(GOCMD) build -tags "netgo static_build" -installsuffix netgo \ + -ldflags "-w \ + -X $(shell go list).Version=$(VERSION) \ + -X $(shell go list).Commit=$(COMMIT) \ + -X $(shell go list).Build=$(BUILD)" \ + . +endif + +build: cli ## Build the cli + +install: build ## Install zs (cli) to $DESTDIR + @install -D -m 755 zs $(DESTDIR)/zs + +ifeq ($(PUBLISH), 1) +image: generate ## Build the Docker image + @docker buildx build \ + --build-arg VERSION="$(VERSION)" \ + --build-arg COMMIT="$(COMMIT)" \ + --build-arg BUILD="$(BUILD)" \ + --platform linux/amd64,linux/arm64 --push -t $(IMAGE):$(TAG) . +else +image: generate + @docker build \ + --build-arg VERSION="$(VERSION)" \ + --build-arg COMMIT="$(COMMIT)" \ + --build-arg BUILD="$(BUILD)" \ + -t $(IMAGE):$(TAG) . +endif + +release: generate ## Release a new version to Gitea + @./tools/release.sh + +fmt: ## Format sources files + @$(GOCMD) fmt ./... + +test: ## Run test suite + @CGO_ENABLED=1 $(GOCMD) test -v -cover -race ./... + +coverage: ## Get test coverage report + @CGO_ENABLED=1 $(GOCMD) test -v -cover -race -cover -coverprofile=coverage.out ./... + @$(GOCMD) tool cover -html=coverage.out + +clean: ## Remove untracked files + @git clean -f -d -x + +clean-all: ## Remove untracked and Git ignored files + @git clean -f -d -X diff --git a/README.md b/README.md index dda5588..c36b04a 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ -# zs +# zs - Zen Static site generator zs is an extremely minimal static site generator written in Go. -It's inspired by `zas` generator, but is even more minimal. - -The name stands for 'zen static' as well as it's my initials. +[![Build Status](https://ci.mills.io/api/badges/prologic/zs/status.svg)](https://ci.mills.io/prologic/zs) ## Features @@ -17,9 +15,19 @@ The name stands for 'zen static' as well as it's my initials. ## Installation -Download the binaries from Github or build it manually: +Download the binaries from [go.mills.io/prologic/zs](https://git.mills.io/prologic/zs): - $ go get git.mills.io/prologic/zs +```console +go get go.mills.io/zs@latest +``` + +Or build from source manaully: + +```console +git clone https://git.mills.io/prologic/zs +cd zs +make install +``` ## Ideology @@ -29,13 +37,16 @@ of your blog/site. Keep all service files (extensions, layout pages, deployment scripts etc) in the `.zs` subdirectory. -Define variables in the header of the content files using [YAML]: +Define variables in the header of the content files using [YAML front matter](https://assemble.io/docs/YAML-front-matter.html): - title: My web site - keywords: best website, hello, world - --- +```markdown +--- +title: My web site +keywords: best website, hello, world +--- - Markdown text goes after a header *separator* +Markdown text goes after a header *separator* +``` Use placeholders for variables and plugins in your markdown or html files, e.g. `{{ title }}` or `{{ command arg1 arg2 }}. @@ -48,16 +59,16 @@ placeholder. Every variable from the content header will be passed via environment variables like `title` becomes `$ZS_TITLE` and so on. There are some special variables: -* `$ZS` - a path to the `zs` executable -* `$ZS_OUTDIR` - a path to the directory with generated files -* `$ZS_FILE` - a path to the currently processed markdown file -* `$ZS_URL` - a URL for the currently generated page +- `$ZS` - a path to the `zs` executable +- `$ZS_OUTDIR` - a path to the directory with generated files +- `$ZS_FILE` - a path to the currently processed markdown file +- `$ZS_URL` - a URL for the currently generated page ## Example of RSS generation Extensions can be written in any language you know (Bash, Python, Lua, JavaScript, Go, even Assembler). Here's an example of how to scan all markdown blog posts and create RSS items: -``` bash +```bash for f in ./blog/*.md ; do d=$($ZS var $f date) if [ ! -z $d ] ; then @@ -81,26 +92,29 @@ done | sort -r -n | cut -d' ' -f2- There are two special plugin names that are executed every time the build happens - `prehook` and `posthook`. You can define some global actions here like -content generation, or additional commands, like LESS to CSS conversion: +content generation, or additional commands, like to minify CSS or Javascript files. - # .zs/post +```bash +#!/bin/sh - #!/bin/sh - lessc < $ZS_OUTDIR/styles.less > $ZS_OUTDIR/styles.css - rm -f $ZS_OUTDIR/styles.css +set -e + +minify -o "$ZS_OUTDIR/css/fa.min.css" "$ZS_OUTDIR/css/fa.css" +minify -o "$ZS_OUTDIR/css/site.min.css" "$ZS_OUTDIR/css/site.css" + +rm -rf "$ZS_OUTDIR/css/fa.css" +rm -rf "$ZS_OUTDIR/css/screen.css" +``` ## Command line usage -`zs build` re-builds your site. - -`zs build ` re-builds one file and prints resulting content to stdout. - -`zs watch` rebuilds your site every time you modify any file. - -`zs var [var1 var2...]` prints a list of variables defined in the +- `zs build` re-builds your site. +- `zs build ` re-builds one file and prints resulting content to stdout. +- `zs watch` rebuilds your site every time you modify any file. +- `zs var [var1 var2...]` prints a list of variables defined in the header of a given markdown file, or the values of certain variables (even if it's an empty string). ## License -The software is distributed under the MIT license. +`zs` is licensed under the terms of the [MIT License](/LICENSE) and was orignally forked from [zserge/zs](https://github.com/zserge/zs) also licensed under the terms of the [MIT License](/LICENSE.old). diff --git a/go.mod b/go.mod index 636544d..c5e3459 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,18 @@ -module git.mills.io/prologic/zs +module go.mills.io/zs go 1.17 require ( github.com/russross/blackfriday/v2 v2.1.0 + go.mills.io/static v0.0.0-20230312034046-6dff09caed3b gopkg.in/yaml.v2 v2.4.0 ) + +require ( + github.com/NYTimes/gziphandler v1.1.1 // indirect + github.com/cyphar/filepath-securejoin v0.2.3 // indirect + github.com/julienschmidt/httprouter v1.3.0 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect + github.com/unrolled/logger v0.0.0-20201216141554-31a3694fe979 // indirect + golang.org/x/sys v0.0.0-20221010170243-090e33056c14 // indirect +) diff --git a/go.sum b/go.sum index 6fc9007..3ad7232 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,33 @@ +github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/unrolled/logger v0.0.0-20201216141554-31a3694fe979 h1:47+K4wN0S8L3fUwgZtPEBIfNqtAE3tUvBfvHVZJAXfg= +github.com/unrolled/logger v0.0.0-20201216141554-31a3694fe979/go.mod h1:X5DBNY1yIVkuLwJP3BXlCoQCa5mGg7hPJPIA0Blwc44= +go.mills.io/static v0.0.0-20230312034046-6dff09caed3b h1:9mSSHQJztO83b4939B31Z8bCOlvQUei6bRhnJq8eRC0= +go.mills.io/static v0.0.0-20230312034046-6dff09caed3b/go.mod h1:TmaEDwM+IgrCRyMxtVWtmSdoxLP3N6ehBa7AiOZj2Mk= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14 h1:k5II8e6QD8mITdi+okbbmR/cIyEbeXLBhy5Ha4nevyc= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 5335dcc..a281d39 100644 --- a/main.go +++ b/main.go @@ -1,29 +1,53 @@ +// Package main is a command-lint tool `zs` called Zen Static for generating static websites package main import ( "bytes" + "context" "fmt" "io" "io/ioutil" "log" "os" "os/exec" + "os/signal" "path/filepath" "strings" + "syscall" "text/template" "time" "github.com/russross/blackfriday/v2" + "go.mills.io/static" "gopkg.in/yaml.v2" ) const ( - ZSDIR = ".zs" + // ZSDIR is the default directory for storing layouts and extensions + ZSDIR = ".zs" + + // PUBDIR is the default directory for publishing final built content PUBDIR = ".pub" ) +// Vars holds a map of global variables type Vars map[string]string +// NewTicker is a function that wraps a time.Ticker and ticks immediately instead of waiting for the first interval +func NewTicker(d time.Duration) *time.Ticker { + ticker := time.NewTicker(d) + oc := ticker.C + nc := make(chan time.Time, 1) + go func() { + nc <- time.Now() + for tm := range oc { + nc <- tm + } + }() + ticker.C = nc + return ticker +} + // renameExt renames extension (if any) from oldext to newext // If oldext is an empty string - extension is extracted automatically. // If path has no extension - new extension is appended @@ -113,60 +137,62 @@ func getVars(path string, globals Vars) (Vars, string, error) { } delim := "\n---\n" - if sep := strings.Index(s, delim); sep == -1 { + sep := strings.Index(s, delim) + if sep == -1 { return v, s, nil - } else { - header := s[:sep] - body := s[sep+len(delim):] - - vars := Vars{} - if err := yaml.Unmarshal([]byte(header), &vars); err != nil { - fmt.Println("ERROR: failed to parse header", err) - return nil, "", err - } else { - // Override default values + globals with the ones defines in the file - for key, value := range vars { - v[key] = value - } - } - if strings.HasPrefix(v["url"], "./") { - v["url"] = v["url"][2:] - } - return v, body, nil } + + header := s[:sep] + body := s[sep+len(delim):] + + vars := Vars{} + if err := yaml.Unmarshal([]byte(header), &vars); err != nil { + fmt.Println("WARN: failed to parse header", err) + return v, s, nil + } + // Override default values + globals with the ones defines in the file + for key, value := range vars { + v[key] = value + } + if strings.HasPrefix(v["url"], "./") { + v["url"] = v["url"][2:] + } + return v, body, nil } // Render expanding zs plugins and variables func render(s string, vars Vars) (string, error) { - delim_open := "{{" - delim_close := "}}" + openingDelimiter := "{{" + closingDelimiter := "}}" out := &bytes.Buffer{} for { - if from := strings.Index(s, delim_open); from == -1 { + from := strings.Index(s, openingDelimiter) + if from == -1 { out.WriteString(s) return out.String(), nil - } else { - if to := strings.Index(s, delim_close); to == -1 { - return "", fmt.Errorf("Close delim not found") - } else { - out.WriteString(s[:from]) - cmd := s[from+len(delim_open) : to] - s = s[to+len(delim_close):] - m := strings.Fields(cmd) - if len(m) == 1 { - if v, ok := vars[m[0]]; ok { - out.WriteString(v) - continue - } - } - if res, err := run(vars, m[0], m[1:]...); err == nil { - out.WriteString(res) - } else { - fmt.Println(err) - } + } + + to := strings.Index(s, closingDelimiter) + if to == -1 { + return "", fmt.Errorf("Close delim not found") + } + + out.WriteString(s[:from]) + cmd := s[from+len(openingDelimiter) : to] + s = s[to+len(closingDelimiter):] + m := strings.Fields(cmd) + if len(m) == 1 { + if v, ok := vars[m[0]]; ok { + out.WriteString(v) + continue } } + if res, err := run(vars, m[0], m[1:]...); err == nil { + out.WriteString(res) + } else { + fmt.Println(err) + } } } @@ -228,12 +254,12 @@ func buildRaw(path string, w io.Writer) error { } defer in.Close() if w == nil { - if out, err := os.Create(filepath.Join(PUBDIR, path)); err != nil { + out, err := os.Create(filepath.Join(PUBDIR, path)) + if err != nil { return err - } else { - defer out.Close() - w = out } + defer out.Close() + w = out } _, err = io.Copy(w, in) return err @@ -250,51 +276,80 @@ func build(path string, w io.Writer, vars Vars) error { } } -func buildAll(watch bool) { +func buildAll(ctx context.Context, watch bool) { + ticker := NewTicker(time.Second) + defer ticker.Stop() + lastModified := time.Unix(0, 0) modified := false vars := globals() for { - os.Mkdir(PUBDIR, 0755) - filepath.Walk(".", func(path string, info os.FileInfo, err error) error { - // ignore hidden files and directories - if filepath.Base(path)[0] == '.' || strings.HasPrefix(path, ".") { - return nil - } - // inform user about fs walk errors, but continue iteration - if err != nil { - fmt.Println("error:", err) - return nil - } - - if info.IsDir() { - os.Mkdir(filepath.Join(PUBDIR, path), 0755) - return nil - } else if info.ModTime().After(lastModified) { - if !modified { - // First file in this build cycle is about to be modified - run(vars, "prehook") - modified = true + select { + case <-ctx.Done(): + return + case <-ticker.C: + os.Mkdir(PUBDIR, 0755) + filepath.Walk(".", func(path string, info os.FileInfo, err error) error { + // ignore hidden files and directories + if filepath.Base(path)[0] == '.' || strings.HasPrefix(path, ".") { + return nil } - log.Println("build:", path) - return build(path, nil, vars) + // inform user about fs walk errors, but continue iteration + if err != nil { + fmt.Println("error:", err) + return nil + } + + if info.IsDir() { + os.Mkdir(filepath.Join(PUBDIR, path), 0755) + return nil + } else if info.ModTime().After(lastModified) { + if !modified { + // First file in this build cycle is about to be modified + run(vars, "prehook") + modified = true + } + log.Println("build:", path) + return build(path, nil, vars) + } + return nil + }) + if modified { + // At least one file in this build cycle has been modified + run(vars, "posthook") + modified = false } - return nil - }) - if modified { - // At least one file in this build cycle has been modified - run(vars, "posthook") - modified = false + if !watch { + return + } + lastModified = time.Now() } - if !watch { - break - } - lastModified = time.Now() - time.Sleep(1 * time.Second) } } +// serve runs a static web server and builds and continuously watches for changes to rebuild +func serve(ctx context.Context, bind string) error { + os.Mkdir(PUBDIR, 0755) + + svr, err := static.NewServer( + static.WithBind(bind), + static.WithDir(true), + static.WithRoot(PUBDIR), + static.WithSPA(true), + ) + if err != nil { + return err + } + + go svr.Run(ctx) + go buildAll(ctx, true) + + <-ctx.Done() + + return nil +} + func init() { // prepend .zs to $PATH, so plugins will be found before OS commands p := os.Getenv("PATH") @@ -305,15 +360,21 @@ func init() { func main() { if len(os.Args) == 1 { - fmt.Println(os.Args[0], " [args]") + fmt.Printf("%s [args]\n", filepath.Base(os.Args[0])) + os.Exit(1) return } + cmd := os.Args[1] args := os.Args[2:] + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer cancel() + switch cmd { case "build": if len(args) == 0 { - buildAll(false) + buildAll(ctx, false) } else if len(args) == 1 { if err := build(args[0], os.Stdout, globals()); err != nil { fmt.Println("ERROR: " + err.Error()) @@ -322,7 +383,15 @@ func main() { fmt.Println("ERROR: too many arguments") } case "watch": - buildAll(true) + buildAll(ctx, true) + case "serve": + bind := ":8000" + if len(args) > 1 { + bind = args[0] + } + if err := serve(ctx, bind); err != nil { + fmt.Println("ERROR: " + err.Error()) + } case "var": if len(args) == 0 { fmt.Println("var: filename expected") diff --git a/preflight.sh b/preflight.sh new file mode 100755 index 0000000..164f4e1 --- /dev/null +++ b/preflight.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env sh + +set -e + +color() { + fg="$1" + bg="${2}" + ft="${3:-0}" + + printf "\33[%s;%s;%s" "$ft" "$fg" "$bg" +} + +color_reset() { + printf "\033[0m" +} + +ok() { + if [ -t 1 ]; then + printf "%s[ OK ]%s\n" "$(color 37 42m 1)" "$(color_reset)" + else + printf "%s\n" "[ OK ]" + fi +} + +err() { + if [ -t 1 ]; then + printf "%s[ ERR ]%s\n" "$(color 37 41m 1)" "$(color_reset)" + else + printf "%s\n" "[ ERR ]" + fi +} + +run() { + retval=0 + logfile="$(mktemp -t "run-XXXXXX")" + if "$@" 2> "$logfile"; then + ok + else + retval=$? + err + cat "$logfile" || true + fi + rm -rf "$logfile" + return $retval +} + +progress() { + printf "%-40s" "$(printf "%s ... " "$1")" +} + +log() { + printf "%s\n" "$1" +} + +log2() { + printf "%s\n" "$1" 1>&2 +} + +error() { + log "ERROR: ${1}" +} + +fail() { + log "FATAL: ${1}" + exit 1 +} + +check_goversion() { + progress "Checking Go version" + + if ! command -v go > /dev/null 2>&1; then + log2 "Cannot find the Go compiler" + return 1 + fi + + gover="$(go version | grep -o -E 'go[0-9]+\.[0-9]+(\.[0-9]+)?')" + + if ! go version | grep -E 'go1\.1[6789](\.[0-9]+)?' > /dev/null; then + log2 "Go 1.16+ is required, found ${gover}" + return 1 + fi + + return 0 +} + +check_path() { + progress "Checking \$PATH" + + gobin="$(eval "$(go env | grep GOBIN)")" + gopath="$(eval "$(go env | grep GOPATH)")" + + if [ -n "$gobin" ] && ! echo "$PATH" | grep "$gobin" > /dev/null; then + log2 "\$GOBIN '$gobin' is not in your \$PATH" + return 1 + fi + + if [ -n "$gopath" ] && ! echo "$PATH" | grep "$gopath/bin" > /dev/null; then + log2 "\$GOPATH/bin '$gopath/bin' is not in your \$PATH" + return 1 + fi + + if ! echo "$PATH" | grep "$HOME/go/bin" > /dev/null; then + log2 "\$HOME/go/bin is not in your \$PATH" + return 1 + fi + + return 0 +} + +check_deps() { + progress "Checking deps" + + if ! command -v minify > /dev/null 2>&1; then + log2 "minify not found, Try running: make deps" + return 1 + fi + + if ! minify --help 2>&1 | grep '\-b, \-\-bundle' > /dev/null; then + log2 "wrong version of minify found, Try running: make deps" + return 1 + fi + + if ! command -v goi18n > /dev/null 2>&1; then + log2 "goi18n not found, Try running: make deps" + return 1 + fi + + return 0 +} + +steps="check_goversion check_path check_deps" + +_main() { + for step in $steps; do + if ! run "$step"; then + fail "๐Ÿ™ preflight failed" + fi + done + + log "๐Ÿฅณ All Done! Ready to build, run: make build" +} + +if [ -n "$0" ] && [ x"$0" != x"-bash" ]; then + _main "$@" +fi