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,84 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>500 — Internal Server Error</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
|
+
(function () {
|
|
11
|
+
var t = localStorage.getItem("theme");
|
|
12
|
+
if (!t) {
|
|
13
|
+
t = (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light";
|
|
14
|
+
localStorage.setItem("theme", t);
|
|
15
|
+
}
|
|
16
|
+
document.documentElement.setAttribute("data-theme", t);
|
|
17
|
+
})();
|
|
18
|
+
</script>
|
|
19
|
+
<style>
|
|
20
|
+
:root {
|
|
21
|
+
--bg: #fff; --fg: #222; --muted: #888;
|
|
22
|
+
--border: #eee; --code-bg: #f7f7f7;
|
|
23
|
+
--accent: #0066cc; --status: #b00020;
|
|
24
|
+
}
|
|
25
|
+
:root[data-theme="dark"] {
|
|
26
|
+
--bg: #141414; --fg: #e8e8e8; --muted: #9a9a9a;
|
|
27
|
+
--border: #2a2a2a; --code-bg: #1f1f1f;
|
|
28
|
+
--accent: #6ab0ff; --status: #ff6680;
|
|
29
|
+
}
|
|
30
|
+
body {
|
|
31
|
+
font-family: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
32
|
+
max-width: 40rem;
|
|
33
|
+
margin: 4rem auto;
|
|
34
|
+
padding: 0 1.5rem;
|
|
35
|
+
background: var(--bg);
|
|
36
|
+
color: var(--fg);
|
|
37
|
+
transition: background 0.15s, color 0.15s;
|
|
38
|
+
}
|
|
39
|
+
h1 { font-weight: 700; font-size: 1.5rem; margin-bottom: 0.25rem; }
|
|
40
|
+
.status { color: var(--status); font-size: 0.9rem; letter-spacing: 0.05em; text-transform: uppercase; }
|
|
41
|
+
p { line-height: 1.5; }
|
|
42
|
+
code { background: var(--code-bg); padding: 0.1rem 0.3rem; border-radius: 3px; }
|
|
43
|
+
a { color: var(--accent); }
|
|
44
|
+
a:hover { text-decoration: underline; }
|
|
45
|
+
hr { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
|
|
46
|
+
footer { color: var(--muted); font-size: 0.85rem; }
|
|
47
|
+
#theme-toggle {
|
|
48
|
+
position: fixed; top: 1rem; right: 1rem;
|
|
49
|
+
background: transparent; border: 1px solid var(--border);
|
|
50
|
+
color: var(--fg); border-radius: 6px;
|
|
51
|
+
padding: 0.4rem; cursor: pointer;
|
|
52
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
53
|
+
}
|
|
54
|
+
#theme-toggle:hover { background: var(--code-bg); }
|
|
55
|
+
#theme-toggle svg { width: 18px; height: 18px; display: none; }
|
|
56
|
+
:root[data-theme="light"] #theme-toggle .icon-moon { display: inline-block; }
|
|
57
|
+
:root[data-theme="dark"] #theme-toggle .icon-sun { display: inline-block; }
|
|
58
|
+
</style>
|
|
59
|
+
</head>
|
|
60
|
+
<body>
|
|
61
|
+
<button id="theme-toggle" aria-label="Toggle color theme" title="Toggle color theme">
|
|
62
|
+
<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>
|
|
63
|
+
<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>
|
|
64
|
+
</button>
|
|
65
|
+
|
|
66
|
+
<div class="status">500 · Internal Server Error</div>
|
|
67
|
+
<h1>Something went wrong on our end.</h1>
|
|
68
|
+
<p>An action raised an exception while handling the request. The error has been logged; the page you tried to load couldn't be rendered.</p>
|
|
69
|
+
<p><a href="/">Back to home</a></p>
|
|
70
|
+
<hr>
|
|
71
|
+
<footer>Powered by Spinel + Fresco.</footer>
|
|
72
|
+
|
|
73
|
+
<script>
|
|
74
|
+
(function () {
|
|
75
|
+
document.getElementById("theme-toggle").addEventListener("click", function () {
|
|
76
|
+
var cur = document.documentElement.getAttribute("data-theme");
|
|
77
|
+
var nxt = cur === "dark" ? "light" : "dark";
|
|
78
|
+
document.documentElement.setAttribute("data-theme", nxt);
|
|
79
|
+
localStorage.setItem("theme", nxt);
|
|
80
|
+
});
|
|
81
|
+
})();
|
|
82
|
+
</script>
|
|
83
|
+
</body>
|
|
84
|
+
</html>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Build-time migration DSL. db/migrations/<NNNN>_<name>.rb files
|
|
2
|
+
# each call `Fresco.migration "<NNNN>_<name>" do ... end`. `fresco build`
|
|
3
|
+
# loads all migration files in sorted order and emits the captured
|
|
4
|
+
# SQL into generated/migrations.rb, which the runtime runs via
|
|
5
|
+
# `./app db:migrate` / `./app db:rollback`.
|
|
6
|
+
#
|
|
7
|
+
# Each migration captures two ordered Arrays of SQL strings (up and
|
|
8
|
+
# down). No DSL helpers for ALTER / ADD / RENAME yet — write raw SQL
|
|
9
|
+
# in the block. Adapter-specific syntax goes in the file the
|
|
10
|
+
# developer wrote; we don't translate. (M6+ could add adapter-aware
|
|
11
|
+
# helpers, but raw SQL covers the M5 footprint.)
|
|
12
|
+
module Fresco
|
|
13
|
+
@migrations = []
|
|
14
|
+
|
|
15
|
+
def self.migrations
|
|
16
|
+
@migrations
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.migration(name, &blk)
|
|
20
|
+
builder = Fresco::MigrationBuilder.new
|
|
21
|
+
builder.instance_eval(&blk) if blk
|
|
22
|
+
@migrations << {
|
|
23
|
+
name: name,
|
|
24
|
+
up_sql: builder.up_sql,
|
|
25
|
+
down_sql: builder.down_sql,
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class Fresco::MigrationBuilder
|
|
31
|
+
attr_reader :up_sql, :down_sql
|
|
32
|
+
|
|
33
|
+
def initialize
|
|
34
|
+
@up_sql = []
|
|
35
|
+
@down_sql = []
|
|
36
|
+
@current = nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def up(&blk)
|
|
40
|
+
@current = @up_sql
|
|
41
|
+
instance_eval(&blk) if blk
|
|
42
|
+
@current = nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def down(&blk)
|
|
46
|
+
@current = @down_sql
|
|
47
|
+
instance_eval(&blk) if blk
|
|
48
|
+
@current = nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def sql(stmt)
|
|
52
|
+
abort "[build] `sql` called outside `up` / `down` block" if @current.nil?
|
|
53
|
+
@current << stmt
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Build-time model DSL. Captured by `app/models/*.rb` files; each
|
|
2
|
+
# calls `Fresco.model :User, table: :users do ... end`. `fresco build`
|
|
3
|
+
# iterates `Fresco.models` and emits one generated/models/*.rb
|
|
4
|
+
# per entry, with adapter-specific SQL baked in.
|
|
5
|
+
#
|
|
6
|
+
# Shape captured per model:
|
|
7
|
+
# { name: :User, table: :users, finders: [:email, :name] }
|
|
8
|
+
module Fresco
|
|
9
|
+
@models = []
|
|
10
|
+
|
|
11
|
+
def self.models
|
|
12
|
+
@models
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.model(name, table:, &blk)
|
|
16
|
+
builder = Fresco::ModelBuilder.new
|
|
17
|
+
builder.instance_eval(&blk) if blk
|
|
18
|
+
@models << {
|
|
19
|
+
name: name,
|
|
20
|
+
table: table,
|
|
21
|
+
finders: builder.finders,
|
|
22
|
+
validators: builder.validators,
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class Fresco::ModelBuilder
|
|
28
|
+
attr_reader :finders, :validators
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
@finders = []
|
|
32
|
+
@validators = []
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Each call adds one indexed column that gets a `where_<col>`
|
|
36
|
+
# finder method on the generated model class. Spinel-shape
|
|
37
|
+
# constraint: each finder is its own typed method, single arity —
|
|
38
|
+
# we don't generate a dynamic `where(hash)` because hash dispatch
|
|
39
|
+
# widens to RbVal.
|
|
40
|
+
def finder(column_name)
|
|
41
|
+
@finders << column_name
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# M4 validations DSL. Currently supports `validates :col, presence:
|
|
45
|
+
# true` — emitted as a top-of-method early-return guard inside
|
|
46
|
+
# insert/update. All validation is pure-Ruby (no DB roundtrip);
|
|
47
|
+
# rules expand to monomorphic `==` / `.length` checks so Spinel
|
|
48
|
+
# doesn't widen the param types.
|
|
49
|
+
def validates(column_name, **opts)
|
|
50
|
+
if opts[:presence]
|
|
51
|
+
@validators << { column: column_name, rule: :presence }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/fresco/paths.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Resolves paths into the installed gem so the CLI never hardcodes
|
|
2
|
+
# anything cwd-relative. Codegen reads templates and verbatim runtime
|
|
3
|
+
# files from here; `fresco new` reads the app skeleton from here.
|
|
4
|
+
module Fresco
|
|
5
|
+
module Paths
|
|
6
|
+
GEM_ROOT = File.expand_path("../..", __dir__)
|
|
7
|
+
|
|
8
|
+
def self.template_root
|
|
9
|
+
File.join(GEM_ROOT, "lib/fresco/templates")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.runtime_root
|
|
13
|
+
File.join(GEM_ROOT, "lib/fresco/runtime")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.generator_root
|
|
17
|
+
File.join(GEM_ROOT, "lib/fresco/generators/app")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
class Fresco::Router
|
|
2
|
+
# Rails-style CRUD expansion table. Verb / suffix / class suffix
|
|
3
|
+
# for each of the seven canonical resource actions.
|
|
4
|
+
RESOURCE_ROUTES = [
|
|
5
|
+
[:index, "GET", "", "Index"],
|
|
6
|
+
[:new, "GET", "/new", "New"],
|
|
7
|
+
[:create, "POST", "", "Create"],
|
|
8
|
+
[:show, "GET", "/:id", "Show"],
|
|
9
|
+
[:edit, "GET", "/:id/edit", "Edit"],
|
|
10
|
+
[:update, "PATCH", "/:id", "Update"],
|
|
11
|
+
[:destroy, "DELETE", "/:id", "Destroy"],
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
# Each entry: [verb, pattern, klass, constraints]. `fresco build` reads
|
|
15
|
+
# this and emits generated/dispatch.rb.
|
|
16
|
+
attr_reader :entries
|
|
17
|
+
|
|
18
|
+
def initialize
|
|
19
|
+
@entries = []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
%i[get post put delete patch].each do |verb|
|
|
23
|
+
define_method(verb) do |pattern, to:, constraints: {}|
|
|
24
|
+
@entries << [verb.to_s.upcase, pattern, to, constraints]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Sugar for `get "/", to: ...`. Mirrors Rails' `root to: Foo`.
|
|
29
|
+
def root(to:)
|
|
30
|
+
@entries << ["GET", "/", to, {}]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Expand `resources :users` into the seven CRUD routes. `only:` /
|
|
34
|
+
# `except:` filter the set. Each generated route expects a matching
|
|
35
|
+
# action file under app/actions/ — missing files fail the build
|
|
36
|
+
# with a path-and-suggestion message rather than emitting a
|
|
37
|
+
# dispatch line that references a non-existent class.
|
|
38
|
+
# `resources :users` resolves to namespaced classes — `Users::Index`
|
|
39
|
+
# at `app/actions/users/index.rb`, not `UsersIndex` flat. Mixing
|
|
40
|
+
# `resources` with flat files isn't supported on purpose: if you
|
|
41
|
+
# opt into the resources DSL, you opt into the folder convention.
|
|
42
|
+
def resources(name, only: nil, except: nil)
|
|
43
|
+
base_path = "/#{name}"
|
|
44
|
+
base_class = camelize(name)
|
|
45
|
+
|
|
46
|
+
RESOURCE_ROUTES.each do |action, verb, suffix, class_suffix|
|
|
47
|
+
next if only && !only.include?(action)
|
|
48
|
+
next if except && except.include?(action)
|
|
49
|
+
|
|
50
|
+
class_name = "#{base_class}::#{class_suffix}"
|
|
51
|
+
file_path = "app/actions/#{Fresco.class_to_path(class_name)}.rb"
|
|
52
|
+
unless File.exist?(file_path)
|
|
53
|
+
raise "[build] resources :#{name} expects #{file_path} for :#{action} " \
|
|
54
|
+
"(add the file or use `only:`/`except:`)"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
Fresco.const_set_path(class_name, Class.new)
|
|
58
|
+
@entries << [verb, base_path + suffix, Fresco.const_get_path(class_name), {}]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def camelize(name)
|
|
65
|
+
name.to_s.split("_").map(&:capitalize).join
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Fresco: boot sequence. Auto-generated by `fresco build`; do not edit.
|
|
2
|
+
#
|
|
3
|
+
# Source of truth: lib/fresco/runtime/boot.rb in the fresco gem —
|
|
4
|
+
# `fresco build` copies this file verbatim into generated/boot.rb.
|
|
5
|
+
# app.rb requires it and then hands off to App::Base.run.
|
|
6
|
+
#
|
|
7
|
+
# Load order matters:
|
|
8
|
+
# runtime → Fresco::Action, Fresco::App, Request, Response, Sock
|
|
9
|
+
# views → render_<name> helpers (depend on Spinel::View)
|
|
10
|
+
# layout_dispatch → dispatch_layout (depends on render_layouts_<name>)
|
|
11
|
+
# db_adapter → Fresco::Db::Active + adapter FFI module (Sqlite or Pg);
|
|
12
|
+
# empty when no config/database.rb is present
|
|
13
|
+
# migrations → Fresco::DbMigrations runner with embedded SQL;
|
|
14
|
+
# consumed by `./app db:migrate` / `db:rollback`
|
|
15
|
+
# and depends on Fresco::Db.exec being defined
|
|
16
|
+
# config/app → App::Base < Fresco::App (depends on runtime)
|
|
17
|
+
# config/database → Fresco.database :sqlite/:postgres call; sets
|
|
18
|
+
# Fresco.app.database_config so actions and (later)
|
|
19
|
+
# generated code can read the configured DSN at
|
|
20
|
+
# runtime via ENV.fetch
|
|
21
|
+
# requires → models + app/action.rb + app/actions/*.rb (depend
|
|
22
|
+
# on Fresco::App, dispatch_layout, Fresco::Db, and
|
|
23
|
+
# any view helpers actions may call)
|
|
24
|
+
# dispatch → dispatch_request (depends on action classes being defined)
|
|
25
|
+
|
|
26
|
+
require_relative "runtime"
|
|
27
|
+
require_relative "views"
|
|
28
|
+
require_relative "layout_dispatch"
|
|
29
|
+
require_relative "db_adapter"
|
|
30
|
+
require_relative "migrations"
|
|
31
|
+
require_relative "../config/app"
|
|
32
|
+
require_relative "../config/database"
|
|
33
|
+
require_relative "requires"
|
|
34
|
+
require_relative "dispatch"
|