17 Commits

Author SHA1 Message Date
4eaead6abc hint for running project 2025-12-31 12:16:59 -05:00
4abf2969ca new hint ui function 2025-12-31 12:16:51 -05:00
1c229f1b3e update examples 2025-12-28 20:17:46 -05:00
5a733b8642 create default site 2025-12-28 17:07:08 -05:00
11ab1630be maint: annotate source code 2025-12-28 16:39:33 -05:00
5fabd0233d fix: print usage to err instead of out 2025-12-28 16:38:40 -05:00
c43e905729 Merge branch 'new-default-project-creation-structure' 2025-12-28 15:52:26 -05:00
3430141184 new creation format 2025-12-28 15:51:58 -05:00
c5fe2eb7e7 maint: tidy 2025-12-28 14:08:55 -05:00
afd0d9eef4 doc: fix update date 2025-12-27 19:17:19 -05:00
b593aa26f0 doc: update documentation 2025-12-27 19:16:59 -05:00
fb8dc3cb90 fix: use 0.0.0.0 instead of 127.0.0.0 2025-12-27 16:29:55 -05:00
c681e342a0 fix: update docker interface 2025-12-27 12:04:04 -05:00
99e437b42b Docker image first version 2025-12-26 22:03:49 -05:00
56f22bb472 limit philosphy to 80 width 2025-12-26 15:00:27 -05:00
e53cc17025 default stylua formatting 2025-12-26 13:24:30 -05:00
2798cd6553 Update README.md 2025-12-26 10:16:42 -05:00
33 changed files with 1061 additions and 701 deletions

27
Dockerfile Normal file
View File

@@ -0,0 +1,27 @@
FROM golang:1.25-alpine AS builder
WORKDIR /src
RUN apk add --no-cache git build-base
COPY go.mod go.sum ./
RUN go mod download
COPY . .
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
RUN go build -ldflags="-X fes/modules/version.gitCommit=$(git rev-parse --short HEAD) -s -w" -o fes
FROM scratch
COPY --from=builder /src/fes /fes
WORKDIR /app
EXPOSE 8080
ENTRYPOINT ["/fes"]
CMD ["run", "/app"]

View File

