fresco 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/exe/fresco +3 -0
- data/lib/fresco/application.rb +12 -0
- data/lib/fresco/cli/build.rb +682 -0
- data/lib/fresco/cli/dev.rb +17 -0
- data/lib/fresco/cli/dev_loop.rb +815 -0
- data/lib/fresco/cli/new.rb +120 -0
- data/lib/fresco/cli/release.rb +76 -0
- data/lib/fresco/cli.rb +56 -0
- data/lib/fresco/database_config.rb +34 -0
- data/lib/fresco/generators/app/Gemfile.tt +18 -0
- data/lib/fresco/generators/app/README.md.tt +32 -0
- data/lib/fresco/generators/app/app/action.rb.tt +20 -0
- data/lib/fresco/generators/app/app/actions/root_path.rb.tt +5 -0
- data/lib/fresco/generators/app/app/views/layouts/application.html.erb +29 -0
- data/lib/fresco/generators/app/app/views/root_path.html.erb +8 -0
- data/lib/fresco/generators/app/app.rb.tt +15 -0
- data/lib/fresco/generators/app/bin/build +2 -0
- data/lib/fresco/generators/app/bin/dev +2 -0
- data/lib/fresco/generators/app/bin/release +2 -0
- data/lib/fresco/generators/app/config/app.rb.tt +26 -0
- data/lib/fresco/generators/app/config/database.rb +17 -0
- data/lib/fresco/generators/app/config/routes.rb +11 -0
- data/lib/fresco/generators/app/db/schema.rb +14 -0
- data/lib/fresco/generators/app/public/404.html +87 -0
- data/lib/fresco/generators/app/public/500.html +84 -0
- data/lib/fresco/migration_builder.rb +55 -0
- data/lib/fresco/model_builder.rb +54 -0
- data/lib/fresco/paths.rb +20 -0
- data/lib/fresco/router.rb +67 -0
- data/lib/fresco/runtime/boot.rb +34 -0
- data/lib/fresco/runtime/db_postgres.rb +403 -0
- data/lib/fresco/runtime/db_sqlite.rb +495 -0
- data/lib/fresco/runtime/http.c +456 -0
- data/lib/fresco/runtime/postgres.c +339 -0
- data/lib/fresco/runtime/runtime.rb +1810 -0
- data/lib/fresco/runtime/sqlite.c +220 -0
- data/lib/fresco/runtime/welcome.rb +152 -0
- data/lib/fresco/schema_builder.rb +71 -0
- data/lib/fresco/templates/dispatch.rb.erb +32 -0
- data/lib/fresco/templates/layout_dispatch.rb.erb +16 -0
- data/lib/fresco/templates/manifest.rb.erb +5 -0
- data/lib/fresco/templates/migrations.rb.erb +152 -0
- data/lib/fresco/templates/model.rb.erb +223 -0
- data/lib/fresco/templates/view.rb.erb +5 -0
- data/lib/fresco/version.rb +3 -0
- data/lib/fresco.rb +61 -0
- metadata +115 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
require "erb"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
|
|
4
|
+
require "fresco/paths"
|
|
5
|
+
|
|
6
|
+
module Fresco
|
|
7
|
+
class CLI
|
|
8
|
+
class New
|
|
9
|
+
USAGE = <<~USAGE
|
|
10
|
+
Usage: fresco new <app_name> [--fresco-path <dir>]
|
|
11
|
+
|
|
12
|
+
--fresco-path <dir> Generate a Gemfile pinned to a path-installed
|
|
13
|
+
fresco (instead of the rubygems version). Useful
|
|
14
|
+
during co-development of fresco itself.
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
fresco new bookshelf
|
|
18
|
+
fresco new bookshelf --fresco-path ../fresco
|
|
19
|
+
USAGE
|
|
20
|
+
|
|
21
|
+
def run(argv)
|
|
22
|
+
app_name = nil
|
|
23
|
+
fresco_path = nil
|
|
24
|
+
i = 0
|
|
25
|
+
while i < argv.length
|
|
26
|
+
a = argv[i]
|
|
27
|
+
if a == "--fresco-path"
|
|
28
|
+
fresco_path = argv[i + 1]
|
|
29
|
+
unless fresco_path
|
|
30
|
+
warn "fresco: --fresco-path requires a directory argument"
|
|
31
|
+
return 1
|
|
32
|
+
end
|
|
33
|
+
i += 2
|
|
34
|
+
elsif a.start_with?("-")
|
|
35
|
+
warn "fresco: unknown flag #{a.inspect}"
|
|
36
|
+
warn USAGE
|
|
37
|
+
return 1
|
|
38
|
+
else
|
|
39
|
+
if app_name
|
|
40
|
+
warn "fresco: positional <app_name> already set to #{app_name.inspect}"
|
|
41
|
+
return 1
|
|
42
|
+
end
|
|
43
|
+
app_name = a
|
|
44
|
+
i += 1
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if app_name.nil? || app_name.empty?
|
|
49
|
+
warn USAGE
|
|
50
|
+
return 1
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
unless app_name.match?(/\A[a-z][a-z0-9_]*\z/)
|
|
54
|
+
warn "fresco: app name #{app_name.inspect} must be lowercase letters/digits/underscores starting with a letter"
|
|
55
|
+
return 1
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
target = File.expand_path(app_name)
|
|
59
|
+
if File.exist?(target)
|
|
60
|
+
warn "fresco: #{target} already exists"
|
|
61
|
+
return 1
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
locals = {
|
|
65
|
+
app_name: app_name,
|
|
66
|
+
module_name: camelize(app_name),
|
|
67
|
+
fresco_source: fresco_path ? ", path: #{File.expand_path(fresco_path).inspect}" : "",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
FileUtils.mkdir_p(target)
|
|
71
|
+
scaffold_root = Fresco::Paths.generator_root
|
|
72
|
+
|
|
73
|
+
copied = 0
|
|
74
|
+
Dir.glob("#{scaffold_root}/**/*", File::FNM_DOTMATCH).sort.each do |src|
|
|
75
|
+
next if src.end_with?("/.", "/..")
|
|
76
|
+
rel = src.sub(/\A#{Regexp.escape(scaffold_root)}\/?/, "")
|
|
77
|
+
next if rel.empty?
|
|
78
|
+
dst = File.join(target, strip_template_suffix(rel))
|
|
79
|
+
|
|
80
|
+
if File.directory?(src)
|
|
81
|
+
FileUtils.mkdir_p(dst)
|
|
82
|
+
next
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
FileUtils.mkdir_p(File.dirname(dst))
|
|
86
|
+
if src.end_with?(".tt")
|
|
87
|
+
File.write(dst, ERB.new(File.read(src), trim_mode: "-").result_with_hash(locals))
|
|
88
|
+
else
|
|
89
|
+
FileUtils.cp(src, dst)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Preserve executable bit for bin/* scripts (cp doesn't strip it,
|
|
93
|
+
# but ERB-rendered .tt files re-create with default 0644). The
|
|
94
|
+
# generator tree doesn't currently put .tt files under bin/, but
|
|
95
|
+
# chmod here makes the rule uniform.
|
|
96
|
+
File.chmod(0o755, dst) if rel.start_with?("bin/")
|
|
97
|
+
copied += 1
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
puts "Created #{app_name}/ (#{copied} files)"
|
|
101
|
+
puts ""
|
|
102
|
+
puts " cd #{app_name}"
|
|
103
|
+
puts " bundle install"
|
|
104
|
+
puts " bin/dev"
|
|
105
|
+
puts ""
|
|
106
|
+
0
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def camelize(name)
|
|
112
|
+
name.split("_").map(&:capitalize).join
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def strip_template_suffix(rel)
|
|
116
|
+
rel.end_with?(".tt") ? rel[0...-3] : rel
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
3
|
+
module Fresco
|
|
4
|
+
class CLI
|
|
5
|
+
class Release
|
|
6
|
+
# On macOS, libpq's headers ship in Homebrew's keg-only `libpq`
|
|
7
|
+
# formula (or Postgres.app), so they're not in cc's default
|
|
8
|
+
# include path. Auto-detect a few known locations and prepend
|
|
9
|
+
# them to CPATH / LIBRARY_PATH so Spinel's compile of the
|
|
10
|
+
# postgres.c shim finds libpq-fe.h / -lpq without the user
|
|
11
|
+
# having to remember the export dance. SQLite builds are
|
|
12
|
+
# unaffected — cc ignores include/lib paths that nothing
|
|
13
|
+
# references.
|
|
14
|
+
LIBPQ_ROOTS = [
|
|
15
|
+
"/opt/homebrew/opt/libpq",
|
|
16
|
+
"/usr/local/opt/libpq",
|
|
17
|
+
"/Applications/Postgres.app/Contents/Versions/latest",
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
def run(argv = [])
|
|
21
|
+
# Regenerate generated/* first so the binary picks up any
|
|
22
|
+
# routes/views/actions that changed since the last dev cycle.
|
|
23
|
+
require "fresco/cli/build"
|
|
24
|
+
Fresco::CLI::Build.new.run
|
|
25
|
+
|
|
26
|
+
export_libpq_env!
|
|
27
|
+
|
|
28
|
+
spinel = find_spinel_binary
|
|
29
|
+
unless spinel
|
|
30
|
+
abort <<~MSG
|
|
31
|
+
[release] no spinel binary found. Tried: ./spinel, vendor/spinel/bin/spinel, $PATH.
|
|
32
|
+
Build Spinel from https://github.com/.../spinel and place the binary at
|
|
33
|
+
./spinel, vendor/spinel/bin/spinel, or on your PATH.
|
|
34
|
+
MSG
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
FileUtils.mkdir_p("build")
|
|
38
|
+
unless system(spinel, "app.rb", "-o", "build/app")
|
|
39
|
+
abort "[release] spinel failed; see output above"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
size = File.size("build/app")
|
|
43
|
+
puts "built build/app (#{size} bytes)"
|
|
44
|
+
0
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def export_libpq_env!
|
|
50
|
+
LIBPQ_ROOTS.each do |root|
|
|
51
|
+
header = File.join(root, "include/libpq-fe.h")
|
|
52
|
+
next unless File.exist?(header)
|
|
53
|
+
prepend_env("CPATH", File.join(root, "include"))
|
|
54
|
+
prepend_env("LIBRARY_PATH", File.join(root, "lib"))
|
|
55
|
+
puts "[release] using libpq at #{root}"
|
|
56
|
+
break
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def prepend_env(var, dir)
|
|
61
|
+
existing = ENV[var]
|
|
62
|
+
ENV[var] = existing && !existing.empty? ? "#{dir}:#{existing}" : dir
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Spinel binary lookup ladder. Order matches the messages the
|
|
66
|
+
# release script prints on miss; keep them in sync if you change
|
|
67
|
+
# one.
|
|
68
|
+
def find_spinel_binary
|
|
69
|
+
return File.expand_path("./spinel") if File.executable?("./spinel")
|
|
70
|
+
return File.expand_path("vendor/spinel/bin/spinel") if File.executable?("vendor/spinel/bin/spinel")
|
|
71
|
+
path = `command -v spinel 2>/dev/null`.strip
|
|
72
|
+
path.empty? ? nil : path
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/fresco/cli.rb
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
require "fresco/version"
|
|
2
|
+
|
|
3
|
+
# Subcommand dispatcher. Each command lives in lib/fresco/cli/<name>.rb
|
|
4
|
+
# as a class with a #run(argv) method; we load lazily so `fresco --help`
|
|
5
|
+
# doesn't pay for parsing herb / loading the dev loop.
|
|
6
|
+
module Fresco
|
|
7
|
+
class CLI
|
|
8
|
+
COMMANDS = {
|
|
9
|
+
"new" => "Fresco::CLI::New",
|
|
10
|
+
"build" => "Fresco::CLI::Build",
|
|
11
|
+
"dev" => "Fresco::CLI::Dev",
|
|
12
|
+
"release" => "Fresco::CLI::Release",
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
def self.start(argv)
|
|
16
|
+
cmd = argv.first
|
|
17
|
+
|
|
18
|
+
if cmd.nil? || cmd == "--help" || cmd == "-h"
|
|
19
|
+
print_usage
|
|
20
|
+
return 0
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
if cmd == "--version" || cmd == "-v"
|
|
24
|
+
puts "fresco #{Fresco::VERSION}"
|
|
25
|
+
return 0
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
const_name = COMMANDS[cmd]
|
|
29
|
+
unless const_name
|
|
30
|
+
warn "fresco: unknown command #{cmd.inspect}"
|
|
31
|
+
print_usage
|
|
32
|
+
return 1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
require "fresco/cli/#{cmd}"
|
|
36
|
+
klass = const_name.split("::").inject(Object) { |m, n| m.const_get(n) }
|
|
37
|
+
klass.new.run(argv[1..])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.print_usage
|
|
41
|
+
puts <<~USAGE
|
|
42
|
+
Usage: fresco <command> [args]
|
|
43
|
+
|
|
44
|
+
Commands:
|
|
45
|
+
new <name> Scaffold a new Fresco app in ./<name>/
|
|
46
|
+
build Regenerate generated/* from config + app/
|
|
47
|
+
dev Run the CRuby dev loop (build + watch + serve)
|
|
48
|
+
release Build a Spinel-compiled binary at ./build/app
|
|
49
|
+
|
|
50
|
+
Flags:
|
|
51
|
+
-h, --help Show this message
|
|
52
|
+
-v, --version Show fresco version
|
|
53
|
+
USAGE
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Build-time + runtime-safe database config slots. Populated by
|
|
2
|
+
# config/database.rb's `Fresco.database :sqlite, ...` (or :postgres)
|
|
3
|
+
# call. `fresco build` reads `database_adapter` to decide which adapter
|
|
4
|
+
# template to copy into `generated/db_adapter.rb`; at runtime the
|
|
5
|
+
# same file is re-loaded as part of the boot chain so ENV.fetch'd
|
|
6
|
+
# values (paths, URLs) resolve in the running binary's environment
|
|
7
|
+
# rather than at build time.
|
|
8
|
+
#
|
|
9
|
+
# Three typed scalars instead of one hash — see the note in
|
|
10
|
+
# lib/fresco/runtime/runtime.rb for the Spinel poly-hash + nil-init traps.
|
|
11
|
+
# Mirrored verbatim there so build-time and runtime stay in sync.
|
|
12
|
+
module Fresco
|
|
13
|
+
@database_adapter = :none
|
|
14
|
+
@database_path = ""
|
|
15
|
+
@database_url = ""
|
|
16
|
+
|
|
17
|
+
def self.database_adapter
|
|
18
|
+
@database_adapter
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.database_path
|
|
22
|
+
@database_path
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.database_url
|
|
26
|
+
@database_url
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.database(adapter, path: "", url: "")
|
|
30
|
+
@database_adapter = adapter
|
|
31
|
+
@database_path = path
|
|
32
|
+
@database_url = url
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
source "https://gem.coop"
|
|
2
|
+
|
|
3
|
+
gem "fresco"<%= fresco_source %>
|
|
4
|
+
|
|
5
|
+
group :development do
|
|
6
|
+
# Lints user code against the Spinel-acceptable Ruby subset.
|
|
7
|
+
# `fresco build` runs this and fails the build on violations.
|
|
8
|
+
gem "rubocop_spinel"
|
|
9
|
+
|
|
10
|
+
# Powers `fresco dev`'s CRuby stand-in for the SQLite FFI shim.
|
|
11
|
+
# Production binaries link libsqlite3 directly via the gem-shipped
|
|
12
|
+
# sqlite.c — this is a dev-only convenience so actions that hit
|
|
13
|
+
# Fresco::Db::Active work under CRuby without a Spinel recompile.
|
|
14
|
+
gem "sqlite3"
|
|
15
|
+
|
|
16
|
+
# Uncomment if you switch the adapter in config/database.rb.
|
|
17
|
+
# gem "pg"
|
|
18
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# <%= app_name %>
|
|
2
|
+
|
|
3
|
+
A Fresco app — Ruby that compiles to a static binary via Spinel.
|
|
4
|
+
|
|
5
|
+
## Getting started
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
bundle install
|
|
9
|
+
bin/dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Then open <http://localhost:3030/>.
|
|
13
|
+
|
|
14
|
+
## Layout
|
|
15
|
+
|
|
16
|
+
- `app/actions/` — request handlers. Add a file, point a route at it.
|
|
17
|
+
- `app/views/` — ERB templates compiled at build time.
|
|
18
|
+
- `app/models/` — model declarations (drive generated DB helpers).
|
|
19
|
+
- `config/routes.rb` — route table.
|
|
20
|
+
- `config/database.rb` — adapter + DSN.
|
|
21
|
+
- `db/schema.rb` — table definitions.
|
|
22
|
+
- `db/migrations/` — versioned SQL migrations.
|
|
23
|
+
|
|
24
|
+
## Commands
|
|
25
|
+
|
|
26
|
+
- `bin/dev` — CRuby dev loop with auto-reload. Edit + refresh.
|
|
27
|
+
- `bin/build` — regenerate `generated/` without booting the server.
|
|
28
|
+
- `bin/release` — compile a production binary to `build/app`.
|
|
29
|
+
|
|
30
|
+
You'll need the `spinel` binary on PATH (or symlinked into this
|
|
31
|
+
directory) for `bin/release`. See the Fresco docs for how to obtain
|
|
32
|
+
it.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Shared base for all actions in this application. Concrete actions
|
|
2
|
+
# under app/actions/ inherit from this rather than Fresco::Action
|
|
3
|
+
# directly, so cross-cutting changes (layouts, filters, helper
|
|
4
|
+
# methods) stay in one place.
|
|
5
|
+
#
|
|
6
|
+
# Filters available from the framework base:
|
|
7
|
+
# - #before_action(req) — override to run pre-call code; call
|
|
8
|
+
# halt!(resp) to short-circuit #call.
|
|
9
|
+
# - #after_action(req, res) — override to run post-call code; call
|
|
10
|
+
# halt!(resp) to replace the response.
|
|
11
|
+
# - #handle(req) + super — wrap the whole pipeline ("around").
|
|
12
|
+
module <%= module_name %>
|
|
13
|
+
class Action < Fresco::Action
|
|
14
|
+
# Default layout for every action. Override `#layout` on a
|
|
15
|
+
# specific action class (or return :none) to opt out per-action.
|
|
16
|
+
def layout
|
|
17
|
+
:application
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Fresco App</title>
|
|
6
|
+
<style>
|
|
7
|
+
body {
|
|
8
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
9
|
+
max-width: 40rem;
|
|
10
|
+
margin: 2rem auto;
|
|
11
|
+
padding: 0 1.5rem;
|
|
12
|
+
color: #222;
|
|
13
|
+
}
|
|
14
|
+
header nav a { color: #0066cc; text-decoration: none; margin-right: 1rem; }
|
|
15
|
+
header nav a:hover { text-decoration: underline; }
|
|
16
|
+
code { background: #f3f3f3; padding: 0.1rem 0.3rem; border-radius: 3px; }
|
|
17
|
+
footer { color: #888; font-size: 0.85rem; border-top: 1px solid #eee; margin-top: 2rem; padding-top: 1rem; }
|
|
18
|
+
</style>
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
<header>
|
|
22
|
+
<nav><a href="/">home</a></nav>
|
|
23
|
+
</header>
|
|
24
|
+
<main>
|
|
25
|
+
<%= yield %>
|
|
26
|
+
</main>
|
|
27
|
+
<footer><small>Powered by Spinel + Fresco.</small></footer>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<h1>Welcome to Fresco</h1>
|
|
2
|
+
<p>You're up and running. Edit <code>app/views/root_path.html.erb</code> to change this page, or add new actions under <code>app/actions/</code>.</p>
|
|
3
|
+
<p>Next steps:</p>
|
|
4
|
+
<ul>
|
|
5
|
+
<li>Define routes in <code>config/routes.rb</code>.</li>
|
|
6
|
+
<li>Add a database in <code>config/database.rb</code>, then declare your schema in <code>db/schema.rb</code>.</li>
|
|
7
|
+
<li>Run <code>bin/release</code> to produce a Spinel-compiled binary at <code>build/app</code>.</li>
|
|
8
|
+
</ul>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Fresco: entry point. Spinel reads this file; everything reachable
|
|
2
|
+
# via require_relative gets AOT-compiled into the binary.
|
|
3
|
+
#
|
|
4
|
+
# The boot file (auto-generated by `fresco build`) wires the load
|
|
5
|
+
# order: runtime → views → config/app → action manifest → dispatcher.
|
|
6
|
+
# Everything that changes per-app lives in config/, app/, or
|
|
7
|
+
# app/views/ — this file stays a one-liner plus the boot call.
|
|
8
|
+
#
|
|
9
|
+
# Spinel quirk: ARGV is read directly inside Fresco::App rather
|
|
10
|
+
# than passed in — the sp_Argv type doesn't survive being routed
|
|
11
|
+
# through a Ruby parameter.
|
|
12
|
+
|
|
13
|
+
require_relative "generated/boot"
|
|
14
|
+
|
|
15
|
+
<%= module_name %>::Base.run
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Boot-time customization point. `<%= module_name %>::Base.new` is
|
|
2
|
+
# instantiated from app.rb at startup, so anything set in #initialize
|
|
3
|
+
# takes effect before the listener spawns.
|
|
4
|
+
|
|
5
|
+
# Sign signed-session cookies (and anything else that uses HMAC) with
|
|
6
|
+
# the value of $SESSION_SECRET. Falls back to a placeholder for the
|
|
7
|
+
# dev loop so /session works out of the box; production deployments
|
|
8
|
+
# MUST set the env var to a long random string (e.g. 64 hex bytes
|
|
9
|
+
# from `openssl rand -hex 32`). Empty value silently disables
|
|
10
|
+
# sessions entirely.
|
|
11
|
+
#
|
|
12
|
+
# Split the ENV read from the assignment instead of `ENV.fetch(key,
|
|
13
|
+
# default)`: Spinel's two-arg `ENV.fetch` codegen emits a `const
|
|
14
|
+
# char *` ternary but the surrounding context wraps the result in
|
|
15
|
+
# `sp_box_str`, producing an "assigning to 'const char *' from
|
|
16
|
+
# incompatible type 'sp_RbVal'" compile error at the setter site.
|
|
17
|
+
session_secret = ENV["SESSION_SECRET"]
|
|
18
|
+
if session_secret.nil? || session_secret.empty?
|
|
19
|
+
session_secret = "dev-only-INSECURE-session-secret-change-me-for-prod"
|
|
20
|
+
end
|
|
21
|
+
Fresco.set_session_secret(session_secret)
|
|
22
|
+
|
|
23
|
+
module <%= module_name %>
|
|
24
|
+
class Base < Fresco::App
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Fresco: database configuration. Loaded twice — once by `fresco
|
|
2
|
+
# build` to capture the adapter symbol (drives the codegen template
|
|
3
|
+
# selection in generated/db_adapter.rb), and once at runtime as part
|
|
4
|
+
# of boot.rb so ENV.fetch'd values resolve in the running binary's
|
|
5
|
+
# environment rather than at build time.
|
|
6
|
+
#
|
|
7
|
+
# Switching adapters requires re-running `fresco build` (the linker
|
|
8
|
+
# picks one C shim or the other; we don't link both). For SQLite, the
|
|
9
|
+
# path can be `:memory:` for an ephemeral in-process DB, or any
|
|
10
|
+
# filesystem path. For Postgres, pass any libpq conninfo string —
|
|
11
|
+
# `postgres://user@host/db` or `host=... user=... dbname=...`.
|
|
12
|
+
|
|
13
|
+
Fresco.database :sqlite,
|
|
14
|
+
path: ENV.fetch("DATABASE_PATH", "db/app.sqlite3")
|
|
15
|
+
|
|
16
|
+
# Fresco.database :postgres,
|
|
17
|
+
# url: ENV.fetch("DATABASE_URL", "postgres://localhost/app_dev")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Fresco: route definitions.
|
|
2
|
+
#
|
|
3
|
+
# Evaluated by `fresco build` under CRuby. The block captures (verb,
|
|
4
|
+
# pattern, action_class) triples on Fresco.app.routes; build emits
|
|
5
|
+
# generated/dispatch.rb from them. Not loaded at runtime —
|
|
6
|
+
# production reads the generated dispatcher.
|
|
7
|
+
|
|
8
|
+
Fresco.app.routes do
|
|
9
|
+
# Uncomment and edit the line below to define your app's root route. Example:
|
|
10
|
+
# root to: RootPath
|
|
11
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Fresco: schema declaration. Loaded by `fresco build` to drive model
|
|
2
|
+
# codegen. Add tables with:
|
|
3
|
+
#
|
|
4
|
+
# Fresco.schema do
|
|
5
|
+
# table :users do
|
|
6
|
+
# column :id, :int, primary_key: true
|
|
7
|
+
# column :email, :str, null: false, index: :unique
|
|
8
|
+
# end
|
|
9
|
+
# end
|
|
10
|
+
#
|
|
11
|
+
# Runtime never loads this file — generated/models/*.rb are
|
|
12
|
+
# self-contained.
|
|
13
|
+
Fresco.schema do
|
|
14
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>404 — Not Found</title>
|
|
6
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
7
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
|
9
|
+
<script>
|
|
10
|
+
// Pre-paint so the page doesn't flash light before JS runs. If
|
|
11
|
+
// the user has no stored preference we seed from the OS, then
|
|
12
|
+
// persist so subsequent visits skip the matchMedia probe.
|
|
13
|
+
(function () {
|
|
14
|
+
var t = localStorage.getItem("theme");
|
|
15
|
+
if (!t) {
|
|
16
|
+
t = (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light";
|
|
17
|
+
localStorage.setItem("theme", t);
|
|
18
|
+
}
|
|
19
|
+
document.documentElement.setAttribute("data-theme", t);
|
|
20
|
+
})();
|
|
21
|
+
</script>
|
|
22
|
+
<style>
|
|
23
|
+
:root {
|
|
24
|
+
--bg: #fff; --fg: #222; --muted: #888;
|
|
25
|
+
--border: #eee; --code-bg: #f7f7f7;
|
|
26
|
+
--accent: #0066cc; --status: #888;
|
|
27
|
+
}
|
|
28
|
+
:root[data-theme="dark"] {
|
|
29
|
+
--bg: #141414; --fg: #e8e8e8; --muted: #9a9a9a;
|
|
30
|
+
--border: #2a2a2a; --code-bg: #1f1f1f;
|
|
31
|
+
--accent: #6ab0ff; --status: #b8b8b8;
|
|
32
|
+
}
|
|
33
|
+
body {
|
|
34
|
+
font-family: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
35
|
+
max-width: 40rem;
|
|
36
|
+
margin: 4rem auto;
|
|
37
|
+
padding: 0 1.5rem;
|
|
38
|
+
background: var(--bg);
|
|
39
|
+
color: var(--fg);
|
|
40
|
+
transition: background 0.15s, color 0.15s;
|
|
41
|
+
}
|
|
42
|
+
h1 { font-weight: 700; font-size: 1.5rem; margin-bottom: 0.25rem; }
|
|
43
|
+
.status { color: var(--status); font-size: 0.9rem; letter-spacing: 0.05em; text-transform: uppercase; }
|
|
44
|
+
p { line-height: 1.5; }
|
|
45
|
+
code { background: var(--code-bg); padding: 0.1rem 0.3rem; border-radius: 3px; }
|
|
46
|
+
a { color: var(--accent); }
|
|
47
|
+
a:hover { text-decoration: underline; }
|
|
48
|
+
hr { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
|
|
49
|
+
footer { color: var(--muted); font-size: 0.85rem; }
|
|
50
|
+
#theme-toggle {
|
|
51
|
+
position: fixed; top: 1rem; right: 1rem;
|
|
52
|
+
background: transparent; border: 1px solid var(--border);
|
|
53
|
+
color: var(--fg); border-radius: 6px;
|
|
54
|
+
padding: 0.4rem; cursor: pointer;
|
|
55
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
56
|
+
}
|
|
57
|
+
#theme-toggle:hover { background: var(--code-bg); }
|
|
58
|
+
#theme-toggle svg { width: 18px; height: 18px; display: none; }
|
|
59
|
+
:root[data-theme="light"] #theme-toggle .icon-moon { display: inline-block; }
|
|
60
|
+
:root[data-theme="dark"] #theme-toggle .icon-sun { display: inline-block; }
|
|
61
|
+
</style>
|
|
62
|
+
</head>
|
|
63
|
+
<body>
|
|
64
|
+
<button id="theme-toggle" aria-label="Toggle color theme" title="Toggle color theme">
|
|
65
|
+
<svg class="icon-sun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>
|
|
66
|
+
<svg class="icon-moon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
|
67
|
+
</button>
|
|
68
|
+
|
|
69
|
+
<div class="status">404 · Not Found</div>
|
|
70
|
+
<h1>That page isn't here.</h1>
|
|
71
|
+
<p>The URL you requested didn't match any route, or the file you asked for doesn't exist under <code>public/</code>.</p>
|
|
72
|
+
<p><a href="/">Back to home</a></p>
|
|
73
|
+
<hr>
|
|
74
|
+
<footer>Powered by Spinel + Fresco.</footer>
|
|
75
|
+
|
|
76
|
+
<script>
|
|
77
|
+
(function () {
|
|
78
|
+
document.getElementById("theme-toggle").addEventListener("click", function () {
|
|
79
|
+
var cur = document.documentElement.getAttribute("data-theme");
|
|
80
|
+
var nxt = cur === "dark" ? "light" : "dark";
|
|
81
|
+
document.documentElement.setAttribute("data-theme", nxt);
|
|
82
|
+
localStorage.setItem("theme", nxt);
|
|
83
|
+
});
|
|
84
|
+
})();
|
|
85
|
+
</script>
|
|
86
|
+
</body>
|
|
87
|
+
</html>
|