GithubHelp home page GithubHelp logo

adhocteam / pushup Goto Github PK

View Code? Open in Web Editor NEW
831.0 18.0 30.0 2.66 MB

Pushup is for making modern, page-oriented web apps in Go

Home Page: https://pushup.adhoc.dev

License: MIT License

Makefile 0.25% Go 95.18% Nix 0.76% CSS 1.35% Vim Script 2.34% Dockerfile 0.12%
go http www

pushup's Introduction

Pushup - a page-oriented web framework for Go

workflow status Contributor Covenant

Project status

Pushup is an experiment. In terms of the development life cycle, it should be considered preview pre-release software: it is largely functional, likely has significant bugs (including potential for data loss) and/or subpar performance, but is suitable for demos and testing. It has a decent unit test suite, including fuzzing test cases for the parser. Don't count on it for anything serious yet, and expect significant breaking changes.

screenshot of syntax highlighting of an example Pushup page

Pushup is an experimental new project that is exploring the viability of a new approach to web frameworks in Go.

Pushup seeks to make building page-oriented, server-side web apps using Go easy. It embraces the server, while acknowledging the reality of modern web apps and their improvements to UI interactivity over previous generations.

What is Pushup?

Pushup is a program that compiles projects developed with the Pushup markup language into standalone web app servers.

There are three main aspects to Pushup:

  1. An opinionated project/app directory structure that enables file-based routing,
  2. A lightweight markup alternative to traditional web framework templates that combines Go code for control flow and imperative, view-controller-like code with HTML markup, and
  3. A compiler that parses that markup and generates pure Go code, building standalone web apps on top of the Go stdlib net/http package.

Pages in Pushup

The core object in Pushup is the "page": a file with the .up extension that is a mix of HTML, Go code, and a lightweight markup language that glues them together. Pushup pages participate in URL routing by virtue of their path in the filesystem. Pushup pages are compiled into pure Go which is then built along with a thin runtime into a standalone web app server (which is all net/http under the hood).

The main proposition motivating Pushup is that the page is the right level of abstraction for most kinds of server-side web apps.

The syntax of the Pushup markup language looks like this:


^import "time"

^{
    title := "Hello, from Pushup!"
}

<h1>^title</h1>

<p>The time is now ^time.Now().String().</p>

^if time.Now().Weekday() == time.Friday {
    <p>It's Friday! Enjoy the start to your weekend.</p>
} ^else {
    <p>Have a great day, we're glad you're here.</p>
}

You would then place this code in a file somewhere in your app/pages directory, like hello.up. The .up extension is important and tells the compiler that it is a Pushup page. Once you build and run your Pushup app, that page is automatically mapped to the URL path /hello.

Quick start with Docker

git clone https://github.com/adhocteam/pushup.git
cd pushup
make build-docker

Then create a scaffolded new project in the current directory:

docker run --rm -v $(pwd):/usr/src/app --user $(id -u):$(id -g) pushup new myproject
cd myproject
docker run --rm -v $(pwd):/usr/src/app --user $(id -u):$(id -g) -p 8080:8080 pushup run

See Creating a new Pushup project for more information.

Getting started

To make a new Pushup app, first install the main Pushup executable.

Installing Pushup

Prerequisites

  • go 1.18 or later

Make sure the directory where the go tool installs executables is in your $PATH. It is $(go env GOPATH)/bin. You can check if this is the case with:

echo $PATH | grep $(go env GOPATH)/bin > /dev/null && echo yes || echo no

Install an official release

Download Pushup for your platform from the releases page.

Install via Nix

Pushup is available via the Nix package manager. It is currently in the unstable channel.

  • Add to your local profile
$ nix-env -iA nixpkgs.pushup
  • Create a temporary shell
$ nix-shell -p pushup
  • Create a temporary shell (flakes)
$ nix shell nixpkgs#pushup
  • Run Pushup without installing (flakes)
$ nix run nixpkgs#pushup

Install via git

git clone [email protected]:AdHocRandD/pushup.git
cd pushup
make

Install via go install

Make sure you have Go installed (at least version 1.18), and type:

go install github.com/adhocteam/pushup@latest

Creating a new Pushup project

To create a new Pushup project, use the pushup new command.

pushup new

Without any additional arguments, it will attempt to create a scaffolded new project in the current directory. However, the directory must be completely empty, or the command will abort. To simulataneously make a new directory and generate a scaffolded project, pass a relative path as argument:

pushup new myproject

The scaffolded new project directory consists of a directory structure for .up files and auxiliary project Go code, and a go.mod file.

Change to the new project directory if necessary, then do a pushup run, which compiles the Pushup project to Go code, builds the app, and starts up the server.

pushup run

If all goes well, you should see a message on the terminal that the Pushup app is running and listening on a port:

↑↑ Pushup ready and listening on 0.0.0.0:8080 ↑↑

By default it listens on port 8080, but with the -port or -unix-socket flags you can pick your own listener.