@@ -34,7 +34,7 @@ go install fes
## Documentation ## Documentation
Run `fes run doc` for the documentation website or goto [docs.vxserver.dev](https://docs.vxserver.dev) Run `fes doc` for the documentation website or goto [docs.vxserver.dev](https://docs.vxserver.dev)
## License ## License

View File

@@ -1,7 +0,0 @@
local foo = {}
foo.render = function()
return "This was called from a foo function"
end
return foo

View File

@@ -0,0 +1,4 @@
# archive
This example demonstrates the archive feature of Fes it is useful for file
sharing purposes.

View File

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@@ -1,17 +1,10 @@
local fes = require("fes") local fes = require("fes")
local std = fes.std
local site = fes.fes() local site = fes.fes()
site.copyright = fes.util.copyright("https://fsd.vxserver.dev", "fSD") site.copyright = fes.util.copyright("https://fsd.vxserver.dev", "fSD")
site:h1("Hello, World!") site:h1("Hello, World!")
site:note(fes.util.cc { site:a("/archive", fes.std.h2("To the file room!"))
std.h2("Files"),
std.ul {
std.a("/archive", "to the file room!"),
}
})
return site return site

View File

@@ -1,5 +1,5 @@
[app] [app]
name = "advanced" name = "best"
version = "0.0.1" version = "0.0.1"
authors = ["vx-clutch"] authors = ["vx-clutch"]

23
examples/best/README.md Normal file
View File

@@ -0,0 +1,23 @@
# best
This is an example of best practices for the Fes framework.
## Parts
With best practice we can break our sites into a few parts.
## Index
The main page of the site loads in the header and the footer, as well as shows
some core information
## Include
Within include the header and footer are defined.
* **Header:** Site navigation and name display
* **Footer:** Extra and external information.
## Static
This is where we store our favicon.

View File

@@ -0,0 +1,13 @@
local footer = {}
footer.render = function(std)
return table.concat({
std.h2("Other resources"),
std.tl({
std.external("https://git.vxserver.dev/fSD/fes", "Fes source"),
std.external("https://docs.vxserver.dev/static/fes.html", "Documentation"),
}),
})
end
return footer

View File

@@ -0,0 +1,7 @@
local header = {}
header.render = function(std)
return std.center(std.ha("/", std.h1("Best Practices")))
end
return header

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,20 @@
local fes = require("fes")
local std = fes.std
local u = fes.util
local site = fes.fes()
site.copyright = fes.util.copyright("https://fsd.vxserver.dev", "fSD")
site.title = "Best practices"
site.favicon = "/static/favicon.ico"
site:banner(fes.app.header.render(std))
site:note(u.cc {
std.h2("Hello, World!"),
std.p("This is an example of the best practices/canonical Fes site.")
})
site:note(fes.app.footer.render(std))
return site

View File

@@ -0,0 +1,5 @@
[app]
name = "default"
version = "0.0.1"
authors = ["vx-clutch"]

View File

@@ -0,0 +1,33 @@
# default
```
fes new default
```
> **Know what you are doing?** Delete this file. Have fun!
## Project Structure
Inside your Fes project, you'll see the following directories and files:
```
.
├── Fes.toml
├── README.md
└── www
└── index.lua
```
Fes looks for `.lua` files in the `www/` directory. Each file is exposed as a route based on its file name.
## Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `fes run .` | Runs the project at `.` |
## What to learn more?
Check out [Fes's docs](https://docs.vxserver.dev/static/fes.html).

View File

@@ -1,15 +1,8 @@
local fes = require("fes") local fes = require("fes")
local std = fes.std
local site = fes.fes() local site = fes.fes()
site.copyright = fes.util.copyright("https://fsd.vxserver.dev", "fSD") site.copyright = fes.util.copyright("https://fsd.vxserver.dev", "fSD")
site:h1("Hello, World!") site:h1("Hello, World!")
site:note(
fes.app.foo.render()
)
return site return site

6
examples/error/README.md Normal file
View File

@@ -0,0 +1,6 @@
# error
This shows what a Lua error looks like to the user. Lua errors are the most
common and the most critical so that is why they are shown to the user. Other,
lesser errors, are only shown to the developer because of their different
nature.

View File

@@ -1,6 +1,10 @@
local fes = require("fes") local fes = require("fes")
local site = fes.fes() local site = fes.fes()
site.copyright = fes.util.copyright("https://fsd.vxserver.dev", "fSD")
This is what an error looks like This is what an error looks like
site:h1("Hello, World!")
return site return site

4
examples/hello/README.md Normal file
View File

@@ -0,0 +1,4 @@
# hello
This is a very simple hello world program, the only difference between this and
default is this README.

View File

@@ -0,0 +1,3 @@
# markdown
This example demonstrate Fes's ability to handle markdown routes.

View File

@@ -1 +1,3 @@
# Hello, World! # Markdown!
**Fes** also supports markdown routes!

View File

@@ -0,0 +1,5 @@
# simple
This simple example shows the extensibility of the Fes framework. It shows the
you do not necessarily need to use the site object (although it is recommended)
you can define your own site, similar to how Lisps do things.

14
go.mod
View File

@@ -3,13 +3,15 @@ module fes
go 1.25.4 go 1.25.4
require ( require (
github.com/fatih/color v1.18.0 // indirect github.com/fatih/color v1.18.0
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a // indirect github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a
github.com/gomarkdown/mdtohtml v0.0.0-20240124153210-d773061d1585 // indirect github.com/pelletier/go-toml/v2 v2.2.4
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/yuin/gopher-lua v1.1.1
)
require (
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect
golang.org/x/sys v0.25.0 // indirect golang.org/x/sys v0.25.0 // indirect
) )

2
go.sum
View File

@@ -1,9 +1,7 @@
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A=
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/gomarkdown/mdtohtml v0.0.0-20240124153210-d773061d1585/go.mod h1:6grYm5/uY15CwgBBqwA3+o/cAzaxssckznJ0B35ouBY=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=

View File

@@ -183,6 +183,7 @@ footer {
<li><a href="#introduction">Introduction</a></li> <li><a href="#introduction">Introduction</a></li>
<li><a href="#installation">Installation</a></li> <li><a href="#installation">Installation</a></li>
<li><a href="#usage">Usage</a></li> <li><a href="#usage">Usage</a></li>
<li><a href="#quick">Quick Start</a></li>
<li><a href="#cli-reference">Cli Reference</a></li> <li><a href="#cli-reference">Cli Reference</a></li>
<li><a href="#reference">Reference</a></li> <li><a href="#reference">Reference</a></li>
</ul> </ul>
@@ -210,6 +211,34 @@ footer {
</ul> </ul>
</section> </section>
<section id="quick">
<h2>Quick Start</h2>
<pre><code>fes new hello</code></pre>
<p>This creates a new project under the name <code>hello</code>.</p>
<pre><code>fes run hello</code></pre>
<p>This runs your project <code>hello</code>, by default at <a href="localhost:3000" target="_blank">localhost:3000</a>.</p>
<h3>Extensions</h3>
<p>Let's add a paragraph to this simple site. Right now you have the following page:</p>
<pre><code>local fes = require("fes")
local site = fes.fes()
-- site.copyright = fes.util.copyright("https://fsd.vxserver.dev", "vx-clutch")
site:h1("Hello, World!")
return site</code></pre>
<p>To add a simple paragraph modify like so:</p>
<pre><code>local fes = require("fes")
local site = fes.fes()
-- site.copyright = fes.util.copyright("https://fsd.vxserver.dev", "vx-clutch")
site:h1("Hello, World!")
site:p("This is a paragraph")
return site</code></pre>
</section>
<section id="cli-reference"> <section id="cli-reference">
<h2>Cli Reference</h2> <h2>Cli Reference</h2>
<table> <thead> <table> <thead>
@@ -219,18 +248,6 @@ footer {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr>
<td><code>--help</code></td>
<td>Display help information</td>
</tr>
<tr>
<td><code>--no-color</code></td>
<td>Disable color output</td>
</tr>
<tr>
<td><code>-p &lt;port&gt;</code></td>
<td>Set the server port</td>
</tr>
<tr> <tr>
<td><code>new &lt;project&gt;</code></td> <td><code>new &lt;project&gt;</code></td>
<td>Create a new projet called &lt;project&gt;</td> <td>Create a new projet called &lt;project&gt;</td>
@@ -243,6 +260,30 @@ footer {
<td><code>run &lt;project&gt;</code></td> <td><code>run &lt;project&gt;</code></td>
<td>Run the projet called &lt;project&gt;</td> <td>Run the projet called &lt;project&gt;</td>
</tr> </tr>
<tr>
<td><code>-help</code></td>
<td>Display help information.</td>
</tr>
<tr>
<td><code>-V1</code></td>
<td>Print extended version information, this is very helpful when it comes to bug reporting.</td>
</tr>
<tr>
<td><code>-no-color</code></td>
<td>Disable color output.</td>
</tr>
<tr>
<td><code>-p &lt;port&gt;</code></td>
<td>Set the server port.</td>
</tr>
<tr>
<td><code>-static</code></td>
<td>Render and save all pages. (this feature is yet to be implemented)</td>
</tr>
<tr>
<td><code>-version</code></td>
<td>Print the version.</td>
</tr>
</tbody> </tbody>
</table> </table>
</section> </section>
@@ -586,7 +627,7 @@ return hello</pre></code> This can be called from another with,
</section> </section>
<footer> <footer>
<p>Last updated: 2025-12-16</p> <p>Last updated: 2025-12-27</p>
</footer> </footer>
</main> </main>
</body> </body>

File diff suppressed because it is too large Load Diff

View File

@@ -4,25 +4,26 @@ local M = {}
M.__index = M M.__index = M
function M.fes(header, footer) function M.fes(header, footer)
local config = {} local site_config = {} local config = {}
local fes_mod = package.loaded.fes local site_config = {}
if fes_mod and fes_mod.config then local fes_mod = package.loaded.fes
config = fes_mod.config if fes_mod and fes_mod.config then
if config.site then config = fes_mod.config
site_config = config.site if config.site then
end site_config = config.site
end end
end
if site_config.favicon then if site_config.favicon then
site_config.favicon = '<link rel="icon" type="image/x-icon" href="' .. site_config.favicon .. '">' site_config.favicon = '<link rel="icon" type="image/x-icon" href="' .. site_config.favicon .. '">'
end end
local self = { local self = {
version = site_config.version, version = site_config.version,
title = site_config.title, title = site_config.title,
copyright = site_config.copyright, copyright = site_config.copyright,
favicon = site_config.favicon, favicon = site_config.favicon,
header = header or [[ header = header or [[
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@@ -300,7 +301,7 @@ em, i { font-style: italic; }
<body> <body>
<div class="container"> <div class="container">
]], ]],
footer = footer or [[ footer = footer or [[
<footer class="footer"> <footer class="footer">
<a href="https://git.vxserver.dev/fSD/fes" target="_blank">Fes Powered</a> <a href="https://git.vxserver.dev/fSD/fes" target="_blank">Fes Powered</a>
<a href="https://www.lua.org/" target="_blank">Lua Powered</a> <a href="https://www.lua.org/" target="_blank">Lua Powered</a>
@@ -311,38 +312,42 @@ em, i { font-style: italic; }
</body> </body>
</html> </html>
]], ]],
parts = {} parts = {},
} }
return setmetatable(self, M) return setmetatable(self, M)
end end
function M:custom(str) function M:custom(str)
table.insert(self.parts, str) table.insert(self.parts, str)
return self return self
end end
for name, func in pairs(std) do for name, func in pairs(std) do
if type(func) == "function" then if type(func) == "function" then
M[name] = function(self, ...) M[name] = function(self, ...)
local result = func(...) local result = func(...)
table.insert(self.parts, result) table.insert(self.parts, result)
return self return self
end end
end end
end end
function M:build() function M:build()
local header = self.header local header = self.header
header = header:gsub("{{TITLE}}", self.title or "Document") header = header:gsub("{{TITLE}}", self.title or "Document")
local favicon_html = self.favicon and ('<link rel="icon" type="image/x-icon" href="' .. self.favicon .. '">') local favicon_html = self.favicon and ('<link rel="icon" type="image/x-icon" href="' .. self.favicon .. '">')
header = header:gsub("{{FAVICON}}", favicon_html or [[<link rel="icon" href="data:image/svg+xml,<svg xmlns=%%22http://www.w3.org/2000/svg%%22 viewBox=%%220 0 100 100%%22><text y=%%22.9em%%22 font-size=%%2290%%22>🔥</text></svg>">]]) header = header:gsub(
local footer = self.footer:gsub("{{COPYRIGHT}}", self.copyright or "&#169; The Copyright Holder") "{{FAVICON}}",
return header .. table.concat(self.parts, "\n") .. footer favicon_html
or [[<link rel="icon" href="data:image/svg+xml,<svg xmlns=%%22http://www.w3.org/2000/svg%%22 viewBox=%%220 0 100 100%%22><text y=%%22.9em%%22 font-size=%%2290%%22>🔥</text></svg>">]]
)
local footer = self.footer:gsub("{{COPYRIGHT}}", self.copyright or "&#169; The Copyright Holder")
return header .. table.concat(self.parts, "\n") .. footer
end end
M.__tostring = function(self) M.__tostring = function(self)
return self:build() return self:build()
end end
return M return M

View File

@@ -19,29 +19,29 @@ end
function M.a(link, str) function M.a(link, str)
link = link or "https://example.com" link = link or "https://example.com"
str = str or link str = str or link
return "<a href=\"" .. link .. "\">" .. str .. "</a>" return '<a href="' .. link .. '">' .. str .. "</a>"
end end
function M.ha(link, str) function M.ha(link, str)
link = link or "https://example.com" link = link or "https://example.com"
str = str or link str = str or link
return "<a class=\"hidden\" href=\"" .. link .. "\">" .. str .. "</a>" return '<a class="hidden" href="' .. link .. '">' .. str .. "</a>"
end end
function M.external(link, str) function M.external(link, str)
return "<a target=\"_blank\" href=\"" .. link .. "\">" .. str .. "</a>" return '<a target="_blank" href="' .. link .. '">' .. str .. "</a>"
end end
function M.note(str) function M.note(str)
return '<div class="note">' .. str .. '</div>' return '<div class="note">' .. str .. "</div>"
end end
function M.muted(str) function M.muted(str)
return '<div class="muted">' .. str .. '</div>' return '<div class="muted">' .. str .. "</div>"
end end
function M.callout(str) function M.callout(str)
return '<div class="callout">' .. str .. '</div>' return '<div class="callout">' .. str .. "</div>"
end end
function M.h1(str) function M.h1(str)
@@ -55,7 +55,8 @@ end
function M.h3(str) function M.h3(str)
return "<h3>" .. (str or "") .. "</h3>" return "<h3>" .. (str or "") .. "</h3>"
end end
function M.h4(str) return "<h4>" .. (str or "") .. "</h4>" function M.h4(str)
return "<h4>" .. (str or "") .. "</h4>"
end end
function M.h5(str) function M.h5(str)

15
main.go
View File

@@ -25,7 +25,8 @@ var documentation string
func init() { func init() {
config.Port = flag.Int("p", 3000, "Set the server port") config.Port = flag.Int("p", 3000, "Set the server port")
config.Color = flag.Bool("no-color", false, "Disable color output") config.Color = flag.Bool("no-color", false, "Disable color output")
config.Static = flag.Bool("static", false, "Render and save all pages.") config.Static = flag.Bool("static", false, "Render and save all pages")
config.Docker = flag.Bool("docker", false, "Create a docker project")
config.Lib = lib config.Lib = lib
config.Doc = documentation config.Doc = documentation
} }
@@ -33,13 +34,13 @@ func init() {
func main() { func main() {
flag.Usage = func() { flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] <command> <project_dir>\n", os.Args[0]) fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] <command> <project_dir>\n", os.Args[0])
fmt.Println("Commands:") fmt.Fprintf(os.Stderr, "Commands:")
fmt.Println(" new <project_dir> Create a new project") fmt.Fprintf(os.Stderr, " new <project_dir> Create a new project")
fmt.Println(" doc Open documentation") fmt.Fprintf(os.Stderr, " doc Open documentation")
fmt.Println(" run <project_dir> Start the server") fmt.Fprintf(os.Stderr, " run <project_dir> Start the server")
fmt.Println("Options:") fmt.Fprintf(os.Stderr, "Options:")
flag.PrintDefaults() flag.PrintDefaults()
fmt.Println("For bug reports, contact a developer and describe the issue. Provide the output of the `-V1` flag.") fmt.Fprintf(os.Stderr, "For bug reports, contact a developer and describe the issue. Provide the output of the `-V1` flag.")
} }
showVersion := flag.Bool("version", false, "Show version and exit") showVersion := flag.Bool("version", false, "Show version and exit")

View File

@@ -10,6 +10,7 @@ var Doc string
var Port *int var Port *int
var Color *bool var Color *bool
var Static *bool var Static *bool
var Docker *bool
type AppConfig struct { type AppConfig struct {
App struct { App struct {

View File

@@ -9,6 +9,7 @@ import (
"github.com/pkg/browser" "github.com/pkg/browser"
) )
/* open documentation in browser */
func Open() error { func Open() error {
fmt.Println("Opening documentation in browser") fmt.Println("Opening documentation in browser")

View File

@@ -1,6 +1,8 @@
package new package new
import ( import (
"fes/modules/config"
"fes/modules/ui"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@@ -26,7 +28,7 @@ func getName() string {
} }
/* helper function for writing files */ /* helper function for writing files */
func write(path string, format string, args ...interface{}) error { func write(path string, format string, args ...any) error {
dir := filepath.Dir(path) dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil { if err := os.MkdirAll(dir, 0755); err != nil {
panic(err) panic(err)
@@ -49,6 +51,22 @@ func Project(dir string) error {
return err return err
} }
if *config.Docker {
write("docker-compose.yml", `services:
%s:
image: git.vxserver.dev/fsd/fes:latest
ports:
- "3000:3000"
volumes:
- ./app:/app`, dir)
if err := os.Mkdir("app", 0755); err != nil {
return err
}
if err := os.Chdir("app"); err != nil {
return err
}
}
name := getName() name := getName()
write("www/index.lua", `local fes = require("fes") write("www/index.lua", `local fes = require("fes")
@@ -64,5 +82,48 @@ return site`, name)
name = "%s" name = "%s"
version = "0.0.1" version = "0.0.1"
authors = ["%s"]`, dir, name) authors = ["%s"]`, dir, name)
write("README.md", strings.ReplaceAll(`# %s
$$$$$$
fes new %s
$$$$$$
> **Know what you are doing?** Delete this file. Have fun!
## Project Structure
Inside your Fes project, you'll see the following directories and files:
$$$$$$
.
├── Fes.toml
├── README.md
└── www
└── index.lua
$$$$$$
Fes looks for $$.lua$$ files in the $$www/$$ directory. Each file is exposed as a route based on its file name.
## Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| $$fes run .$$ | Runs the project at $$.$$ |
## What to learn more?
Check out [Fes's docs](https://docs.vxserver.dev/static/fes.html).`, "$$", "`"), dir, dir)
ui.Hint("you can run this with `fes run %s`", dir)
fmt.Println("Created new Fes project at", func() string {
if res, err := filepath.Abs(dir); err == nil {
return res
}
return dir
}())
return nil return nil
} }

View File

@@ -17,11 +17,21 @@ import (
"time" "time"
) )
/* this is the request data we pass over the bus to the application, via the fes.bus interface */
type reqData struct { type reqData struct {
path string path string
params map[string]string params map[string]string
} }
/* performs relavent handling based on the directory passaed
*
* Special directories
* - www/ <= contains lua routes.
* - static/ <= static content accessable at /static/path or /static/dir/path.
* - include/ <= globally accessable lua functions, cannot directly access "fes" right now.
* - archive/ <= contains user facing files such as archives or dists.
*
*/
func handleDir(entries []os.DirEntry, dir string, routes map[string]string, base string, isStatic bool) error { func handleDir(entries []os.DirEntry, dir string, routes map[string]string, base string, isStatic bool) error {
for _, entry := range entries { for _, entry := range entries {
path := filepath.Join(dir, entry.Name()) path := filepath.Join(dir, entry.Name())
@@ -59,6 +69,7 @@ func handleDir(entries []os.DirEntry, dir string, routes map[string]string, base
return nil return nil
} }
// TODO(vx-clutch): this should not be a function
func loadIncludeModules(L *lua.LState, includeDir string) *lua.LTable { func loadIncludeModules(L *lua.LState, includeDir string) *lua.LTable {
app := L.NewTable() app := L.NewTable()
ents, err := os.ReadDir(includeDir) ents, err := os.ReadDir(includeDir)
@@ -94,7 +105,8 @@ func loadIncludeModules(L *lua.LState, includeDir string) *lua.LTable {
return app return app
} }
func loadLua(entry string, cfg *config.AppConfig, requestData reqData) ([]byte, error) { /* renders the given lua route */
func renderRoute(entry string, cfg *config.AppConfig, requestData reqData) ([]byte, error) {
L := lua.NewState() L := lua.NewState()
defer L.Close() defer L.Close()
@@ -219,6 +231,7 @@ func loadLua(entry string, cfg *config.AppConfig, requestData reqData) ([]byte,
return []byte(""), nil return []byte(""), nil
} }
/* this indexes and generate the page for viewing the archive directory */
func generateArchiveIndex(fsPath string, urlPath string) (string, error) { func generateArchiveIndex(fsPath string, urlPath string) (string, error) {
info, err := os.Stat(fsPath) info, err := os.Stat(fsPath)
if err != nil { if err != nil {
@@ -309,6 +322,9 @@ func generateArchiveIndex(fsPath string, urlPath string) (string, error) {
return b.String(), nil return b.String(), nil
} }
/* generates the data for the not found page. Checks for user-defined source in this order
* 404.lua => 404.md => 404.html => default.
*/
func generateNotFoundData(cfg *config.AppConfig) []byte { func generateNotFoundData(cfg *config.AppConfig) []byte {
notFoundData := []byte(` notFoundData := []byte(`
<html> <html>
@@ -320,9 +336,13 @@ func generateNotFoundData(cfg *config.AppConfig) []byte {
</html> </html>
`) `)
if _, err := os.Stat(filepath.Join("www", "404.lua")); err == nil { if _, err := os.Stat(filepath.Join("www", "404.lua")); err == nil {
if nf, err := loadLua("www/404.lua", cfg, reqData{}); err == nil { if nf, err := renderRoute("www/404.lua", cfg, reqData{}); err == nil {
notFoundData = nf notFoundData = nf
} }
} else if _, err := os.Stat("www/404.md"); err == nil {
if buf, err := os.ReadFile("www/404.html"); err == nil {
notFoundData = []byte(markdownToHTML(string(buf)))
}
} else if _, err := os.Stat("www/404.html"); err == nil { } else if _, err := os.Stat("www/404.html"); err == nil {
if buf, err := os.ReadFile("www/404.html"); err == nil { if buf, err := os.ReadFile("www/404.html"); err == nil {
notFoundData = buf notFoundData = buf
@@ -331,6 +351,7 @@ func generateNotFoundData(cfg *config.AppConfig) []byte {
return notFoundData return notFoundData
} }
/* helper to load all special directories */
func loadDirs() map[string]string { func loadDirs() map[string]string {
routes := make(map[string]string) routes := make(map[string]string)
@@ -355,6 +376,7 @@ func loadDirs() map[string]string {
return routes return routes
} }
/* helper to parse the Fes.toml and generate config */
func parseConfig() config.AppConfig { func parseConfig() config.AppConfig {
tomlDocument, err := os.ReadFile("Fes.toml") tomlDocument, err := os.ReadFile("Fes.toml")
if err != nil { if err != nil {
@@ -372,15 +394,21 @@ func parseConfig() config.AppConfig {
return cfg return cfg
} }
func readArchive(w http.ResponseWriter, route string) { /* helper to read the archive files */
func readArchive(w http.ResponseWriter, route string) error {
fsPath := "." + route fsPath := "." + route
if info, err := os.Stat(fsPath); err == nil && info.IsDir() { if info, err := os.Stat(fsPath); err == nil && info.IsDir() {
if page, err := generateArchiveIndex(fsPath, route); err == nil { if page, err := generateArchiveIndex(fsPath, route); err == nil {
w.Write([]byte(page)) w.Write([]byte(page))
return nil
} else {
return err
} }
} }
return nil
} }
/* start the Fes server */
func Start(dir string) error { func Start(dir string) error {
if err := os.Chdir(dir); err != nil { if err := os.Chdir(dir); err != nil {
return ui.Error(fmt.Sprintf("failed to change directory to %s", dir), err) return ui.Error(fmt.Sprintf("failed to change directory to %s", dir), err)
@@ -405,7 +433,7 @@ func Start(dir string) error {
route = r.URL.Path route = r.URL.Path
if strings.HasPrefix(route, "/archive") { if strings.HasPrefix(route, "/archive") {
readArchive(w, route) err = readArchive(w, route)
} else { } else {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
w.Write([]byte(notFoundData)) w.Write([]byte(notFoundData))
@@ -422,10 +450,11 @@ func Start(dir string) error {
var data []byte var data []byte
if strings.HasSuffix(route, ".lua") { if strings.HasSuffix(route, ".lua") {
data, err = loadLua(route, &cfg, reqData{path: r.URL.Path, params: params}) data, err = renderRoute(route, &cfg, reqData{path: r.URL.Path, params: params})
} else if strings.HasSuffix(route, ".md") { } else if strings.HasSuffix(route, ".md") {
data, err = os.ReadFile(route) data, err = os.ReadFile(route)
data = []byte(markdownToHTML(string(data))) data = []byte(markdownToHTML(string(data)))
data = []byte("<style>body {max-width: 80ch;}</style>\n" + string(data))
} else { } else {
data, err = os.ReadFile(route) data, err = os.ReadFile(route)
} }
@@ -438,5 +467,5 @@ func Start(dir string) error {
}) })
fmt.Printf("Server is running on http://localhost:%d\n", *config.Port) fmt.Printf("Server is running on http://localhost:%d\n", *config.Port)
return http.ListenAndServe(fmt.Sprintf(":%d", *config.Port), nil) return http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", *config.Port), nil)
} }

View File

@@ -11,6 +11,11 @@ import (
"github.com/fatih/color" "github.com/fatih/color"
) )
const (
hint_color = 0xbda02a
)
/* print out the current path (route) and relevant error */
func Path(path string, err error) { func Path(path string, err error) {
path = strings.TrimPrefix(path, "/") path = strings.TrimPrefix(path, "/")
@@ -29,29 +34,45 @@ func Path(path string, err error) {
} }
} }
/* print general system warning */
func Warning(msg string, err error) error { func Warning(msg string, err error) error {
fmt.Printf("%s: %s: %v\n", version.PROGRAM_NAME, color.MagentaString("warning"), err) fmt.Printf("%s: %s: %v\n", version.PROGRAM_NAME, color.MagentaString("warning"), err)
return err return err
} }
/* print general system error */
func Error(msg string, err error) error { func Error(msg string, err error) error {
fmt.Printf("%s: %s: %v\n", version.PROGRAM_NAME, color.RedString("error"), err) fmt.Printf("%s: %s: %v\n", version.PROGRAM_NAME, color.RedString("error"), err)
return err return err
} }
/* print fatality and panic */
func Fatal(msg string, err error) error { func Fatal(msg string, err error) error {
fmt.Printf("%s: %s: %v\n", version.PROGRAM_NAME, color.RedString("fatal"), err) fmt.Printf("%s: %s: %v\n", version.PROGRAM_NAME, color.RedString("fatal"), err)
panic(err) panic(err)
} }
/* print a useful hint to the user */
func Hint(format string, args ...any) {
color.RGB(func(hex int) (r, g, b int) {
r = (hex >> 16) & 0xFF
g = (hex >> 8) & 0xFF
b = hex & 0xFF
return
}(hint_color)).Printf("hint: %s\n", fmt.Sprintf(format, args...))
}
/* print message using the ok status color */
func OK(msg string) { func OK(msg string) {
color.Green(msg) color.Green(msg)
} }
/* print message using the warning status color */
func WARN(msg string) { func WARN(msg string) {
color.Magenta(msg) color.Magenta(msg)
} }
/* print message using the error status color */
func ERROR(msg string) { func ERROR(msg string) {
color.Red(msg) color.Red(msg)
} }