belt 0.3.4 → 0.3.6
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 +52 -0
- data/lib/belt/cli/auth_command.rb +27 -12
- data/lib/belt/cli/deploy_command.rb +27 -14
- data/lib/belt/cli/destroy_command.rb +55 -24
- data/lib/belt/cli/environment_command.rb +12 -0
- data/lib/belt/cli/explain_command.rb +3 -1
- data/lib/belt/cli/frontend_command.rb +31 -10
- data/lib/belt/cli/frontend_deploy_command.rb +110 -21
- data/lib/belt/cli/frontend_env_command.rb +55 -18
- data/lib/belt/cli/frontend_env_map.rb +27 -9
- data/lib/belt/cli/frontend_registry.rb +373 -0
- data/lib/belt/cli/frontend_setup_command.rb +71 -5
- data/lib/belt/cli/generate_command.rb +21 -6
- data/lib/belt/cli/routes_command/request_model_inference.rb +131 -0
- data/lib/belt/cli/routes_command.rb +7 -0
- data/lib/belt/cli/server_command.rb +28 -15
- data/lib/belt/cli/terraform_command.rb +9 -0
- data/lib/belt/cli/views_command.rb +13 -7
- data/lib/belt/cli/zip_artifact_builder.rb +199 -0
- data/lib/belt/cli.rb +12 -8
- data/lib/belt/docs/deployment.md +27 -2
- data/lib/belt/docs/frontend.md +103 -0
- data/lib/belt/docs/generators.md +9 -0
- data/lib/belt/docs/structure.md +3 -0
- data/lib/belt/route_dsl.rb +113 -23
- data/lib/belt/version.rb +1 -1
- data/lib/templates/frontend/react/env.yml.example +1 -1
- data/lib/templates/module/frontend.tf.erb +49 -43
- data/lib/templates/new_app/AGENTS.md.erb +29 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: abed8bb1f75407be5e61c3725a880c52096ecbd62285910afcc640bff6be277d
|
|
4
|
+
data.tar.gz: cdb9b586a8ce7cdc9e6f41bc39cc77b80c5155e2bb8a19d5390e65bf5a86b70f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b34e9c0a0edfc2c8df99876af80e2f6bbf7848a5e63c842c4d7f39262165089c8b6a69182981e629b6f936859dbf062c5ab5d0d65ed0221f156e1aead3d26b44
|
|
7
|
+
data.tar.gz: 53c37765c40fac45c9770bdee1ae337f3a9f928656ece072b7c907da84b535e2c460ff0bfd824d106935e02792c3234cc60470738151556b2d247afa7fb5f731
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.3.6
|
|
6
|
+
|
|
7
|
+
### Enhancement
|
|
8
|
+
|
|
9
|
+
- **Per-action request_model**: `request_model` on `resources` now accepts a Hash
|
|
10
|
+
to specify different validation schemas per action:
|
|
11
|
+
```ruby
|
|
12
|
+
resources :items, request_model: { create: :create_item, update: :update_item }
|
|
13
|
+
```
|
|
14
|
+
Symbol style (uniform for all body verbs) still works unchanged.
|
|
15
|
+
|
|
16
|
+
- **Convention-based request_model inference**: When no explicit `request_model` is
|
|
17
|
+
set on a resource route, `belt routes` auto-discovers matching contracts from
|
|
18
|
+
`contracts.rb` using a naming cascade:
|
|
19
|
+
1. `:<verb>_<gateway>_<singular_resource>` (e.g. `:create_customer_item`)
|
|
20
|
+
2. `:<verb>_<singular_resource>` (e.g. `:create_item`)
|
|
21
|
+
|
|
22
|
+
Only applies to body-accepting verbs (POST/PUT/PATCH). Explicit `request_model:`
|
|
23
|
+
always takes priority. No matching contract = no validation (same as before).
|
|
24
|
+
|
|
25
|
+
- **Convention-based response_model inference**: When no explicit `response_model` is
|
|
26
|
+
set on a resource route, `belt routes` auto-discovers a matching response model
|
|
27
|
+
from `contracts.rb` using the singular resource name.
|
|
28
|
+
e.g. `resources :items` → looks for `model :item` → auto-wires `response_model: :item`.
|
|
29
|
+
Applies to all verbs. Explicit `response_model:` always takes priority.
|
|
30
|
+
|
|
31
|
+
## 0.3.5
|
|
32
|
+
|
|
33
|
+
### Enhancement
|
|
34
|
+
|
|
35
|
+
- **Multiple frontends**: Frontend directory is no longer hardcoded to `frontend/`.
|
|
36
|
+
Declare named SPAs in `config/frontends.yml` (path, dist dir, terraform output names).
|
|
37
|
+
`belt generate frontend react --name ops --path ops-app` scaffolds an additional app.
|
|
38
|
+
Generators, `belt server`, `belt frontend env`, and `belt deploy frontend` accept
|
|
39
|
+
`--frontend NAME`. `belt deploy frontend <env>` deploys all configured frontends;
|
|
40
|
+
pass `--frontend` to target one. Existing single-`frontend/` apps keep working
|
|
41
|
+
with no config file.
|
|
42
|
+
- **Sidecar Lambda zips**: `belt deploy` / `belt plan` / `belt apply` build zip
|
|
43
|
+
artifacts that Terraform `filebase64sha256` needs at plan time (e.g. Stowzilla's
|
|
44
|
+
Node `image-processor`). Node packages are installed in Docker on linux/amd64
|
|
45
|
+
so native addons match Lambda. Missing zips are built automatically; unchanged
|
|
46
|
+
sources skip the rebuild.
|
|
47
|
+
|
|
48
|
+
### Bug fix
|
|
49
|
+
|
|
50
|
+
- **Frontend CloudFront invalidation**: `belt deploy frontend` no longer prints
|
|
51
|
+
terraform's "output not found" error when a named frontend sets
|
|
52
|
+
`cloudfront_domain_output` instead of a distribution-id output. Missing
|
|
53
|
+
terraform outputs are read with stderr silenced.
|
|
54
|
+
|
|
3
55
|
## 0.3.4
|
|
4
56
|
|
|
5
57
|
### Enhancement
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
require 'erb'
|
|
5
5
|
require_relative 'app_detection'
|
|
6
|
+
require_relative 'frontend_registry'
|
|
6
7
|
|
|
7
8
|
module Belt
|
|
8
9
|
module CLI
|
|
@@ -20,13 +21,14 @@ module Belt
|
|
|
20
21
|
|
|
21
22
|
force = args.delete('--force') || args.delete('-f')
|
|
22
23
|
signup = args.delete('--signup')
|
|
24
|
+
frontend_name = FrontendRegistry.extract_flag!(args, '--frontend')
|
|
23
25
|
pools = parse_pools(args)
|
|
24
26
|
|
|
25
|
-
new(pools: pools, force: force, signup: signup).generate
|
|
27
|
+
new(pools: pools, force: force, signup: signup, frontend_name: frontend_name).generate
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
def self.destroy(_args)
|
|
29
|
-
new(pools: [], force: false).remove
|
|
31
|
+
new(pools: [], force: false, skip_frontend_resolve: true).remove
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
def self.print_help
|
|
@@ -36,8 +38,9 @@ module Belt
|
|
|
36
38
|
Usage: belt generate auth [pool_names...] [options]
|
|
37
39
|
|
|
38
40
|
Options:
|
|
39
|
-
--signup
|
|
40
|
-
--
|
|
41
|
+
--signup Allow public user registration (generates frontend views)
|
|
42
|
+
--frontend NAME Target frontend when several exist
|
|
43
|
+
--force, -f Overwrite existing cognito.tf (skip collision check)
|
|
41
44
|
|
|
42
45
|
Arguments:
|
|
43
46
|
pool_names Optional pool names for multiple user pools.
|
|
@@ -86,12 +89,13 @@ module Belt
|
|
|
86
89
|
names.map(&:downcase).map { |n| n.gsub(/[^a-z0-9_]/, '_') }
|
|
87
90
|
end
|
|
88
91
|
|
|
89
|
-
def initialize(pools:, force: false, signup: false)
|
|
92
|
+
def initialize(pools:, force: false, signup: false, frontend_name: nil, skip_frontend_resolve: false)
|
|
90
93
|
@pool_names = pools
|
|
91
94
|
@force = force
|
|
92
95
|
@signup = signup
|
|
93
96
|
@app_name = detect_app_name
|
|
94
97
|
@pools = build_pool_metadata
|
|
98
|
+
@frontend = skip_frontend_resolve ? nil : resolve_frontend(frontend_name)
|
|
95
99
|
end
|
|
96
100
|
|
|
97
101
|
def generate
|
|
@@ -117,7 +121,7 @@ module Belt
|
|
|
117
121
|
puts ' end'
|
|
118
122
|
puts ''
|
|
119
123
|
if frontend?
|
|
120
|
-
puts
|
|
124
|
+
puts " 2. Wire auth into your #{@frontend.src_dir}/App.jsx:"
|
|
121
125
|
puts ''
|
|
122
126
|
puts " import Login from './pages/auth/Login'"
|
|
123
127
|
puts " import ProtectedRoute from './components/ProtectedRoute'"
|
|
@@ -185,20 +189,30 @@ module Belt
|
|
|
185
189
|
end
|
|
186
190
|
end
|
|
187
191
|
|
|
192
|
+
def resolve_frontend(name)
|
|
193
|
+
registry = FrontendRegistry.new
|
|
194
|
+
return nil if registry.empty? && name.nil?
|
|
195
|
+
|
|
196
|
+
return registry.resolve!(name) if name
|
|
197
|
+
|
|
198
|
+
registry.default
|
|
199
|
+
end
|
|
200
|
+
|
|
188
201
|
def frontend?
|
|
189
|
-
Dir.exist?(
|
|
202
|
+
@frontend && Dir.exist?(@frontend.src_dir)
|
|
190
203
|
end
|
|
191
204
|
|
|
192
205
|
def generate_frontend_auth
|
|
193
206
|
frontend_template_dir = File.join(TEMPLATE_DIR, 'frontend')
|
|
207
|
+
src = @frontend.src_dir
|
|
194
208
|
|
|
195
209
|
# Generate auth lib files
|
|
196
|
-
lib_dir = '
|
|
210
|
+
lib_dir = File.join(src, 'lib')
|
|
197
211
|
FileUtils.mkdir_p(lib_dir)
|
|
198
212
|
copy_frontend_file(frontend_template_dir, 'auth.js', File.join(lib_dir, 'auth.js'))
|
|
199
213
|
copy_frontend_file(frontend_template_dir, 'apiClient.js', File.join(lib_dir, 'apiClient.js'))
|
|
200
214
|
|
|
201
|
-
pages_dir = '
|
|
215
|
+
pages_dir = File.join(src, 'pages/auth')
|
|
202
216
|
FileUtils.mkdir_p(pages_dir)
|
|
203
217
|
copy_frontend_file(frontend_template_dir, 'Login.jsx', File.join(pages_dir, 'Login.jsx'))
|
|
204
218
|
copy_frontend_file(frontend_template_dir, 'auth.css', File.join(pages_dir, 'auth.css'))
|
|
@@ -209,7 +223,7 @@ module Belt
|
|
|
209
223
|
end
|
|
210
224
|
|
|
211
225
|
# Generate ProtectedRoute component
|
|
212
|
-
components_dir = '
|
|
226
|
+
components_dir = File.join(src, 'components')
|
|
213
227
|
FileUtils.mkdir_p(components_dir)
|
|
214
228
|
copy_frontend_file(frontend_template_dir, 'ProtectedRoute.jsx',
|
|
215
229
|
File.join(components_dir, 'ProtectedRoute.jsx'))
|
|
@@ -226,11 +240,12 @@ module Belt
|
|
|
226
240
|
def install_cognito_sdk
|
|
227
241
|
puts "\n Installing @aws-sdk/client-cognito-identity-provider..."
|
|
228
242
|
success = system('npm', 'install', '@aws-sdk/client-cognito-identity-provider',
|
|
229
|
-
'--prefix',
|
|
243
|
+
'--prefix', @frontend.path, '--no-fund', '--no-audit', '--silent')
|
|
230
244
|
if success
|
|
231
245
|
puts ' ✓ npm dependency installed'
|
|
232
246
|
else
|
|
233
|
-
puts
|
|
247
|
+
puts " ⚠ npm install failed — run: cd #{@frontend.path} && " \
|
|
248
|
+
'npm install @aws-sdk/client-cognito-identity-provider'
|
|
234
249
|
end
|
|
235
250
|
end
|
|
236
251
|
|
|
@@ -10,6 +10,7 @@ require_relative 'backup_config'
|
|
|
10
10
|
require_relative 'backup_runner'
|
|
11
11
|
require_relative 'environment_config'
|
|
12
12
|
require_relative 'path_gem_materializer'
|
|
13
|
+
require_relative 'zip_artifact_builder'
|
|
13
14
|
|
|
14
15
|
module Belt
|
|
15
16
|
module CLI
|
|
@@ -66,7 +67,8 @@ module Belt
|
|
|
66
67
|
puts ' belt deploy prod # Deploy to prod'
|
|
67
68
|
puts ' belt deploy dev --auto # Deploy without confirmation'
|
|
68
69
|
puts ' belt deploy --rebuild # Fast code push (no infra changes)'
|
|
69
|
-
puts ' belt deploy frontend dev
|
|
70
|
+
puts ' belt deploy frontend dev # Deploy frontend(s)'
|
|
71
|
+
puts ' belt deploy frontend dev --frontend ops'
|
|
70
72
|
puts "\nNo environments found. Run `belt generate environment dev` first."
|
|
71
73
|
exit 1
|
|
72
74
|
end
|
|
@@ -92,10 +94,11 @@ module Belt
|
|
|
92
94
|
1. Ensure Gemfile.lock is consistent (fix stale PATH refs)
|
|
93
95
|
2. Regenerate route manifests
|
|
94
96
|
3. Run pre-deploy backups (if configured in belt.rb)
|
|
95
|
-
4.
|
|
96
|
-
5. terraform
|
|
97
|
-
6.
|
|
98
|
-
7.
|
|
97
|
+
4. Build sidecar Lambda zips Terraform needs at plan time
|
|
98
|
+
5. terraform init (initialize providers/modules)
|
|
99
|
+
6. terraform plan (preview changes)
|
|
100
|
+
7. Prompt for confirmation (unless --auto)
|
|
101
|
+
8. terraform apply (deploy changes)
|
|
99
102
|
|
|
100
103
|
Options:
|
|
101
104
|
--auto, --yes, -y Skip confirmation prompt (auto-approve)
|
|
@@ -130,7 +133,8 @@ module Belt
|
|
|
130
133
|
belt deploy prod --rebuild # Fast code push to prod
|
|
131
134
|
belt deploy prod --skip-backup # Skip backups for this deploy
|
|
132
135
|
belt deploy prod --backup-only # Run backups without deploying
|
|
133
|
-
belt deploy frontend dev
|
|
136
|
+
belt deploy frontend dev # Deploy frontend asset(s)
|
|
137
|
+
belt deploy frontend dev --frontend ops
|
|
134
138
|
HELP
|
|
135
139
|
end
|
|
136
140
|
|
|
@@ -155,6 +159,7 @@ module Belt
|
|
|
155
159
|
warn_active_path_gems!
|
|
156
160
|
generate_routes_if_needed
|
|
157
161
|
run_backups unless @skip_backup
|
|
162
|
+
build_zip_artifacts!
|
|
158
163
|
|
|
159
164
|
Dir.chdir(env_dir) do
|
|
160
165
|
run_init
|
|
@@ -663,6 +668,10 @@ module Belt
|
|
|
663
668
|
puts ' (timed out waiting, but code was pushed)'
|
|
664
669
|
end
|
|
665
670
|
|
|
671
|
+
def build_zip_artifacts!
|
|
672
|
+
ZipArtifactBuilder.build!(project_root: @project_root, infra_dir: @infra_dir)
|
|
673
|
+
end
|
|
674
|
+
|
|
666
675
|
def run_init
|
|
667
676
|
puts '━━━ terraform init ━━━'
|
|
668
677
|
success = system('terraform', 'init')
|
|
@@ -706,15 +715,19 @@ module Belt
|
|
|
706
715
|
end
|
|
707
716
|
|
|
708
717
|
def deploy_frontend_if_exists
|
|
709
|
-
|
|
710
|
-
return unless Dir.exist?(frontend_dir) && File.exist?(File.join(frontend_dir, 'package.json'))
|
|
711
|
-
|
|
712
|
-
puts "\n━━━ frontend deploy ━━━"
|
|
718
|
+
require_relative 'frontend_registry'
|
|
713
719
|
require_relative 'frontend_deploy_command'
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
720
|
+
|
|
721
|
+
frontends = FrontendRegistry.new.existing
|
|
722
|
+
return if frontends.empty?
|
|
723
|
+
|
|
724
|
+
frontends.each do |frontend|
|
|
725
|
+
puts "\n━━━ #{frontend.label} deploy ━━━"
|
|
726
|
+
Belt::CLI::FrontendDeployCommand.new(@env, frontend: frontend).run
|
|
727
|
+
rescue StandardError => e
|
|
728
|
+
puts "\n ⚠ #{frontend.label.capitalize} deploy failed: #{e.message}"
|
|
729
|
+
puts " Run `belt deploy frontend #{@env} --frontend #{frontend.name}` manually to retry."
|
|
730
|
+
end
|
|
718
731
|
end
|
|
719
732
|
|
|
720
733
|
def print_outputs(env_dir)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
require_relative 'app_detection'
|
|
5
5
|
require_relative 'auth_command'
|
|
6
|
+
require_relative 'frontend_registry'
|
|
6
7
|
require_relative 'generator_registry'
|
|
7
8
|
require_relative 'tables_command'
|
|
8
9
|
require_relative '../inflector'
|
|
@@ -51,25 +52,28 @@ module Belt
|
|
|
51
52
|
flags = parse_environment_flags(args)
|
|
52
53
|
new(generator, name, [], **flags).destroy
|
|
53
54
|
when 'frontend'
|
|
54
|
-
|
|
55
|
+
frontend_name = FrontendRegistry.extract_flag!(args, '--frontend')
|
|
56
|
+
new(generator, nil, [], frontend_name: frontend_name).destroy
|
|
55
57
|
when 'auth'
|
|
56
58
|
Belt::CLI::AuthCommand.destroy(args)
|
|
57
59
|
when 'views'
|
|
60
|
+
frontend_name = FrontendRegistry.extract_flag!(args, '--frontend')
|
|
58
61
|
name = args.shift
|
|
59
62
|
if name.nil? || name.empty?
|
|
60
|
-
puts 'Usage: belt destroy views <name>'
|
|
63
|
+
puts 'Usage: belt destroy views <name> [--frontend NAME]'
|
|
61
64
|
exit 1
|
|
62
65
|
end
|
|
63
|
-
new(generator, name, args.map { |a| parse_field(a) }).destroy
|
|
66
|
+
new(generator, name, args.map { |a| parse_field(a) }, frontend_name: frontend_name).destroy
|
|
64
67
|
when 'index'
|
|
65
68
|
Belt::CLI::IndexCommand.run(['remove'] + args)
|
|
66
69
|
else
|
|
70
|
+
frontend_name = FrontendRegistry.extract_flag!(args, '--frontend')
|
|
67
71
|
name = args.shift
|
|
68
72
|
if name.nil? || name.empty?
|
|
69
73
|
puts "Usage: belt destroy #{generator} <name>"
|
|
70
74
|
exit 1
|
|
71
75
|
end
|
|
72
|
-
new(generator, name, args.map { |a| parse_field(a) }).destroy
|
|
76
|
+
new(generator, name, args.map { |a| parse_field(a) }, frontend_name: frontend_name).destroy
|
|
73
77
|
end
|
|
74
78
|
end
|
|
75
79
|
|
|
@@ -105,7 +109,7 @@ module Belt
|
|
|
105
109
|
controller Remove a controller
|
|
106
110
|
auth Remove Cognito user pool infrastructure
|
|
107
111
|
environment Remove a deployment environment and tear down infrastructure
|
|
108
|
-
frontend Remove
|
|
112
|
+
frontend Remove a frontend directory (use --frontend NAME when several exist)
|
|
109
113
|
views Remove React pages for a resource
|
|
110
114
|
|
|
111
115
|
Environment options:
|
|
@@ -120,7 +124,9 @@ module Belt
|
|
|
120
124
|
belt d environment dev --skip-terraform
|
|
121
125
|
belt d environment dev --force
|
|
122
126
|
belt d frontend
|
|
127
|
+
belt d frontend --frontend ops
|
|
123
128
|
belt d views post
|
|
129
|
+
belt d views post --frontend ops
|
|
124
130
|
|
|
125
131
|
⚠ This is destructive. Files will be permanently deleted.
|
|
126
132
|
|
|
@@ -129,12 +135,14 @@ module Belt
|
|
|
129
135
|
HELP
|
|
130
136
|
end
|
|
131
137
|
|
|
132
|
-
|
|
138
|
+
# rubocop:disable Metrics/ParameterLists -- keyword options for destroy flags
|
|
139
|
+
def initialize(generator, name, fields, force: false, skip_terraform: false, frontend_name: nil)
|
|
133
140
|
@generator = generator
|
|
134
141
|
@name = name&.downcase&.gsub(/[^a-z0-9_]/, '_')
|
|
135
142
|
@fields = fields
|
|
136
143
|
@force = force
|
|
137
144
|
@skip_terraform = skip_terraform
|
|
145
|
+
@frontend_name = frontend_name
|
|
138
146
|
@app_name = detect_namespace
|
|
139
147
|
@singular_name = @name ? Belt::Inflector.singularize(@name) : nil
|
|
140
148
|
@resource_name = @singular_name ? Belt::Inflector.pluralize(@singular_name) : nil
|
|
@@ -142,6 +150,7 @@ module Belt
|
|
|
142
150
|
@removed = []
|
|
143
151
|
@updated = []
|
|
144
152
|
end
|
|
153
|
+
# rubocop:enable Metrics/ParameterLists
|
|
145
154
|
|
|
146
155
|
def destroy
|
|
147
156
|
case @generator
|
|
@@ -350,39 +359,62 @@ module Belt
|
|
|
350
359
|
end
|
|
351
360
|
|
|
352
361
|
def destroy_frontend
|
|
353
|
-
|
|
362
|
+
frontend = FrontendRegistry.new.resolve!(@frontend_name)
|
|
363
|
+
dir = frontend.path
|
|
354
364
|
if Dir.exist?(dir)
|
|
355
365
|
FileUtils.rm_rf(dir)
|
|
356
366
|
@removed << dir
|
|
357
367
|
puts " remove #{dir}/"
|
|
358
368
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
puts " remove #{module_frontend}"
|
|
369
|
+
tf_file = File.join('infrastructure/modules/app', "#{frontend.tf_name}.tf")
|
|
370
|
+
if File.exist?(tf_file)
|
|
371
|
+
FileUtils.rm_f(tf_file)
|
|
372
|
+
@removed << tf_file
|
|
373
|
+
puts " remove #{tf_file}"
|
|
365
374
|
end
|
|
366
375
|
|
|
367
|
-
puts "\n✓
|
|
376
|
+
puts "\n✓ #{frontend.label.capitalize} removed!"
|
|
368
377
|
else
|
|
369
|
-
puts
|
|
378
|
+
puts "✗ No #{dir}/ directory found."
|
|
370
379
|
exit 1
|
|
371
380
|
end
|
|
372
381
|
end
|
|
373
382
|
|
|
383
|
+
def target_frontends
|
|
384
|
+
registry = FrontendRegistry.new
|
|
385
|
+
return [] if registry.empty?
|
|
386
|
+
|
|
387
|
+
if @frontend_name
|
|
388
|
+
[registry.resolve!(@frontend_name)]
|
|
389
|
+
else
|
|
390
|
+
registry.all
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
374
394
|
def destroy_views
|
|
375
395
|
return unless @resource_name
|
|
376
396
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
397
|
+
targets = target_frontends
|
|
398
|
+
if targets.empty?
|
|
399
|
+
pages_dir = "frontend/src/pages/#{@resource_name}"
|
|
400
|
+
if Dir.exist?(pages_dir)
|
|
401
|
+
FileUtils.rm_rf(pages_dir)
|
|
402
|
+
@removed << pages_dir
|
|
403
|
+
puts " remove #{pages_dir}/"
|
|
404
|
+
end
|
|
405
|
+
remove_view_routes_from('frontend/src/App.jsx')
|
|
406
|
+
return
|
|
383
407
|
end
|
|
384
408
|
|
|
385
|
-
|
|
409
|
+
targets.each do |frontend|
|
|
410
|
+
pages_dir = "#{frontend.src_dir}/pages/#{@resource_name}"
|
|
411
|
+
if Dir.exist?(pages_dir)
|
|
412
|
+
FileUtils.rm_rf(pages_dir)
|
|
413
|
+
@removed << pages_dir
|
|
414
|
+
puts " remove #{pages_dir}/"
|
|
415
|
+
end
|
|
416
|
+
remove_view_routes_from(frontend.app_jsx)
|
|
417
|
+
end
|
|
386
418
|
end
|
|
387
419
|
|
|
388
420
|
def remove_file(path)
|
|
@@ -467,8 +499,7 @@ module Belt
|
|
|
467
499
|
Belt::CLI::TablesCommand.sync_all_environments
|
|
468
500
|
end
|
|
469
501
|
|
|
470
|
-
def
|
|
471
|
-
app_jsx = 'frontend/src/App.jsx'
|
|
502
|
+
def remove_view_routes_from(app_jsx)
|
|
472
503
|
return unless File.exist?(app_jsx)
|
|
473
504
|
|
|
474
505
|
content = File.read(app_jsx)
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
require 'erb'
|
|
5
5
|
require_relative 'app_detection'
|
|
6
|
+
require_relative 'frontend_registry'
|
|
7
|
+
require_relative 'frontend_setup_command'
|
|
6
8
|
|
|
7
9
|
module Belt
|
|
8
10
|
module CLI
|
|
@@ -52,6 +54,8 @@ module Belt
|
|
|
52
54
|
puts " create #{dest_path}" unless @quiet
|
|
53
55
|
end
|
|
54
56
|
|
|
57
|
+
append_extra_frontend_outputs(File.join(dest_dir, 'outputs.tf'))
|
|
58
|
+
|
|
55
59
|
return if @quiet || !@announce
|
|
56
60
|
|
|
57
61
|
puts "\n✓ Environment '#{@env_name}' created!"
|
|
@@ -82,6 +86,14 @@ module Belt
|
|
|
82
86
|
File.write(dest_path, content)
|
|
83
87
|
end
|
|
84
88
|
|
|
89
|
+
def append_extra_frontend_outputs(outputs_file)
|
|
90
|
+
FrontendRegistry.new.all.each do |frontend|
|
|
91
|
+
next if frontend.tf_name == 'frontend'
|
|
92
|
+
|
|
93
|
+
FrontendSetupCommand.append_env_outputs_for(frontend, outputs_file)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
85
97
|
# Resolve the state bucket name to use in backend.tf.
|
|
86
98
|
# Priority: existing sibling backend.tf → AWS account ID → bare placeholder.
|
|
87
99
|
def resolve_state_bucket
|
|
@@ -5,6 +5,7 @@ require 'erb'
|
|
|
5
5
|
require 'json'
|
|
6
6
|
require 'open3'
|
|
7
7
|
require_relative 'app_detection'
|
|
8
|
+
require_relative 'frontend_registry'
|
|
8
9
|
|
|
9
10
|
module Belt
|
|
10
11
|
module CLI
|
|
@@ -15,33 +16,39 @@ module Belt
|
|
|
15
16
|
include AppDetection
|
|
16
17
|
|
|
17
18
|
def self.run(args)
|
|
19
|
+
name = FrontendRegistry.extract_flag!(args, '--name')
|
|
20
|
+
path = FrontendRegistry.extract_flag!(args, '--path')
|
|
18
21
|
framework = args.shift
|
|
19
22
|
|
|
20
23
|
if framework.nil? || !FRAMEWORKS.include?(framework)
|
|
21
|
-
puts "Usage: belt generate frontend <#{FRAMEWORKS.join('|')}>"
|
|
24
|
+
puts "Usage: belt generate frontend <#{FRAMEWORKS.join('|')}> [--name NAME] [--path DIR]"
|
|
22
25
|
puts "\nScaffolds a frontend application with build tooling and API client."
|
|
23
26
|
puts "\nExamples:"
|
|
24
27
|
puts ' belt generate frontend react'
|
|
25
28
|
puts ' belt generate frontend vue'
|
|
29
|
+
puts ' belt generate frontend react --name ops --path ops-app'
|
|
26
30
|
exit 1
|
|
27
31
|
end
|
|
28
32
|
|
|
29
|
-
new(framework).generate
|
|
33
|
+
new(framework, name: name, path: path).generate
|
|
30
34
|
end
|
|
31
35
|
|
|
32
|
-
def initialize(framework, quiet: false, announce: true)
|
|
36
|
+
def initialize(framework, quiet: false, announce: true, name: nil, path: nil)
|
|
33
37
|
@framework = framework
|
|
34
38
|
@quiet = quiet
|
|
35
39
|
@announce = announce
|
|
36
40
|
@app_name = detect_app_name
|
|
37
41
|
@module_name = @app_name.split(/[-_]/).map(&:capitalize).join
|
|
42
|
+
@name = name || (path ? File.basename(path) : 'frontend')
|
|
43
|
+
@path = path || name || 'frontend'
|
|
44
|
+
@frontend = Frontend.new(name: @name, path: @path, default: @name == 'frontend' && @path == 'frontend')
|
|
38
45
|
end
|
|
39
46
|
|
|
40
47
|
def generate
|
|
41
|
-
dest_dir =
|
|
48
|
+
dest_dir = @frontend.path
|
|
42
49
|
|
|
43
50
|
if Dir.exist?(dest_dir) && !Dir.empty?(dest_dir)
|
|
44
|
-
puts "Directory '
|
|
51
|
+
puts "Directory '#{dest_dir}/' already exists and is not empty."
|
|
45
52
|
exit 1
|
|
46
53
|
end
|
|
47
54
|
|
|
@@ -54,8 +61,9 @@ module Belt
|
|
|
54
61
|
end
|
|
55
62
|
|
|
56
63
|
copy_template(framework_dir, dest_dir)
|
|
64
|
+
register_frontend!
|
|
57
65
|
|
|
58
|
-
puts "\n✓ Frontend (#{@framework}) created in
|
|
66
|
+
puts "\n✓ Frontend (#{@framework}) created in #{dest_dir}/" unless @quiet
|
|
59
67
|
|
|
60
68
|
install_dependencies(dest_dir)
|
|
61
69
|
setup_frontend_infra_for_existing_environments
|
|
@@ -64,7 +72,11 @@ module Belt
|
|
|
64
72
|
return if @quiet || !@announce
|
|
65
73
|
|
|
66
74
|
puts "\nNext steps:"
|
|
67
|
-
|
|
75
|
+
if @frontend.name == 'frontend'
|
|
76
|
+
puts ' belt server # Start local dev server'
|
|
77
|
+
else
|
|
78
|
+
puts " belt server --frontend #{@frontend.name}"
|
|
79
|
+
end
|
|
68
80
|
puts ' belt deploy # Deploy everything to AWS'
|
|
69
81
|
end
|
|
70
82
|
|
|
@@ -88,9 +100,18 @@ module Belt
|
|
|
88
100
|
end
|
|
89
101
|
end
|
|
90
102
|
|
|
103
|
+
def register_frontend!
|
|
104
|
+
custom = @frontend.name != 'frontend' || @frontend.path != 'frontend'
|
|
105
|
+
config_exists = FrontendRegistry.find_config_path
|
|
106
|
+
return unless custom || config_exists
|
|
107
|
+
|
|
108
|
+
dest = FrontendRegistry.register!(name: @frontend.name, path: @frontend.path)
|
|
109
|
+
puts " update #{dest} (#{@frontend.name})" unless @quiet
|
|
110
|
+
end
|
|
111
|
+
|
|
91
112
|
def setup_frontend_infra_for_existing_environments
|
|
92
113
|
module_dir = 'infrastructure/modules/app'
|
|
93
|
-
frontend_tf = File.join(module_dir, 'frontend.tf
|
|
114
|
+
frontend_tf = File.join(module_dir, "#{@frontend.tf_name == 'frontend' ? 'frontend' : @frontend.tf_name}.tf")
|
|
94
115
|
|
|
95
116
|
if File.exist?(frontend_tf)
|
|
96
117
|
puts " skip #{frontend_tf} (already exists)" unless @quiet
|
|
@@ -101,7 +122,7 @@ module Belt
|
|
|
101
122
|
|
|
102
123
|
puts "\n Setting up frontend infrastructure..." unless @quiet
|
|
103
124
|
require_relative 'frontend_setup_command'
|
|
104
|
-
FrontendSetupCommand.new(nil, quiet: true).run
|
|
125
|
+
FrontendSetupCommand.new(nil, quiet: true, frontend: @frontend).run
|
|
105
126
|
puts " create #{frontend_tf}" unless @quiet
|
|
106
127
|
end
|
|
107
128
|
|
|
@@ -118,7 +139,7 @@ module Belt
|
|
|
118
139
|
require_relative 'views_command'
|
|
119
140
|
resources.each do |resource_name|
|
|
120
141
|
fields = ViewsCommand.read_schema_fields(resource_name)
|
|
121
|
-
ViewsCommand.new(resource_name, fields, force: true).generate
|
|
142
|
+
ViewsCommand.new(resource_name, fields, force: true, frontend: @frontend).generate
|
|
122
143
|
end
|
|
123
144
|
end
|
|
124
145
|
|