Open http://localhost:8080/ in your browser to see the default layout and a welcome index page.

Example demo app

See the example directory for a demo Pushup app that demonstrates many of the concepts in Pushup and implements a few small common patterns like some HTMX examples and a simple CRUD app.

Click on "view source" at the bottom of any page in the example app to see the source of the .up page for that route, including the source of the "view source" .up page itself. This is a good way to see how to write Pushup syntax.

Go modules and Pushup projects

Pushup treats projects as their own self-contained Go module. The build process assumes this is the case by default. But it is possible to include a Pushup project as part of a parent Go module. See the the -module option to pushup new.

Project directory structure

Pushup projects have a particular directory structure that the compiler expects before building. The most minimal Pushup project would look like:

app
├── layouts
├── pages
│   └── index.up
├── pkg
└── static
go.mod

Pages

Pushup pages are the main units in Pushup. They are a combination of logic and content. It may be helpful to think of them as both the controller and the view in a MVC-like system, but colocated together in the same file.

They are also the basis of file-based routing: the name of the Pushup file, minus the .up extension, is mapped to the portion of the URL path for routing.

Layouts

Layouts are HTML templates that used in common across multiple pages. They are just HTML, with Pushup syntax as necessary. Each page renders its contents, and then the layout inserts the page contents into the template with the ^outputSection("contents") Pushup expression.

Static media

Static media files like CSS, JS, and images, can be added to the app/static project directory. These will be embedded directly in the project executable when it is built, and are accessed via a straightforward mapping under the "/static/" URL path.

File-based routing

Pushup maps file locations to URL route paths. So about.up becomes /about, and foo/bar/baz.up becomes /foo/bar/baz. More TK ...

You can print a list of the app's routes with the command:

pushup routes

Dynamic routes

If the filename of a Pushup page starts with a $ dollar sign, the portion of the URL path that matches will be available to the page via the getParam() Pushup API method.

For example, let's say there is a Pushup page at app/pages/people/$id.up. If a browser visits the URL /people/1234, the page can access it like a named parameter with the API method getParam(), for example:

<p>ID: ^getParam(req, "id")</p>

would output:

<p>ID: 1234</p>

The name of the parameter is the word following the $ dollar sign, up to a dot or a slash. Conceptually, the URL route is /people/:id, where :id is the named parameter that is substituted for the actual value in the request URL.

Directories can be dynamic, too. app/pages/products/$pid/details.up maps to /products/:pid/details.

Multiple named parameters are allowed, for example, app/pages/users/$uid/projects/$pid.up maps to /users/:uid/projects/:pid.

Enhanced hypertext

Inline partials

Inline partials allow pages to denote subsections of themselves, and allow for these subsections (the inline partials) to be rendered and returned to the client independently, without having to render the entire enclosing page.

Typically, partials in templating languages are stored in their own files, which are then transcluded into other templates. Inline partials, however, are partials declared and defined in-line a parent or including template.

Inline partials are useful when combined with enhanced hypertext solutions (eg., htmx). The reason is that these sites make AJAX requests for partial HTML responses to update portions of an already-loaded document. Partial responses should not have enclosing markup such as base templates applied by the templating engine, since that would break the of the document they are being inserted into. Inline partials in Pushup automatically disable layouts so that partial responses have just the content they define.

The ability to quickly define partials, and not have to deal with complexities like toggling off layouts, makes it easier to build enhanced hypertext sites.

Basic web framework functionality

All modern web frameworks should implement a standard set of functionality, spanning from safety to convenience. As of this writing, Pushup does not yet implement them all, but aspires to prior to any public release.

Escaping

By default, all content is HTML-escaped, so in general it is safe to directly place user-supplied data into Pushup pages. (Note that the framework does not (yet) do this in your Go code, data from form submissions and URL queries should be validated and treated as unsafe.)

For example, if you wanted to display on the page the query a user searched for, this is safe:

^{ query := req.FormValue("query") }
<p>You search for: <b>^query</b></p>

Pushup syntax

How it works

Pushup is a mix of a new syntax consisting of Pushup directives and keywords, Go code, and HTML markup.

Parsing a .up file always starts out in HTML mode, so you can just put plain HTML in a file and that's a valid Pushup page.

When the parser encounters a '^' character (caret, ASCII 0x5e) while in HTML mode, it switches to parsing Pushup syntax, which consists of simple directives, control flow statements, block delimiters, and Go expressions. It then switches to the Go code parser. Once it detects the end of the directive, statement, or expression, it switches back to HTML mode, and parsing continues in a similar fashion.

Pushup uses the tokenizers from the go/scanner and golang.org/x/net/html packages, so it should be able to handle any valid syntax from either language.

Directives

^import

Use ^import to import a Go package into the current Pushup page. The syntax for ^import is the same as a regular Go import declaration

Example:

^import "strings"
^import "strconv"
^import . "strings"

^layout

