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
This commit is contained in:
James Mills 2023-03-12 04:13:53 +00:00
parent 0f4623afae
commit 339de0457c
12 changed files with 660 additions and 144 deletions

9
.dockerfiles/entrypoint.sh Executable file
View File

@ -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 "$@"

81
.drone.yml Normal file
View File

@ -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

2
.gitignore vendored
View File

@ -4,4 +4,4 @@
/zs
/dist
/test.md
**/.DS_Store

60
Dockerfile Normal file
View File

@ -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""]

36
LICENSE
View File

@ -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.

22
LICENSE.old Normal file
View File

@ -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.

103
Makefile
View File

@ -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

View File

@ -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 <file>` re-builds one file and prints resulting content to stdout.
`zs watch` rebuilds your site every time you modify any file.
`zs var <filename> [var1 var2...]` prints a list of variables defined in the
- `zs build` re-builds your site.
- `zs build <file>` re-builds one file and prints resulting content to stdout.
- `zs watch` rebuilds your site every time you modify any file.
- `zs var <filename> [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).

12
go.mod
View File

@ -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
)

27
go.sum
View File

@ -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=

235
main.go
View File

@ -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], "<command> [args]")
fmt.Printf("%s <command> [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")

145
preflight.sh Executable file
View File

@ -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