belt 0.1.9 → 0.1.11
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 +4 -4
- data/CHANGELOG.md +20 -0
- data/lib/belt/cli/frontend_deploy_command.rb +21 -13
- data/lib/belt/cli/frontend_env_command.rb +110 -0
- data/lib/belt/cli/frontend_env_map.rb +188 -0
- data/lib/belt/cli/routes_command/route_inference.rb +5 -1
- data/lib/belt/cli/server_command.rb +25 -7
- data/lib/belt/cli.rb +6 -0
- data/lib/belt/route_dsl.rb +29 -12
- data/lib/belt/version.rb +1 -1
- data/lib/templates/frontend/react/env.yml.example +13 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 68099232d7da2991436a263de28721bfc4e15822116f11a42cd2433588293b59
|
|
4
|
+
data.tar.gz: ba091a54897cfd88b02e2ddf48750ca8a47ca3f4c298b7b4371be799e835550e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8e3d77c55c4297f2978486d9d0755ce45fda7f4a8886c372af0b211952502e5f5fde50c0d8881d4d5db39be4c3747a469825dfbd1b5d6f9be1120d441116600a
|
|
7
|
+
data.tar.gz: 7226f0ce500b1d19a7bccf60f9f087573998f3589ae7b41dceba7f35289a71e7e4ea3250269b96b18ef823acff06bbd67916f8d39c1ecb025ce97fbb8288019e
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.11
|
|
6
|
+
|
|
7
|
+
### Route DSL
|
|
8
|
+
|
|
9
|
+
- `resources` / `resource` honor `scope path:` (and nested path scopes).
|
|
10
|
+
Example: `scope path: "admin", auth: :cognito { resources :users }` → `/admin/users…` with Cognito auth.
|
|
11
|
+
- Nested `scope path:` segments stack (`a` then `b` → `a/b`) instead of replacing.
|
|
12
|
+
- `scope controller:` is applied to `resources` / `resource` the same way as verb routes.
|
|
13
|
+
- Controller inference for multi-segment resource paths joins segments (`/admin/users` → `admin/users`), matching nested-resource convention.
|
|
14
|
+
|
|
15
|
+
## 0.1.10
|
|
16
|
+
|
|
17
|
+
### Frontend env map
|
|
18
|
+
|
|
19
|
+
- Declarative map (`frontend/env.yml` or `.belt/frontend_env.yml`) maps process env names → terraform output names (framework-agnostic: `VITE_*`, `REACT_APP_*`, `NEXT_PUBLIC_*`, etc.)
|
|
20
|
+
- `belt deploy frontend <env>` injects mapped vars into `npm run build` process env
|
|
21
|
+
- `belt frontend env <env>` smart-merges mapped keys into `frontend/.env` (preserves unmapped keys/comments; missing TF outputs warn and do not clobber)
|
|
22
|
+
- No map → previous default: only `VITE_API_URL` ← `api_url`
|
|
23
|
+
- `belt server` uses the same map for local process env
|
|
24
|
+
|
|
5
25
|
## 0.1.4
|
|
6
26
|
|
|
7
27
|
### Bug fixes
|
|
@@ -4,6 +4,7 @@ require 'shellwords'
|
|
|
4
4
|
require 'open3'
|
|
5
5
|
require_relative 'app_detection'
|
|
6
6
|
require_relative 'env_resolver'
|
|
7
|
+
require_relative 'frontend_env_map'
|
|
7
8
|
|
|
8
9
|
module Belt
|
|
9
10
|
module CLI
|
|
@@ -60,11 +61,18 @@ module Belt
|
|
|
60
61
|
run!(*install_cmd, chdir: 'frontend')
|
|
61
62
|
|
|
62
63
|
puts '🏗️ Building frontend...'
|
|
63
|
-
|
|
64
|
-
env
|
|
64
|
+
env = frontend_build_env
|
|
65
|
+
if env.any?
|
|
66
|
+
puts " Injecting env: #{env.keys.sort.join(', ')}"
|
|
67
|
+
end
|
|
65
68
|
run!(env, 'npm', 'run', 'build', chdir: 'frontend')
|
|
66
69
|
end
|
|
67
70
|
|
|
71
|
+
# Resolve build env from frontend/env.yml map (or default VITE_API_URL ← api_url).
|
|
72
|
+
def frontend_build_env
|
|
73
|
+
FrontendEnvMap.new(@env, env_dir: @env_dir).process_env
|
|
74
|
+
end
|
|
75
|
+
|
|
68
76
|
def sync_to_s3
|
|
69
77
|
bucket = fetch_bucket_name
|
|
70
78
|
abort "Error: Could not determine S3 bucket. Run `belt apply #{@env}` first." unless bucket
|
|
@@ -94,24 +102,24 @@ module Belt
|
|
|
94
102
|
puts '✅ CloudFront cache invalidated'
|
|
95
103
|
end
|
|
96
104
|
|
|
97
|
-
def fetch_api_url
|
|
98
|
-
output, status = Open3.capture2('terraform', 'output', '-raw', 'api_url', chdir: @env_dir)
|
|
99
|
-
status.success? && !output.strip.empty? ? output.strip : nil
|
|
100
|
-
end
|
|
101
|
-
|
|
102
105
|
def fetch_bucket_name
|
|
103
|
-
|
|
104
|
-
status.success? && !output.strip.empty? ? output.strip : nil
|
|
106
|
+
fetch_tf_output('frontend_bucket_name')
|
|
105
107
|
end
|
|
106
108
|
|
|
107
109
|
def fetch_distribution_id
|
|
108
|
-
|
|
109
|
-
status.success? && !output.strip.empty? ? output.strip : nil
|
|
110
|
+
fetch_tf_output('frontend_distribution_id')
|
|
110
111
|
end
|
|
111
112
|
|
|
112
113
|
def fetch_frontend_url
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
fetch_tf_output('frontend_url')
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def fetch_tf_output(name)
|
|
118
|
+
output, status = Open3.capture2('terraform', 'output', '-raw', name, chdir: @env_dir)
|
|
119
|
+
return nil unless status.success?
|
|
120
|
+
|
|
121
|
+
value = output.strip
|
|
122
|
+
value.empty? || value == 'null' ? nil : value
|
|
115
123
|
end
|
|
116
124
|
|
|
117
125
|
def run!(*args, **)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'app_detection'
|
|
4
|
+
require_relative 'env_resolver'
|
|
5
|
+
require_relative 'frontend_env_map'
|
|
6
|
+
|
|
7
|
+
module Belt
|
|
8
|
+
module CLI
|
|
9
|
+
# belt frontend env <environment>
|
|
10
|
+
#
|
|
11
|
+
# Smart-merges terraform outputs into frontend/.env using the declarative map
|
|
12
|
+
# (frontend/env.yml or .belt/frontend_env.yml). Only mapped keys are written;
|
|
13
|
+
# custom local keys are preserved.
|
|
14
|
+
class FrontendEnvCommand
|
|
15
|
+
include AppDetection
|
|
16
|
+
|
|
17
|
+
def self.run(args)
|
|
18
|
+
subcommand = args.shift
|
|
19
|
+
|
|
20
|
+
case subcommand
|
|
21
|
+
when 'env'
|
|
22
|
+
run_env(args)
|
|
23
|
+
when nil, '-h', '--help', 'help'
|
|
24
|
+
puts usage
|
|
25
|
+
exit(subcommand.nil? ? 1 : 0)
|
|
26
|
+
else
|
|
27
|
+
puts "Unknown frontend subcommand: #{subcommand}\n\n#{usage}"
|
|
28
|
+
exit 1
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.run_env(args)
|
|
33
|
+
env = EnvResolver.resolve(args)
|
|
34
|
+
|
|
35
|
+
if env.nil?
|
|
36
|
+
puts 'Usage: belt frontend env <environment>'
|
|
37
|
+
puts "\nWrites frontend/.env from terraform outputs using the env map."
|
|
38
|
+
puts 'You can also set BELT_ENV to skip the environment argument.'
|
|
39
|
+
puts "\nMap file (optional): frontend/env.yml or .belt/frontend_env.yml"
|
|
40
|
+
puts 'Default without map: VITE_API_URL ← api_url'
|
|
41
|
+
puts "\nExamples:"
|
|
42
|
+
puts ' belt frontend env dev'
|
|
43
|
+
puts ' BELT_ENV=dev belt frontend env'
|
|
44
|
+
exit 1
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
new(env).run
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.usage
|
|
51
|
+
<<~USAGE
|
|
52
|
+
Frontend helpers.
|
|
53
|
+
|
|
54
|
+
Usage:
|
|
55
|
+
belt frontend env <environment> Write frontend/.env from terraform outputs
|
|
56
|
+
belt frontend --help
|
|
57
|
+
|
|
58
|
+
Env map (optional):
|
|
59
|
+
frontend/env.yml
|
|
60
|
+
.belt/frontend_env.yml
|
|
61
|
+
|
|
62
|
+
Example map:
|
|
63
|
+
VITE_API_URL: api_url
|
|
64
|
+
VITE_COGNITO_USER_POOL_ID: cognito_user_pool_id
|
|
65
|
+
VITE_COGNITO_CLIENT_ID: cognito_user_pool_client_id
|
|
66
|
+
VITE_AWS_REGION: cognito_region
|
|
67
|
+
|
|
68
|
+
Without a map, belt injects only VITE_API_URL from api_url.
|
|
69
|
+
|
|
70
|
+
Smart merge: only keys listed in the map are updated. Other .env keys
|
|
71
|
+
(local secrets, feature flags) are left alone. Missing terraform outputs
|
|
72
|
+
warn and do not overwrite existing values.
|
|
73
|
+
USAGE
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def initialize(env)
|
|
77
|
+
@env = env
|
|
78
|
+
@app_name = detect_app_name
|
|
79
|
+
@env_dir = "infrastructure/#{@env}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def run
|
|
83
|
+
unless Dir.exist?('frontend')
|
|
84
|
+
abort 'Error: No frontend/ directory found. Run `belt generate frontend react` first.'
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
unless Dir.exist?(@env_dir)
|
|
88
|
+
abort "Error: infrastructure/#{@env} not found. Run `belt generate environment #{@env}` first."
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
map = FrontendEnvMap.new(@env, env_dir: @env_dir)
|
|
92
|
+
|
|
93
|
+
if map.using_default_map?
|
|
94
|
+
puts '📋 No frontend/env.yml found — using default map (VITE_API_URL ← api_url)'
|
|
95
|
+
else
|
|
96
|
+
puts "📋 Loading env map from #{map.map_path}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
result = map.write_dotenv!
|
|
100
|
+
updated = result[:updated]
|
|
101
|
+
|
|
102
|
+
if updated.empty?
|
|
103
|
+
puts "⚠️ No values written to #{result[:path]} (terraform outputs missing?)"
|
|
104
|
+
else
|
|
105
|
+
puts "✅ Updated #{result[:path]} (#{updated.join(', ')})"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'open3'
|
|
5
|
+
require 'yaml'
|
|
6
|
+
|
|
7
|
+
module Belt
|
|
8
|
+
module CLI
|
|
9
|
+
# Resolves frontend env vars from a declarative map (env name → terraform output).
|
|
10
|
+
#
|
|
11
|
+
# Map file (optional), first match wins:
|
|
12
|
+
# frontend/env.yml
|
|
13
|
+
# .belt/frontend_env.yml
|
|
14
|
+
#
|
|
15
|
+
# Example:
|
|
16
|
+
# VITE_API_URL: api_url
|
|
17
|
+
# VITE_COGNITO_USER_POOL_ID: cognito_user_pool_id
|
|
18
|
+
# REACT_APP_API_URL: api_url
|
|
19
|
+
#
|
|
20
|
+
# No map → default { "VITE_API_URL" => "api_url" } (backwards compatible).
|
|
21
|
+
class FrontendEnvMap
|
|
22
|
+
MAP_CANDIDATES = [
|
|
23
|
+
File.join('frontend', 'env.yml'),
|
|
24
|
+
File.join('.belt', 'frontend_env.yml')
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
DEFAULT_MAP = { 'VITE_API_URL' => 'api_url' }.freeze
|
|
28
|
+
|
|
29
|
+
DOTENV_PATH = File.join('frontend', '.env')
|
|
30
|
+
|
|
31
|
+
attr_reader :env_name, :env_dir, :map_path, :map
|
|
32
|
+
|
|
33
|
+
def initialize(env_name, env_dir: nil, map_path: nil)
|
|
34
|
+
@env_name = env_name
|
|
35
|
+
@env_dir = env_dir || File.join('infrastructure', env_name)
|
|
36
|
+
@map_path = map_path || self.class.find_map_path
|
|
37
|
+
@map = load_map
|
|
38
|
+
@tf_cache = {}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.find_map_path
|
|
42
|
+
MAP_CANDIDATES.find { |path| File.file?(path) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Hash of env_var => value suitable for process env (npm run build / vite).
|
|
46
|
+
# Skips keys whose terraform output is missing.
|
|
47
|
+
def process_env
|
|
48
|
+
resolved = {}
|
|
49
|
+
each_resolved do |env_key, value, _tf_output|
|
|
50
|
+
resolved[env_key] = value if value
|
|
51
|
+
end
|
|
52
|
+
resolved
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Smart-merge mapped keys into frontend/.env.
|
|
56
|
+
# - Only overwrites keys present in the map
|
|
57
|
+
# - Preserves unknown keys, comments, and blank lines
|
|
58
|
+
# - Missing TF output → warn, do not clobber an existing value
|
|
59
|
+
def write_dotenv!(path: DOTENV_PATH)
|
|
60
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
61
|
+
|
|
62
|
+
existing_lines = File.file?(path) ? File.readlines(path, chomp: true) : []
|
|
63
|
+
updates = {}
|
|
64
|
+
missing = []
|
|
65
|
+
|
|
66
|
+
each_resolved do |env_key, value, tf_output|
|
|
67
|
+
if value
|
|
68
|
+
updates[env_key] = value
|
|
69
|
+
else
|
|
70
|
+
missing << [env_key, tf_output]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
missing.each do |env_key, tf_output|
|
|
75
|
+
puts "⚠️ terraform output '#{tf_output}' missing — leaving #{env_key} unchanged in #{path}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
written = {}
|
|
79
|
+
new_lines = existing_lines.map do |line|
|
|
80
|
+
key = dotenv_key(line)
|
|
81
|
+
if key && updates.key?(key)
|
|
82
|
+
written[key] = true
|
|
83
|
+
format_dotenv_line(key, updates[key])
|
|
84
|
+
else
|
|
85
|
+
line
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
updates.each do |key, value|
|
|
90
|
+
next if written[key]
|
|
91
|
+
|
|
92
|
+
new_lines << format_dotenv_line(key, value)
|
|
93
|
+
written[key] = true
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
content = "#{new_lines.join("\n")}\n"
|
|
97
|
+
File.write(path, content)
|
|
98
|
+
|
|
99
|
+
{
|
|
100
|
+
path: path,
|
|
101
|
+
updated: written.keys.sort,
|
|
102
|
+
missing: missing.map(&:first)
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def using_default_map?
|
|
107
|
+
@map_path.nil?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def load_map
|
|
113
|
+
return DEFAULT_MAP.dup if @map_path.nil?
|
|
114
|
+
|
|
115
|
+
raw = YAML.safe_load(File.read(@map_path), aliases: false)
|
|
116
|
+
unless raw.is_a?(Hash) && raw.any?
|
|
117
|
+
abort "Error: frontend env map #{@map_path} must be a non-empty YAML mapping " \
|
|
118
|
+
'(e.g. VITE_API_URL: api_url).'
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
raw.each_with_object({}) do |(key, value), hash|
|
|
122
|
+
k = key.to_s.strip
|
|
123
|
+
v = value.to_s.strip
|
|
124
|
+
next if k.empty? || v.empty?
|
|
125
|
+
|
|
126
|
+
hash[k] = v
|
|
127
|
+
end.tap do |parsed|
|
|
128
|
+
abort "Error: frontend env map #{@map_path} has no valid entries." if parsed.empty?
|
|
129
|
+
end
|
|
130
|
+
rescue Psych::SyntaxError => e
|
|
131
|
+
abort "Error: invalid YAML in #{@map_path}: #{e.message}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def each_resolved
|
|
135
|
+
map.each do |env_key, tf_output|
|
|
136
|
+
yield env_key, fetch_tf_output(tf_output), tf_output
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def fetch_tf_output(name)
|
|
141
|
+
return @tf_cache[name] if @tf_cache.key?(name)
|
|
142
|
+
|
|
143
|
+
@tf_cache[name] = read_tf_output(name)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def read_tf_output(name)
|
|
147
|
+
return nil unless Dir.exist?(@env_dir)
|
|
148
|
+
|
|
149
|
+
output, status = Open3.capture2(
|
|
150
|
+
'terraform', 'output', '-raw', name,
|
|
151
|
+
chdir: @env_dir,
|
|
152
|
+
err: File::NULL
|
|
153
|
+
)
|
|
154
|
+
return nil unless status.success?
|
|
155
|
+
|
|
156
|
+
value = output.strip
|
|
157
|
+
return nil if value.empty? || value == 'null'
|
|
158
|
+
|
|
159
|
+
value
|
|
160
|
+
rescue Errno::ENOENT
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Match KEY=value lines; ignore comments and export prefixes.
|
|
165
|
+
def dotenv_key(line)
|
|
166
|
+
return nil if line.strip.empty? || line.lstrip.start_with?('#')
|
|
167
|
+
|
|
168
|
+
stripped = line.strip
|
|
169
|
+
stripped = stripped.sub(/\Aexport\s+/, '')
|
|
170
|
+
return nil unless stripped =~ /\A([A-Za-z_][A-Za-z0-9_]*)=/
|
|
171
|
+
|
|
172
|
+
Regexp.last_match(1)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def format_dotenv_line(key, value)
|
|
176
|
+
"#{key}=#{escape_dotenv_value(value)}"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def escape_dotenv_value(value)
|
|
180
|
+
str = value.to_s
|
|
181
|
+
return str if str.empty?
|
|
182
|
+
return str unless str.match?(/[\s\#"'\\$`]/)
|
|
183
|
+
|
|
184
|
+
%("#{str.gsub('\\', '\\\\').gsub('"', '\\"')}")
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -14,7 +14,11 @@ module Belt
|
|
|
14
14
|
non_param = segments.reject { |s| s.start_with?(':', '{') }
|
|
15
15
|
return gateway.name if non_param.empty?
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
# Nested resources (/posts/{id}/comments) and scoped resources (/admin/users):
|
|
18
|
+
# join non-param segments → posts/comments, admin/users
|
|
19
|
+
if route.resource? && non_param.length > 1
|
|
20
|
+
return non_param.map { |s| s.gsub('-', '_') }.join('/')
|
|
21
|
+
end
|
|
18
22
|
|
|
19
23
|
# For non-resource routes with a single segment (e.g., post '/signup' in :onboarding),
|
|
20
24
|
# the segment is the action name, not the controller. Use the gateway name as controller.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'base64'
|
|
4
4
|
require 'json'
|
|
5
5
|
require_relative 'app_detection'
|
|
6
|
+
require_relative 'frontend_env_map'
|
|
6
7
|
require_relative 'terraform_command'
|
|
7
8
|
|
|
8
9
|
module Belt
|
|
@@ -50,13 +51,14 @@ module Belt
|
|
|
50
51
|
|
|
51
52
|
Behavior:
|
|
52
53
|
• If frontend/ exists → runs the frontend dev server (npm run dev)
|
|
53
|
-
|
|
54
|
+
Injects env from frontend/env.yml (or default VITE_API_URL) using
|
|
55
|
+
terraform outputs when available.
|
|
54
56
|
• If no frontend → serves the welcome page via a local HTTP server
|
|
55
57
|
After deploy, shows live API URL and deployment status.
|
|
56
58
|
|
|
57
59
|
Note: The backend is serverless (AWS Lambda). Use `belt deploy` to deploy
|
|
58
|
-
your backend to AWS. Local frontend development
|
|
59
|
-
|
|
60
|
+
your backend to AWS. Local frontend development reads the env map (or
|
|
61
|
+
frontend/.env via `belt frontend env <env>`).
|
|
60
62
|
|
|
61
63
|
Examples:
|
|
62
64
|
belt server # Start on port #{DEFAULT_PORT}
|
|
@@ -84,17 +86,22 @@ module Belt
|
|
|
84
86
|
|
|
85
87
|
def run_frontend_dev_server
|
|
86
88
|
puts "🚀 Starting frontend dev server on port #{@port}..."
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
build_env = frontend_process_env
|
|
90
|
+
api_url = build_env['VITE_API_URL'] || build_env['REACT_APP_API_URL'] ||
|
|
91
|
+
build_env['NEXT_PUBLIC_API_URL'] || @api_url
|
|
92
|
+
if api_url
|
|
93
|
+
puts " Backend API: #{api_url}"
|
|
89
94
|
else
|
|
90
95
|
puts ' Backend is serverless — deploy with `belt deploy` to set up AWS resources.'
|
|
91
96
|
end
|
|
97
|
+
if build_env.any?
|
|
98
|
+
puts " Env: #{build_env.keys.sort.join(', ')}"
|
|
99
|
+
end
|
|
92
100
|
puts ''
|
|
93
101
|
|
|
94
102
|
open_browser_later if @open_browser
|
|
95
103
|
|
|
96
|
-
env = { 'PORT' => @port.to_s }
|
|
97
|
-
env['VITE_API_URL'] = @api_url if @api_url
|
|
104
|
+
env = { 'PORT' => @port.to_s }.merge(build_env)
|
|
98
105
|
|
|
99
106
|
# Prefer the dev script with the port flag for Vite-based setups
|
|
100
107
|
Dir.chdir('frontend') do
|
|
@@ -102,6 +109,17 @@ module Belt
|
|
|
102
109
|
end
|
|
103
110
|
end
|
|
104
111
|
|
|
112
|
+
# Process env from the declarative map (or default VITE_API_URL), if an env is available.
|
|
113
|
+
def frontend_process_env
|
|
114
|
+
env_name = @deploy_env || ENV.fetch('BELT_ENV', nil) || TerraformCommand.list_environments.first
|
|
115
|
+
return {} unless env_name
|
|
116
|
+
|
|
117
|
+
FrontendEnvMap.new(env_name).process_env
|
|
118
|
+
rescue StandardError
|
|
119
|
+
# Fall back to legacy api_url detection if map resolution fails
|
|
120
|
+
@api_url ? { 'VITE_API_URL' => @api_url } : {}
|
|
121
|
+
end
|
|
122
|
+
|
|
105
123
|
def run_welcome_server
|
|
106
124
|
if @api_url
|
|
107
125
|
puts "🚀 Serving Belt welcome page on http://localhost:#{@port}"
|
data/lib/belt/cli.rb
CHANGED
|
@@ -9,6 +9,7 @@ require_relative 'cli/destroy_command'
|
|
|
9
9
|
require_relative 'cli/frontend_command'
|
|
10
10
|
require_relative 'cli/frontend_setup_command'
|
|
11
11
|
require_relative 'cli/frontend_deploy_command'
|
|
12
|
+
require_relative 'cli/frontend_env_command'
|
|
12
13
|
require_relative 'cli/views_command'
|
|
13
14
|
require_relative 'cli/setup_command'
|
|
14
15
|
require_relative 'cli/terraform_command'
|
|
@@ -29,10 +30,12 @@ module Belt
|
|
|
29
30
|
%w[tasks --tasks -T] => Belt::CLI::TasksCommand,
|
|
30
31
|
'setup' => Belt::CLI::SetupCommand,
|
|
31
32
|
'deploy' => Belt::CLI::DeployCommand,
|
|
33
|
+
'frontend' => Belt::CLI::FrontendEnvCommand,
|
|
32
34
|
%w[server s] => Belt::CLI::ServerCommand,
|
|
33
35
|
%w[version --version -v] => ->(_args) { puts "Belt #{Belt::VERSION}" }
|
|
34
36
|
}.freeze
|
|
35
37
|
|
|
38
|
+
|
|
36
39
|
COMMANDS = COMMANDS_DEFINITION.each_with_object({}) do |(keys, handler), hash|
|
|
37
40
|
Array(keys).each { |key| hash[key] = handler }
|
|
38
41
|
end.freeze
|
|
@@ -99,7 +102,9 @@ module Belt
|
|
|
99
102
|
s Alias for server
|
|
100
103
|
deploy [environment] Deploy to AWS (init → plan → apply)
|
|
101
104
|
deploy frontend <env> Build and deploy frontend to AWS
|
|
105
|
+
frontend env <env> Write frontend/.env from terraform outputs
|
|
102
106
|
routes [-g PATTERN] [-f json] Show route definitions
|
|
107
|
+
|
|
103
108
|
console Start an interactive console (IRB)
|
|
104
109
|
c Alias for console
|
|
105
110
|
tasks [-g PATTERN] [-a] List available rake tasks
|
|
@@ -133,6 +138,7 @@ module Belt
|
|
|
133
138
|
belt deploy # Deploy dev to AWS
|
|
134
139
|
belt deploy prod --auto # Deploy prod without confirmation
|
|
135
140
|
belt deploy frontend wups
|
|
141
|
+
belt frontend env wups # Smart-merge TF outputs into frontend/.env
|
|
136
142
|
belt setup frontend wups
|
|
137
143
|
belt apply wups
|
|
138
144
|
belt tasks # list all rake tasks
|
data/lib/belt/route_dsl.rb
CHANGED
|
@@ -174,20 +174,22 @@ module Belt
|
|
|
174
174
|
resource_name = name.to_s
|
|
175
175
|
singular = singularize(resource_name)
|
|
176
176
|
param_name = options[:param] || "#{singular}_id"
|
|
177
|
+
base_path = resource_base_path(resource_name, options)
|
|
178
|
+
options = options.except(:path_prefix)
|
|
177
179
|
options = auto_infer_tables(resource_name, options)
|
|
178
180
|
resource_options = options.merge(route_type: :resources)
|
|
179
181
|
actions = determine_actions(options)
|
|
180
182
|
|
|
181
|
-
add_route(:get,
|
|
182
|
-
add_route(:post,
|
|
183
|
-
add_route(:get, "
|
|
184
|
-
add_route(:put, "
|
|
185
|
-
add_route(:delete, "
|
|
183
|
+
add_route(:get, base_path, resource_options) if actions.include?(:index)
|
|
184
|
+
add_route(:post, base_path, resource_options) if actions.include?(:create)
|
|
185
|
+
add_route(:get, "#{base_path}/{#{param_name}}", resource_options) if actions.include?(:show)
|
|
186
|
+
add_route(:put, "#{base_path}/{#{param_name}}", resource_options) if actions.include?(:update)
|
|
187
|
+
add_route(:delete, "#{base_path}/{#{param_name}}", resource_options) if actions.include?(:destroy)
|
|
186
188
|
|
|
187
189
|
return unless block_given?
|
|
188
190
|
|
|
189
|
-
collection_prefix =
|
|
190
|
-
member_prefix = "
|
|
191
|
+
collection_prefix = base_path
|
|
192
|
+
member_prefix = "#{base_path}/{#{param_name}}"
|
|
191
193
|
resource_tables = Array(options[:tables] || [])
|
|
192
194
|
inherited_tables = (@default_tables + resource_tables).uniq
|
|
193
195
|
inherited_auth = options[:auth] || @default_auth
|
|
@@ -199,17 +201,25 @@ module Belt
|
|
|
199
201
|
|
|
200
202
|
def resource(name, options = {})
|
|
201
203
|
resource_name = name.to_s
|
|
204
|
+
base_path = resource_base_path(resource_name, options)
|
|
205
|
+
options = options.except(:path_prefix)
|
|
202
206
|
actions = determine_actions(options, default: %i[show update destroy])
|
|
203
207
|
resource_options = options.merge(route_type: :resource)
|
|
204
208
|
|
|
205
|
-
add_route(:get,
|
|
206
|
-
add_route(:put,
|
|
207
|
-
add_route(:delete,
|
|
208
|
-
add_route(:post,
|
|
209
|
+
add_route(:get, base_path, resource_options) if actions.include?(:show)
|
|
210
|
+
add_route(:put, base_path, resource_options) if actions.include?(:update)
|
|
211
|
+
add_route(:delete, base_path, resource_options) if actions.include?(:destroy)
|
|
212
|
+
add_route(:post, base_path, resource_options) if actions.include?(:create)
|
|
209
213
|
end
|
|
210
214
|
|
|
211
215
|
private
|
|
212
216
|
|
|
217
|
+
# Builds "/users" or "/admin/users" when path_prefix is set (from scope path:).
|
|
218
|
+
def resource_base_path(resource_name, options)
|
|
219
|
+
prefix = options[:path_prefix].to_s.gsub(%r{^/|/$}, '')
|
|
220
|
+
prefix.empty? ? "/#{resource_name}" : "/#{prefix}/#{resource_name}"
|
|
221
|
+
end
|
|
222
|
+
|
|
213
223
|
def add_route(method, path, options = {})
|
|
214
224
|
lambda_to_use = options[:lambda] || @current_lambda_context || @default_lambda
|
|
215
225
|
route_tables = Array(options[:tables] || [])
|
|
@@ -308,7 +318,11 @@ module Belt
|
|
|
308
318
|
previous_tables = @scope_tables
|
|
309
319
|
previous_controller = @scope_controller
|
|
310
320
|
|
|
311
|
-
|
|
321
|
+
# Nest path segments (Rails-style): scope path: "a" { scope path: "b" } → "a/b"
|
|
322
|
+
if options.key?(:path)
|
|
323
|
+
segment = options[:path].to_s.gsub(%r{^/|/$}, '')
|
|
324
|
+
@scope_prefix = @scope_prefix.to_s.empty? ? segment : "#{@scope_prefix}/#{segment}"
|
|
325
|
+
end
|
|
312
326
|
@scope_module = options[:module] || @scope_module
|
|
313
327
|
@scope_auth = options[:auth] || @scope_auth
|
|
314
328
|
@scope_tables = (@scope_tables + Array(options[:tables] || [])).uniq
|
|
@@ -340,11 +354,13 @@ module Belt
|
|
|
340
354
|
|
|
341
355
|
def resources(name, options = {}, &)
|
|
342
356
|
options = apply_scope_options(options)
|
|
357
|
+
options = options.merge(path_prefix: @scope_prefix) unless @scope_prefix.to_s.empty?
|
|
343
358
|
@gateway.resources(name, options, &)
|
|
344
359
|
end
|
|
345
360
|
|
|
346
361
|
def resource(name, options = {})
|
|
347
362
|
options = apply_scope_options(options)
|
|
363
|
+
options = options.merge(path_prefix: @scope_prefix) unless @scope_prefix.to_s.empty?
|
|
348
364
|
@gateway.resource(name, options)
|
|
349
365
|
end
|
|
350
366
|
|
|
@@ -400,6 +416,7 @@ module Belt
|
|
|
400
416
|
result = options.dup
|
|
401
417
|
result[:auth] ||= @scope_auth if @scope_auth
|
|
402
418
|
result[:lambda] ||= @scope_module if @scope_module
|
|
419
|
+
result[:controller] ||= @scope_controller if @scope_controller
|
|
403
420
|
result[:tables] = (@scope_tables + Array(result[:tables] || [])).uniq if @scope_tables.any? || result[:tables]
|
|
404
421
|
result
|
|
405
422
|
end
|
data/lib/belt/version.rb
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Declarative frontend env map for belt.
|
|
2
|
+
# Left: env var your build tool reads. Right: terraform output name.
|
|
3
|
+
# Copy to frontend/env.yml (or .belt/frontend_env.yml) and adjust.
|
|
4
|
+
#
|
|
5
|
+
# belt deploy frontend <env> → injects these into npm run build
|
|
6
|
+
# belt frontend env <env> → smart-merges into frontend/.env for local dev
|
|
7
|
+
#
|
|
8
|
+
# Without this file, belt only sets VITE_API_URL from api_url.
|
|
9
|
+
|
|
10
|
+
VITE_API_URL: api_url
|
|
11
|
+
# VITE_COGNITO_USER_POOL_ID: cognito_user_pool_id
|
|
12
|
+
# VITE_COGNITO_CLIENT_ID: cognito_user_pool_client_id
|
|
13
|
+
# VITE_AWS_REGION: cognito_region
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: belt
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stowzilla
|
|
@@ -78,6 +78,8 @@ files:
|
|
|
78
78
|
- lib/belt/cli/environment_command.rb
|
|
79
79
|
- lib/belt/cli/frontend_command.rb
|
|
80
80
|
- lib/belt/cli/frontend_deploy_command.rb
|
|
81
|
+
- lib/belt/cli/frontend_env_command.rb
|
|
82
|
+
- lib/belt/cli/frontend_env_map.rb
|
|
81
83
|
- lib/belt/cli/frontend_setup_command.rb
|
|
82
84
|
- lib/belt/cli/generate_command.rb
|
|
83
85
|
- lib/belt/cli/new_command.rb
|
|
@@ -110,6 +112,7 @@ files:
|
|
|
110
112
|
- lib/templates/environment/outputs.tf.erb
|
|
111
113
|
- lib/templates/environment/terraform.tfvars.erb
|
|
112
114
|
- lib/templates/environment/variables.tf.erb
|
|
115
|
+
- lib/templates/frontend/react/env.yml.example
|
|
113
116
|
- lib/templates/frontend/react/index.html.erb
|
|
114
117
|
- lib/templates/frontend/react/package.json.erb
|
|
115
118
|
- lib/templates/frontend/react/src/App.jsx
|