Layouts are HTML templates that enclose the contents of a Pushup page.

The ^layout directive instructs Pushup what layout to apply the contents of the current page.

The name of the layout following the directive is the filename in the layouts directory minus the .up extension. For example, ^layout main would try to apply the layout located at app/layouts/main.up.

^layout is optional - if it is not specified, pages automatically get the "default" layout (app/layouts/default.up).

Example:

^layout homepage
^layout ! - no layout

A page may choose to have no layout applied - that is, the contents of the page itself are sent directly to the client with no enclosing template. In this case, use the ! name:

^layout !

Go code blocks

^{

To include statements of Go in a Pushup page, type ^{ followed by your Go code, terminating with a closing }.

The scope of a ^{ ... } in the compiled Go code is equal to its surrounding markup, so you can define a variable and immediately use it:

^{
	name := "world"
}
<h1>Hello, ^name!</h1>

Because the Pushup parser is only looking for a balanced closing }, blocks can be one-liners:

^{ name := "world"; greeting := "Hello" }
<h1>^greeting, ^name!</h1>

A Pushup page can have zero or many ^{ ... } blocks.

^handler

A handler is similar to ^{ ... }. The difference is that there may be at most one handler per page, and it is run prior to any other code or markup on the page.

A handler is the appropriate place to do "controller"-like (in the MVC sense) actions, such as HTTP redirects and errors. In other words, any control flow based on the nature of the request, for example, redirecting after a successful POST to create a new object in a CRUD operation.

Example:

^handler {
    if req.Method == "POST" && formValid(req) {
		if err := createObjectFromForm(req.Form); err == nil {
			return http.Redirect(w, req, "/success/", http.StatusSeeOther)
			return nil
		} else {
			// error handling
			...
	}
	...
}
...

Note that handlers (and all Pushup code) run in a method on a receiver that implements Pushup's Responder interface, which is

interface Responder {
	Respond(http.ResponseWriter, *http.Request) error
}

To exit from a page early in a handler (i.e., prior to any normal content being rendered), return from the method with a nil (for success) or an error (which will in general respond with HTTP 500 to the client).

Control flow statements

^if

^if takes a boolean Go expression and a block to conditionally render.

Example:

^if query := req.FormValue("query"); query != "" {
	<p>Query: ^query</p>
}

^for

^for takes a Go "for" statement condition, clause, or range, and a block, and repeatedly executes the block.

Example:

^for i := 0; i < 10; i++ {
	<p>Number ^i</p>
}

Expressions

Simple expressions

Simple Go expressions can be written with just ^ followed by the expression. "Simple" means:

  • variable names (eg., ^x)
  • dotted field name access of structs (eg., ^account.name)
  • function and method calls (eg., ^strings.Repeat("x", 3))
  • index expressions (eg., a[x])

Example:

^{ name := "Paul" }
<p>Hello, ^name!</p>

Outputs:

<p>Hello, Paul!</p>

Notice that the parser stops on the "!" because it knows it is not part of a Go variable name.

Example:

<p>The URL path: ^req.URL.Path</p>

Outputs:

<p>The URL path: /foo/bar</p>

Example:

^import "strings"
<p>^strings.Repeat("Hello", 3)</p>

Outputs:

<p>HelloHelloHello</p>

Explicit expressions

Explicit expressions are written with ^ and followed by any valid Go expression grouped by parentheses.

Example:

^{ numPeople := 4 }
<p>With ^numPeople people there are ^(numPeople * 2) hands</p>

Outputs:

<p>With 4 people there are 8 hands</p>

Layout and templates

^section

Pushup layouts can have sections within the HTML document that Pushup pages can define with their own content to be rendered into those locations.

For example, a layout could have a sidebar section, and each page can set its own sidebar content.

In a Pushup page, sections are defined with the keyword like so:

^section sidebar {
    <article>
        <h1>This is my sidebar content</h1>
        <p>More to come</p>
    </article>
}

Layouts can output sections with the outputSection function.

<aside>
    ^outputSection("sidebar")
</aside>

Layouts can also make sections optional, by first checking if a page has set a section with sectionDefined(), which returns a boolean.

^if sectionDefined("sidebar") {
    <aside>
        ^outputSection("sidebar")
    </aside>
}

Checking for if a section was set by a page lets a layout designer provide default markup that can be overridden by a page.

^if sectionDefined("title") {
    <title>
        ^outputSection("title")
    </title>
} ^else {
    <title>Welcome to our site</title>
}

^partial

Pushup pages can declare and define inline partials with the ^partial keyword.

...
<section>
    <p>Elements</p>
    ^partial list {
            <ul>
                <li>Ag</li>
                <li>Na</li>
                <li>C</li>
            </ul>
    }
</section>
...

A request to the page containing the initial partial will render normally, as if the block where not wrapped in ^partial list { ... }.

A request to the page with the name of the partial appended to the URL path will respond with just the content scoped by the partial block.

For example, if the page above had the route /elements/, then a request to /elements/list would output:

<ul>
    <li>Ag</li>
    <li>Na</li>
    <li>C</li>
</ul>

Inline partials can nest arbitrarily deep.

...
^partial leagues {
    <p>Leagues</p>
    ^partial teams {
        <p>Teams</p>
        ^partial players {
            <p>Players</p>
        }
    }
}
...

To request a nested partial, make sure the URL path is preceded by each containing partial's name and a forward slash, for example, /sports/leagues/teams/players.

Vim syntax file

There is a vim plugin in the vim-pushup directory. You should be able to symlink it into your plugin manager's path. Alternatively, to install it manually:

  • Locate or create a syntax directory in your vim config directory (Usually ~/.vim/syntax for vim or ~/.config/nvim/syntax for neovim)
  • Copy syntax/pushup.vim into that directory
  • Locate or create a ftdetect directory in your vim config directory (Usually ~/.vim/ftdetect for vim or ~/.config/nvim/ftdetect for neovim)
  • Copy ftdetect/pushup.vim into that directory

pushup's People

Contributors

austinmcrane avatar dependabot[bot] avatar dvogel avatar eli-oat avatar fholmqvist avatar gmso avatar halochou avatar llimllib avatar orenfromberg avatar paulsmith avatar shoukoo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

pushup's Issues

File changed logic is not working

Following from the error in #4, I fixed the package name, restarted the server, and it looped on "change detected" even though nothing had changed:

$ pushup run -dev
2022/08/11 10:06:18 adding app to watch
2022/08/11 10:06:18 adding app/layouts to watch
2022/08/11 10:06:18 adding app/pages to watch
2022/08/11 10:06:18 adding app/pkg to watch
↑↑ PUSHUP DEV RELOADER ON http://0.0.0.0:8080 ↑↑
2022/08/11 10:06:19 change detected in project directory, reloading
2022/08/11 10:06:19 FILE CHANGED
2022/08/11 10:06:19 change detected in project directory, reloading
2022/08/11 10:06:19 FILE CHANGED
2022/08/11 10:06:19 change detected in project directory, reloading
2022/08/11 10:06:19 FILE CHANGED
2022/08/11 10:06:19 change detected in project directory, reloading
2022/08/11 10:06:19 FILE CHANGED

etc. Investigating why this happens.

Project name could come from path given to pushup new

I did a pushup new mycoolapp and got a mycoolapp/ folder like I wanted, but my go.mod says module example/myproject and my build artifact is myproject.exe.

I see that I can do pushup new -module mycoolapp mycoolapp, but might be a nice default to take the last item of a split on platform-specific path separator as the module name.

Dev reloader should only reload after error once

Related to #4, if the build of the generated Pushup app fails for any reason, the dev reloader enters an infinite loop, until the user either Ctrl-C's out of it or fixes the build.

The reloader should detect the error condition and wait until the file change notifier reports a change.

User-contributed Go code in app/pkg/*.go has to be package build

This is under-documented at best, and confusing in general.

Ideally the user could name their package whatever they want, but we would have to figure out how the Pushup compiler detects that and also if that breaks any assumptions we have about the generated Go module.

Live reloading should not exit after parse error

Currently if Pushup encounters any error, including a normal parse error while a developer is typing and saving, that causes the program to exit, this also causes the developer live reloading mode to exit as well.

We don't want the reloader to enter an infinite loop where it tries to re-run Pushup after an error condition but before any file change event has occurred. (Which was a previous bug noted in #4).

The desired behavior should be that, upon an error causing Pushup to exit, the reloader should stay running and upon detecting the next file change event, attempt to re-run Pushup.

Friendlier error message on build errors

Related to #4, when the generated Pushup app build fails, we should try to make a friendly error message for the developer, instead of only exposing them to the underlying go tool error.

Wrong package name gives infinite errors

If you use package main instead of package build in a go file in app/pkg, while the server is running in dev mode, I get an infinite list of:

build/cmd/myproject/main.go:18:2: import "example/myproject/build" is a program, not an importable package
build/cmd/myproject/main.go:18:2: import "example/myproject/build" is a program, not an importable package
build/cmd/myproject/main.go:18:2: import "example/myproject/build" is a program, not an importable package
  • Is it possible for us to generate a better error?
  • The dev server should throw the error once and then not throw it again until something has changed

Implement better parse error strategy

The Pushup parser is a hand-written recursive descent parser, and its current strategy for handling syntax errors is to log them to stderr, allow parsing to continue, and only check for an error in the top-level parse() method after attempting to parse the document.

Parsing continues blindly after an error, potentially returning a nil AST node from a parse method, which is risky: we could panic from a nil deference or wind up in some other unexpected state.

A better approach might be to attempt to synchronize on the next statement/keyword. It may be slightly tricky in our case because we have both HTML and Go tokenizers that we switch between depending on context, but should be doable.

Since recursive-descent uses the host lang's call stack for its parse state, if we want to implement synchronizing we need to unwind the stack on a parse error. This may be done in other languages with exceptions. We don't have those in Go, but we could panic at the site of the syntax error and recover in the top-level parse() method, checking for a sentinel value or type so we can distinguish between syntax errors and all other regular panic conditions like slice out of bounds. Go itself does this at least here, and this blog post argues for this as a valid use-case of panic/recover as a quasi-exception system.

Compile pages in parallel

Pushup pages don't depend on each other and so are a good candidate for compilation in parallel in separate threads.

Make Pushup apps easy to import into larger Go projects

Pushup compiles down to Go code in its own package that should be easy to import in other Go projects that would like to get the features Pushup provides. Right now the reality is it's a little messy to do that.

This is a tracking issue but I think there are 3 things necessary to make this a reality:

  1. URL routing - right now Pushup assumes it is at the root of a URL path. This should be straightforward to deal with with http.ServeMux and http.StripPrefix, but there should be an API for it.
  2. Setup code in main.go - in _runtime/cmd/main.go is code for importing from the build directory and running a Pushup app as a standard Go http server. This should be factored out into its own API.
  3. More control over output & build dir - we haven't exercised the controls for specifying non-default output params. This includes the build directory location, as well as the ability to specify the name of the generated Go package (this is in fact not exposed yet).

Then I think we need an example project that demonstrates a Pushup app can be imported like a regular Go package/module.

inline partials inside ^if block are broken

The codegen for partials is finicky, so it is not surprising that the initial pass on it would not work in all cases. There is a composition bug where an inline partial can't be conditional inside an ^if, because of the way top-level content rendering is handled.

Consider features to support static asset caching

Not sure if this is a use case we're concerned with here, but I think there'd need to be something like Django's collectstatic for apps more complex than a toy app. That way we could have hashed filenames to support asset caching and we wouldn't need to check built assets into source control.

The collectstatic approach would work something like:

  • add a way to configure pushup with paths to reference for production-ready static assets, and a desired public path where assets should be located in deployed environments
  • in the pushup template language, add some way of referencing paths to frontend assets relative to the public path
  • pushup build or a new pushup collectstatic command md5's the assets at the source path and produces a public path URL with a hashed filename

HTTP 4xx-5xx status code pages

Pushup currently responds with short plain text status message when handling a 404 or 500. It should detect whether the project has page(s) to respond with. These should be in well-known locations (app/pages/4xx.up and app/pages/5xx.up perhaps?)

Pdeathsig doesn't exist on mac

go build fails on mac because Pdeathsig doesn't exist on mac or openBSD:

$ go build
go: downloading golang.org/x/net v0.0.0-20220706163947-c90051bbdb60
go: downloading golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
go: downloading github.com/fsnotify/fsnotify v1.5.4
go: downloading golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a
# github.com/AdHocRandD/pushup
./main.go:830:3: unknown field 'Pdeathsig' in struct literal of type syscall.S

else if

Pushup needs ^elseif to round out ^if.

Fix -out-dir flag

Passing -out-dir to pushup run allows one to override the build directory, but the subsequent run phase doesn't pick it up.

Inline partials

Inline partials would allow pages to denote subsections of themselves and allow for the subsections (the inline partials) to be rendered and returned to the client independently without having to render the entire enclosing page.

This would make enhanced hypertext solutions such as htmx trivial to support out of the box in Pushup. The reason is that these systems make AJAX requests for partial HTML responses in order to update portions of an already-loaded document. So the ability to easily and precisely define partials (and not have to deal with complexities like toggling off layouts) would be very helpful in building these types of sites.

Typically, partials are a concept in templating languages where the partial template is stored in its own file which is then transcluded into another template. Inline partials however are partials declared and defined inline some parent or including template. So how would they work and why might this be useful?

For example, let's say we have a Pushup page, architects/index.pushup. That page would be routed via the URL path /architects/. It might look like (note the ^partial keyword):

^{ architects := []string {"Mies van der Rohe", "Jeanne Gang", "Le Corbusier" }

<article>
  <h1>Architects</h1>
  ^partial list {
    <ul>
      ^for _, architect := range architects {
        <li>^architect</li>
      }
    </ul>
  }
</article>

We could map the name of the partial, in this case list, to the end of the page's URL route, giving /architects/list. A client could request /architects/list, and get a response with contents of the list partial only, no enclosing content or layout applied.

(Note that this requires us to determine how to handle having an inline partial and a page named the same (in this case, architects/list.pushup) - whether that is statically disallowed (a compiler error) or one has precedence over the other (with a compiler warning to alert the programmer to the collision), or some other resolution.)

A different page could use htmx to bring in the inline partial content they want:

...
<aside>
  <h3>Architects</h3>
  <div hx-get="/architects/list" hx-trigger="revealed"></div>
</aside>
...

The original page itself could leverage partials for htmx as well. Let's say the list of architects is paginated.

...
  ^partial list {
    <div>
      <ul>
        ^for _, architect := range architects {
          <li>^architect</li>
        }
      </ul>
      <button hx-get="/architects/list?page=^(page + 1)" hx-target="closest div">Next page</button>
    </div>
  }
...

Pushup currently has sections, which allows layouts to render content from pages in specific parts of the HTML template. This is a related concept in that it involves portions of a larger template, but isn't connected to the functionality of inline partials.

Embed source code

Give option to embed Pushup source code in the built executable, and expose to Pushup users via API.

Trailing slash or no

#30 brought up how handling a trailing slash should work. Should it be the default to have a trailing slash in URL paths, and redirect if a request would have matched but for the missing trailing slash, like for example how it works in Django? The current Pushup way is to match routes without a trailing slash, and then if one is present, redirect to one that would match with it removed.

Django way:

request /foo redirects to /foo/

(Current) Pushup way:

request /foo/ redirects to /foo

index pages in subdirs don't work

  1. Create a page at app/test/index.pushup
  2. Try to visit the /test/ URL, expecting the page to render
  3. note that it doesn't work

When there's an index.pushup file in a subdirectory, pushup should serve that page at the root of the subdirectory

Performance optimizations re: network thresholds

There's different thresholds that avoid extra network latency increments:

  • If you can get it under 1500 bytes you can get it into a single ethernet frame and avoid electrical signal delay.
  • If you can get it under 9000 bytes then you can get it into a single jumbo frame and (sometimes, partially) avoid routing delay.
  • If you can get it under 64k then you can avoid IP fragmentation delays.

For each of these thresholds, time-to-first-byte could be optimized by flushing the output buffer at the threshold. However time-to-last-byte could be penalized if the full output would have come in under the next threshold anyway.

e.g. if you have a 63k page and you flush after 1500 bytes you'll end up sending two IP packets instead of 1 and the page won't be fully available until the second IP packet arrives whereas without the 1500 byte flush you would not be able to parse as early but you'd probably finish parsing earlier since there would be no need to ACK the first IP packet.

The generated code could be instrumented by the compiler to maintain heuristics for the size of each page. Any pages that usually surpass a threshold could have their output buffer flushed at the lowest threshold to optimize for TTFB.

(caveat: the time-to-last-byte penalty may not matter for HTTP/3 which uses UDP and thus no ACK)

Add CSRF protection

Add cross-site request forgery protection. I like the way Django does it. Since we compile the template the form is in, we could provide automatic rewriting of the HTML to inject the CSRF token.

Ergonomics of path parameters

Given the emphasis on file-based routing, I think it would be good to avoid $ for path parameters. It is very easy to forget to escape it in a case like this:

mv app/pages/$thing.up app/pages/nested/

The obvious choice of ^ also needs to be escaped. The example in the README defers to the more conventional /prefix/:param syntax. That could be an option since : does not need to be escaped. One annoyance is that is in bash it does not autocomplete from mv :<TAB>.


Separate from the filename, I think before committing to this approach, it would be worth a research dive into the question of how to handle URLs like /users/:id/profile/ssh_keys/:key_id

The 404 for this turned out to be an error on my part, too focused on getting the $ escaped that I accidentally named my directory $id.up instead of $id.

Add sections to layouts

A layout should be able to specify a section, which is a Pushup API and syntax that allows pages to, for example, set the HTML <title> element or similar varying across different pages.

This will need some design iteration, but should look roughly like:

<title>^(Section("title"))</title>

Note the use of an explicit expression (transition symbol followed by balanced parens ^(...))

If a page did not reference the section, it would render as

<title></title>

The layout could alternately do something like

^if SectionSet("title") {
    <title>^(Section("title"))</title>
} else {
    <title>My Pushup site</title>
}

For a default value.

<title>My Pushup site</title>

But a page could write something like:

^section title { <text>Foo</text> }

and get

<title>Foo</title>

Within the { } the HTML parser would be in effect.

Add support for static media

There should be a place in the project directory structure for static media: CSS, JS, images, etc. These should be embedded in the executable, and exposed via an HTTP route and handler. There should also be support for dev mode reading directly from the disk.

Better error messages from user-contributed Go code

Project-specific Go code (currently in the app/pkg dir) can fail compilation for all the normal reasons. As a compiler itself, Pushup can preprocess this code and catch common mistakes (such as incorrect package name) and show them to the user in friendlier, Pushup-specific ways than letting it slip to the go tool subprocess.

Define a Pushup API

Currently, users writing .pushup files must somehow know (via docs, looking at source code) a set of variable names and function parameters in the context in which their Go code is executed - these constitute an informal Pushup API, but it is brittle and only inadvertently designed.

Pushup should have a well-defined API and writers of .pushup should use it for functionality and not magic variables.

Add out-of-the-box SQLite support

Pushup should be a "SQLite-first" tool.

  • Library: a pure-Go lib (eg. modernc.org/sqlite) to avoid cgo and allow for easy cross-platform building (or do this)
  • "ORM": As of 1.15.0, sqlc has beta SQLite support
  • Migrations: might DIY something simple

Healthchecks

Pushup should model good ops behavior, so should have healthchecks out of the box. These can be both internal Go code implementations of a healthcheck interface and a registered HTTP route that can be requested by load balancers and proxies.

Consider ergonomics of leading ^ as mode switch

This is the nittiest of nit picky questions, but I wonder if there is an easier to type character to use in place of ^?

Generally, I have little occasion to type ^. Reaching for it feels weird (I've outed myself as not doing any bitwise operations in C, Ruby or Java, nor concatenating strings in OCaml...)

This is by no means a deal breaking issue, but I was immediately struck by how awkward it felt to type ^ when playing with pushup over lunch.

The tests don't pass on main on macOS

I started to look into it, but the TestPushup function is pretty hard to understand

$ go test ./...
--- FAIL: TestPushup (8.10s)
    --- FAIL: TestPushup/$name.pushup (1.19s)
        --- FAIL: TestPushup/$name.pushup/0.conf (0.65s)
            main_test.go:256: stderr:
                [PUSHUP] GET /testdata/Pushup 200 24.75µs
                2022/08/31 08:24:16 SIGNAL TRAPPED
                [PUSHUP] shutting down gracefully, press Ctrl+C to force immediate
                [PUSHUP] shutdown complete

            main_test.go:257: error: *os.SyscallError wait: no child processes
        --- FAIL: TestPushup/$name.pushup/#00 (0.54s)
            main_test.go:256: stderr:
                [PUSHUP] GET /testdata/world 200 13.875µs
                2022/08/31 08:24:16 SIGNAL TRAPPED
                [PUSHUP] shutting down gracefully, press Ctrl+C to force immediate
                [PUSHUP] shutdown complete

            main_test.go:257: error: *os.SyscallError wait: no child processes
    --- FAIL: TestPushup/attributes.pushup (0.57s)
        --- FAIL: TestPushup/attributes.pushup/#00 (0.57s)
            main_test.go:256: stderr:
                [PUSHUP] GET /testdata/attributes 200 13.792µs
                2022/08/31 08:24:17 SIGNAL TRAPPED
                [PUSHUP] shutting down gracefully, press Ctrl+C to force immediate
                [PUSHUP] shutdown complete

            main_test.go:257: error: *os.SyscallError wait: no child processes
    --- FAIL: TestPushup/handler.pushup (0.54s)
        --- FAIL: TestPushup/handler.pushup/#00 (0.54s)
            main_test.go:256: stderr:
                [PUSHUP] GET /testdata/handler 418 5.833µs
                2022/08/31 08:24:18 SIGNAL TRAPPED
                [PUSHUP] shutting down gracefully, press Ctrl+C to force immediate
                [PUSHUP] shutdown complete

            main_test.go:257: error: *os.SyscallError wait: no child processes
    --- FAIL: TestPushup/if.pushup (0.55s)
        --- FAIL: TestPushup/if.pushup/#00 (0.55s)
            main_test.go:256: stderr:
                [PUSHUP] GET /testdata/if 200 30.334µs
                2022/08/31 08:24:19 SIGNAL TRAPPED
                [PUSHUP] shutting down gracefully, press Ctrl+C to force immediate
                [PUSHUP] shutdown complete

            main_test.go:257: error: *os.SyscallError wait: no child processes
    --- FAIL: TestPushup/index.pushup (0.54s)
        --- FAIL: TestPushup/index.pushup/#00 (0.54s)
            main_test.go:256: stderr:
                [PUSHUP] GET /testdata/ 200 18.875µs
                2022/08/31 08:24:19 SIGNAL TRAPPED
                [PUSHUP] shutting down gracefully, press Ctrl+C to force immediate
                [PUSHUP] shutdown complete

            main_test.go:257: error: *os.SyscallError wait: no child processes
    --- FAIL: TestPushup/panicking.pushup (0.54s)
        --- FAIL: TestPushup/panicking.pushup/#00 (0.54s)
            main_test.go:256: stderr:
                2022/08/31 08:24:20 recovered from panic in an HTTP hander:
                goroutine 34 [running]:
                runtime/debug.Stack()
                	/opt/homebrew/Cellar/go/1.19/libexec/src/runtime/debug/stack.go:24 +0x64
                runtime/debug.PrintStack()
                	/opt/homebrew/Cellar/go/1.19/libexec/src/runtime/debug/stack.go:16 +0x1c
                main.panicRecoveryMiddleware.func1.1()
                	/Users/llimllib/adhoc/innovation/pushup/main/build/cmd/myproject/main.go:126 +0x64
                panic({0x1008d0020, 0x1008ab3c0})
                	/opt/homebrew/Cellar/go/1.19/libexec/src/runtime/panic.go:884 +0x204
                github.com/AdHocRandD/pushup/build.(*TestdataPanickingPage).Respond(0x10092b7d8?, {0x1400020c140?, 0x1008e0d20?}, 0x100b08c50?)
                	panicking.pushup:2 +0x48
                github.com/AdHocRandD/pushup/build.Respond({0x10092b160, 0x140002120a0}, 0x14000226000)
                	/Users/llimllib/adhoc/innovation/pushup/main/build/pushup_support.go:110 +0x27c
                main.pushupHandler({0x10092b160, 0x140002120a0}, 0x14000226000)
                	/Users/llimllib/adhoc/innovation/pushup/main/build/cmd/myproject/main.go:111 +0x1a8
                net/http.HandlerFunc.ServeHTTP(0x1?, {0x10092b160?, 0x140002120a0?}, 0x1007bf450?)
                	/opt/homebrew/Cellar/go/1.19/libexec/src/net/http/server.go:2109 +0x38
                main.requestLogMiddleware.func1({0x10092b340?, 0x1400023e000}, 0x14000226000)
                	/Users/llimllib/adhoc/innovation/pushup/main/build/cmd/myproject/main.go:167 +0xbc
                net/http.HandlerFunc.ServeHTTP(0x0?, {0x10092b340?, 0x1400023e000?}, 0x100d6e128?)
                	/opt/homebrew/Cellar/go/1.19/libexec/src/net/http/server.go:2109 +0x38
                main.panicRecoveryMiddleware.func1({0x10092b340?, 0x1400023e000?}, 0x100d6e03b?)
                	/Users/llimllib/adhoc/innovation/pushup/main/build/cmd/myproject/main.go:130 +0x74
                net/http.HandlerFunc.ServeHTTP(0x14000117ad8?, {0x10092b340?, 0x1400023e000?}, 0x0?)
                	/opt/homebrew/Cellar/go/1.19/libexec/src/net/http/server.go:2109 +0x38
                net/http.(*ServeMux).ServeHTTP(0x0?, {0x10092b340, 0x1400023e000}, 0x14000226000)
                	/opt/homebrew/Cellar/go/1.19/libexec/src/net/http/server.go:2487 +0x140
                net/http.serverHandler.ServeHTTP({0x1400020a150?}, {0x10092b340, 0x1400023e000}, 0x14000226000)
                	/opt/homebrew/Cellar/go/1.19/libexec/src/net/http/server.go:2947 +0x2c4
                net/http.(*conn).serve(0x1400021c000, {0x10092b880, 0x1400020a0c0})
                	/opt/homebrew/Cellar/go/1.19/libexec/src/net/http/server.go:1991 +0x560
                created by net/http.(*Server).Serve
                	/opt/homebrew/Cellar/go/1.19/libexec/src/net/http/server.go:3102 +0x444
                2022/08/31 08:24:20 SIGNAL TRAPPED
                [PUSHUP] shutting down gracefully, press Ctrl+C to force immediate
                [PUSHUP] shutdown complete

            main_test.go:257: error: *os.SyscallError wait: no child processes
    --- FAIL: TestPushup/request_basic.pushup (0.55s)
        --- FAIL: TestPushup/request_basic.pushup/#00 (0.54s)
            main_test.go:256: stderr:
                [PUSHUP] GET /testdata/request_basic 200 18.5µs
                2022/08/31 08:24:22 SIGNAL TRAPPED
                [PUSHUP] shutting down gracefully, press Ctrl+C to force immediate
                [PUSHUP] shutdown complete

            main_test.go:257: error: *os.SyscallError wait: no child processes
    --- FAIL: TestPushup/textelement.pushup (0.56s)
        --- FAIL: TestPushup/textelement.pushup/#00 (0.56s)
            main_test.go:256: stderr:
                [PUSHUP] GET /testdata/textelement 200 15.083µs
                2022/08/31 08:24:23 SIGNAL TRAPPED
                [PUSHUP] shutting down gracefully, press Ctrl+C to force immediate
                [PUSHUP] shutdown complete

            main_test.go:257: error: *os.SyscallError wait: no child processes
FAIL
FAIL	github.com/AdHocRandD/pushup	8.422s
?   	github.com/AdHocRandD/pushup/build	[no test files]
?   	github.com/AdHocRandD/pushup/build/cmd/myproject	[no test files]
?   	github.com/AdHocRandD/pushup/scaffold/pkg	[no test files]
FAIL

Inline partials inside a ^for

This currently doesn't work as you might expect, and is ill-defined.

The codegen generates one partial type implementing Responder, but its output is wrapped in the for.

It's not really possible to request a single partial inside the for.

It's a weird mix of runtime state (the values of induction variable(s) of the for loop) and comp-time state (the definition of the partial). Maybe it's a parameterized type over the induction variable(s), which gets passed in somehow ...

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.