brut 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/CODE_OF_CONDUCT.txt +99 -0
- data/Dockerfile.dx +32 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +133 -0
- data/LICENSE.txt +370 -0
- data/README.md +21 -0
- data/Rakefile +1 -0
- data/bin/bin_kit.rb +39 -0
- data/bin/rake +27 -0
- data/bin/setup +145 -0
- data/brut.gemspec +60 -0
- data/docker-compose.dx.yml +16 -0
- data/dx/build +26 -0
- data/dx/docker-compose.env +22 -0
- data/dx/dx.sh.lib +24 -0
- data/dx/exec +58 -0
- data/dx/prune +19 -0
- data/dx/setupkit.sh.lib +144 -0
- data/dx/show-help-in-app-container-then-wait.sh +38 -0
- data/dx/start +30 -0
- data/dx/stop +23 -0
- data/lib/brut/back_end/action.rb +3 -0
- data/lib/brut/back_end/result.rb +46 -0
- data/lib/brut/back_end/seed_data.rb +24 -0
- data/lib/brut/back_end/validator.rb +3 -0
- data/lib/brut/back_end/validators/form_validator.rb +37 -0
- data/lib/brut/cli/app.rb +130 -0
- data/lib/brut/cli/app_runner.rb +219 -0
- data/lib/brut/cli/apps/build_assets.rb +123 -0
- data/lib/brut/cli/apps/db.rb +279 -0
- data/lib/brut/cli/apps/scaffold.rb +256 -0
- data/lib/brut/cli/apps/test.rb +200 -0
- data/lib/brut/cli/command.rb +130 -0
- data/lib/brut/cli/error.rb +12 -0
- data/lib/brut/cli/execution_results.rb +81 -0
- data/lib/brut/cli/executor.rb +37 -0
- data/lib/brut/cli/options.rb +46 -0
- data/lib/brut/cli/output.rb +30 -0
- data/lib/brut/cli.rb +24 -0
- data/lib/brut/factory_bot.rb +20 -0
- data/lib/brut/framework/app.rb +55 -0
- data/lib/brut/framework/config.rb +415 -0
- data/lib/brut/framework/container.rb +190 -0
- data/lib/brut/framework/errors/abstract_method.rb +9 -0
- data/lib/brut/framework/errors/bug.rb +14 -0
- data/lib/brut/framework/errors/not_found.rb +10 -0
- data/lib/brut/framework/errors.rb +14 -0
- data/lib/brut/framework/fussy_type_enforcement.rb +50 -0
- data/lib/brut/framework/mcp.rb +215 -0
- data/lib/brut/framework/project_environment.rb +18 -0
- data/lib/brut/framework.rb +13 -0
- data/lib/brut/front_end/asset_metadata.rb +76 -0
- data/lib/brut/front_end/component.rb +213 -0
- data/lib/brut/front_end/components/form_tag.rb +71 -0
- data/lib/brut/front_end/components/i18n_translations.rb +36 -0
- data/lib/brut/front_end/components/input.rb +13 -0
- data/lib/brut/front_end/components/inputs/csrf_token.rb +8 -0
- data/lib/brut/front_end/components/inputs/select.rb +100 -0
- data/lib/brut/front_end/components/inputs/text_field.rb +63 -0
- data/lib/brut/front_end/components/inputs/textarea.rb +51 -0
- data/lib/brut/front_end/components/locale_detection.rb +25 -0
- data/lib/brut/front_end/components/page_identifier.rb +13 -0
- data/lib/brut/front_end/components/timestamp.rb +33 -0
- data/lib/brut/front_end/download.rb +23 -0
- data/lib/brut/front_end/flash.rb +57 -0
- data/lib/brut/front_end/form.rb +171 -0
- data/lib/brut/front_end/forms/constraint_violation.rb +39 -0
- data/lib/brut/front_end/forms/input.rb +119 -0
- data/lib/brut/front_end/forms/input_definition.rb +100 -0
- data/lib/brut/front_end/forms/validity_state.rb +36 -0
- data/lib/brut/front_end/handler.rb +48 -0
- data/lib/brut/front_end/handlers/csp_reporting_handler.rb +11 -0
- data/lib/brut/front_end/handlers/locale_detection_handler.rb +22 -0
- data/lib/brut/front_end/handling_results.rb +14 -0
- data/lib/brut/front_end/http_method.rb +33 -0
- data/lib/brut/front_end/http_status.rb +16 -0
- data/lib/brut/front_end/middleware.rb +7 -0
- data/lib/brut/front_end/middlewares/reload_app.rb +31 -0
- data/lib/brut/front_end/page.rb +47 -0
- data/lib/brut/front_end/request_context.rb +82 -0
- data/lib/brut/front_end/route_hook.rb +15 -0
- data/lib/brut/front_end/route_hooks/age_flash.rb +8 -0
- data/lib/brut/front_end/route_hooks/csp_no_inline_scripts.rb +17 -0
- data/lib/brut/front_end/route_hooks/csp_no_inline_styles_or_scripts.rb +46 -0
- data/lib/brut/front_end/route_hooks/locale_detection.rb +24 -0
- data/lib/brut/front_end/route_hooks/setup_request_context.rb +11 -0
- data/lib/brut/front_end/routing.rb +236 -0
- data/lib/brut/front_end/session.rb +56 -0
- data/lib/brut/front_end/template.rb +32 -0
- data/lib/brut/front_end/templates/block_filter.rb +60 -0
- data/lib/brut/front_end/templates/erb_engine.rb +26 -0
- data/lib/brut/front_end/templates/erb_parser.rb +84 -0
- data/lib/brut/front_end/templates/escapable_filter.rb +18 -0
- data/lib/brut/front_end/templates/html_safe_string.rb +40 -0
- data/lib/brut/i18n/base_methods.rb +168 -0
- data/lib/brut/i18n/for_cli.rb +4 -0
- data/lib/brut/i18n/for_html.rb +4 -0
- data/lib/brut/i18n/http_accept_language.rb +68 -0
- data/lib/brut/i18n.rb +6 -0
- data/lib/brut/instrumentation/basic.rb +66 -0
- data/lib/brut/instrumentation/event.rb +19 -0
- data/lib/brut/instrumentation/http_event.rb +5 -0
- data/lib/brut/instrumentation/subscriber.rb +41 -0
- data/lib/brut/instrumentation.rb +11 -0
- data/lib/brut/junk_drawer.rb +88 -0
- data/lib/brut/sinatra_helpers.rb +183 -0
- data/lib/brut/spec_support/component_support.rb +49 -0
- data/lib/brut/spec_support/flash_support.rb +7 -0
- data/lib/brut/spec_support/general_support.rb +18 -0
- data/lib/brut/spec_support/handler_support.rb +7 -0
- data/lib/brut/spec_support/matcher.rb +9 -0
- data/lib/brut/spec_support/matchers/be_a_bug.rb +14 -0
- data/lib/brut/spec_support/matchers/be_page_for.rb +14 -0
- data/lib/brut/spec_support/matchers/be_routing_for.rb +11 -0
- data/lib/brut/spec_support/matchers/have_constraint_violation.rb +56 -0
- data/lib/brut/spec_support/matchers/have_html_attribute.rb +69 -0
- data/lib/brut/spec_support/matchers/have_rendered.rb +20 -0
- data/lib/brut/spec_support/matchers/have_returned_http_status.rb +27 -0
- data/lib/brut/spec_support/session_support.rb +3 -0
- data/lib/brut/spec_support.rb +12 -0
- data/lib/brut/version.rb +3 -0
- data/lib/brut.rb +38 -0
- data/lib/sequel/extensions/brut_instrumentation.rb +37 -0
- data/lib/sequel/extensions/brut_migrations.rb +98 -0
- data/lib/sequel/plugins/created_at.rb +14 -0
- data/lib/sequel/plugins/external_id.rb +45 -0
- data/lib/sequel/plugins/find_bang.rb +13 -0
- data/lib/sequel/plugins.rb +3 -0
- metadata +484 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
# The result of back-end processing, which is essentially
|
2
|
+
# a container for constraint violations and arbitrary context.
|
3
|
+
class Brut::BackEnd::Result
|
4
|
+
attr_reader :constraint_violations, :context
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@constraint_violations = {}
|
8
|
+
@context = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def constraint_violation!(object:,
|
12
|
+
field:,
|
13
|
+
key:,
|
14
|
+
context: {})
|
15
|
+
@constraint_violations[object] ||= {}
|
16
|
+
@constraint_violations[object][field] ||= {}
|
17
|
+
@constraint_violations[object][field][key] = context
|
18
|
+
end
|
19
|
+
|
20
|
+
def each_violation(&block)
|
21
|
+
@constraint_violations.each do |object,fields|
|
22
|
+
fields.each do |field,keys|
|
23
|
+
keys.each do |key,context|
|
24
|
+
block.(object,field,key,context)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def []=(key_in_context,object)
|
31
|
+
@context[key_in_context] = object
|
32
|
+
end
|
33
|
+
|
34
|
+
def [](key_in_context)
|
35
|
+
@context.fetch(key_in_context)
|
36
|
+
rescue KeyError => ex
|
37
|
+
raise KeyError.new(
|
38
|
+
"Context did not contain '#{key_in_context}' (#{key_in_context.class}). Context has these keys: #{@context.keys.join(',')}",
|
39
|
+
receiver: ex.receiver,
|
40
|
+
key: ex.key)
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def constraint_violations? = self.constraint_violations.any?
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require_relative "../factory_bot"
|
2
|
+
module Brut
|
3
|
+
module Backend
|
4
|
+
class SeedData
|
5
|
+
def self.inherited(seed_data_klass)
|
6
|
+
@classes ||= []
|
7
|
+
@classes << seed_data_klass
|
8
|
+
end
|
9
|
+
def self.classes = @classes || []
|
10
|
+
|
11
|
+
def setup!
|
12
|
+
Brut::FactoryBot.new.setup!
|
13
|
+
end
|
14
|
+
|
15
|
+
def load_seeds!
|
16
|
+
DB.transaction do
|
17
|
+
self.class.classes.each do |klass|
|
18
|
+
klass.new.seed!
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# Subclass this in your back-end to create a server-side
|
2
|
+
# validator for your form. This provides for a much
|
3
|
+
# richer set of validations than you get from the browser, but
|
4
|
+
# works basically the same way.
|
5
|
+
class Brut::BackEnd::Validators::FormValidator
|
6
|
+
def self.validate(attribute,options)
|
7
|
+
@@validations ||= {}
|
8
|
+
@@validations[attribute] = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def validate(form)
|
12
|
+
@@validations.each do |attribute,options|
|
13
|
+
value = form.send(attribute)
|
14
|
+
options.each do |option, option_value|
|
15
|
+
case option
|
16
|
+
when :required
|
17
|
+
if option_value == true
|
18
|
+
if value.to_s.strip == ""
|
19
|
+
form.server_side_constraint_violation(input_name: attribute, key: :required)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
when :minlength
|
23
|
+
if value.respond_to?(:length) || value.nil?
|
24
|
+
if value.nil? || value.length < option_value
|
25
|
+
form.server_side_constraint_violation(input_name: attribute, key: :too_short, context: { minlength: option_value })
|
26
|
+
end
|
27
|
+
else
|
28
|
+
raise "'#{attribute}''s value (a '#{value.class}') does not respond to 'length' - :minlength cannot be used as a validation"
|
29
|
+
end
|
30
|
+
else
|
31
|
+
raise "'#{option}' is not a recognized validation option"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
data/lib/brut/cli/app.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
require "optparse"
|
2
|
+
require_relative "../junk_drawer"
|
3
|
+
class Brut::CLI::App
|
4
|
+
include Brut::CLI::ExecutionResults
|
5
|
+
include Brut::I18n::ForCLI
|
6
|
+
|
7
|
+
def self.commands
|
8
|
+
self.constants.map { |name|
|
9
|
+
self.const_get(name)
|
10
|
+
}.select { |constant|
|
11
|
+
constant.kind_of?(Class) && constant.ancestors.include?(Brut::CLI::Command) && constant.instance_methods.include?(:execute)
|
12
|
+
}
|
13
|
+
end
|
14
|
+
def self.description(new_description=nil)
|
15
|
+
if new_description.nil?
|
16
|
+
return @description.to_s
|
17
|
+
else
|
18
|
+
@description = new_description
|
19
|
+
end
|
20
|
+
end
|
21
|
+
def self.default_command(new_command_name=nil)
|
22
|
+
if new_command_name.nil?
|
23
|
+
return @default_command || "help"
|
24
|
+
else
|
25
|
+
@default_command = new_command_name.nil? ? nil : new_command_name.to_s
|
26
|
+
end
|
27
|
+
end
|
28
|
+
def self.opts
|
29
|
+
self.option_parser
|
30
|
+
end
|
31
|
+
def self.option_parser
|
32
|
+
@option_parser ||= OptionParser.new do |opts|
|
33
|
+
opts.banner = "%{app} %{global_options} commands [command options] [args]"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
def self.requires_project_env(default: "development")
|
37
|
+
default_message = if default.nil?
|
38
|
+
""
|
39
|
+
else
|
40
|
+
" (default '#{default}')"
|
41
|
+
end
|
42
|
+
opts.on("--env=ENVIRONMENT","Project environment#{default_message}")
|
43
|
+
@default_env = ENV["RACK_ENV"] || default
|
44
|
+
@requires_project_env = true
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.default_env = @default_env
|
48
|
+
def self.requires_project_env? = @requires_project_env
|
49
|
+
|
50
|
+
def self.configure_only!
|
51
|
+
@configure_only = true
|
52
|
+
end
|
53
|
+
def self.configure_only? = !!@configure_only
|
54
|
+
|
55
|
+
def initialize(global_options:,out:,err:,executor:)
|
56
|
+
@global_options = global_options
|
57
|
+
@out = out
|
58
|
+
@err = err
|
59
|
+
@executor = executor
|
60
|
+
if self.class.default_env
|
61
|
+
@global_options.set_default(:env,self.class.default_env)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def set_env_if_needed
|
66
|
+
if self.class.requires_project_env?
|
67
|
+
ENV["RACK_ENV"] = options.env
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def load_env(project_root:)
|
72
|
+
if !ENV["RACK_ENV"]
|
73
|
+
ENV["RACK_ENV"] = "development"
|
74
|
+
end
|
75
|
+
env = ENV["RACK_ENV"]
|
76
|
+
if env != "production"
|
77
|
+
require "dotenv"
|
78
|
+
Dotenv.load(project_root / ".env.#{env}",
|
79
|
+
project_root / ".env.#{env}.local")
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def before_execute
|
85
|
+
end
|
86
|
+
|
87
|
+
def after_bootstrap
|
88
|
+
end
|
89
|
+
|
90
|
+
def configure!
|
91
|
+
end
|
92
|
+
|
93
|
+
def execute!(command,project_root:)
|
94
|
+
before_execute
|
95
|
+
set_env_if_needed
|
96
|
+
command.set_env_if_needed
|
97
|
+
load_env(project_root:)
|
98
|
+
command.before_execute
|
99
|
+
bootstrap_result = begin
|
100
|
+
require "#{project_root}/app/bootstrap"
|
101
|
+
bootstrap = Bootstrap.new
|
102
|
+
if self.class.configure_only?
|
103
|
+
bootstrap.configure_only!
|
104
|
+
else
|
105
|
+
bootstrap.bootstrap!
|
106
|
+
end
|
107
|
+
continue_execution
|
108
|
+
rescue => ex
|
109
|
+
as_execution_result(command.handle_bootstrap_exception(ex))
|
110
|
+
end
|
111
|
+
if bootstrap_result.stop?
|
112
|
+
return bootstrap_result
|
113
|
+
end
|
114
|
+
after_bootstrap
|
115
|
+
as_execution_result(command.execute)
|
116
|
+
rescue Brut::CLI::Error => ex
|
117
|
+
abort_execution(ex.message)
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def options = @global_options
|
123
|
+
def out = @out
|
124
|
+
def err = @err
|
125
|
+
def puts(...)
|
126
|
+
warn("Your CLI apps should use out.puts or err.puts or produce terminal output, not plain puts", uplevel: 1)
|
127
|
+
Kernel.puts(...)
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
module Brut
|
2
|
+
module CLI
|
3
|
+
class AppRunner
|
4
|
+
include Brut::CLI::ExecutionResults
|
5
|
+
|
6
|
+
def initialize(app_klass:,project_root:)
|
7
|
+
@app_klass = app_klass
|
8
|
+
@project_root = project_root
|
9
|
+
end
|
10
|
+
|
11
|
+
def run!
|
12
|
+
app_klass = @app_klass
|
13
|
+
out = Brut::CLI::Output.new(io: $stdout,prefix: "[ #{$0} ] ")
|
14
|
+
err = Brut::CLI::Output.new(io: $stderr,prefix: "[ #{$0} ] ")
|
15
|
+
executor = Brut::CLI::Executor.new(out:,err:)
|
16
|
+
|
17
|
+
result,remaining_argv,global_options,global_option_parser = parse_global(app_klass:,out:)
|
18
|
+
|
19
|
+
if result.stop?
|
20
|
+
return result.to_i
|
21
|
+
end
|
22
|
+
|
23
|
+
result,command_klass = locate_command(remaining_argv:,app_klass:,err:,out:)
|
24
|
+
|
25
|
+
if result.stop?
|
26
|
+
if !result.ok?
|
27
|
+
err.puts_no_prefix "error: #{result.message}"
|
28
|
+
err.puts_no_prefix
|
29
|
+
if result.show_usage?
|
30
|
+
show_global_help(app_klass:,out:)
|
31
|
+
err.puts_no_prefix "error: #{result.message}"
|
32
|
+
end
|
33
|
+
elsif result.show_usage?
|
34
|
+
command_klass = result.command_klass
|
35
|
+
if command_klass.nil?
|
36
|
+
show_global_help(app_klass:,out:)
|
37
|
+
else
|
38
|
+
command_option_parser = command_klass.option_parser
|
39
|
+
show_command_help(global_option_parser:,command_option_parser:,command_klass:,out:)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
return result.to_i
|
43
|
+
end
|
44
|
+
|
45
|
+
command_options = {}
|
46
|
+
|
47
|
+
command_option_parser = command_klass.option_parser
|
48
|
+
|
49
|
+
command_option_parser.on("-h", "--help", "Get help on this command") do
|
50
|
+
show_command_help(global_option_parser:,command_option_parser:,command_klass:,out:)
|
51
|
+
return 0
|
52
|
+
end
|
53
|
+
|
54
|
+
command_argv = remaining_argv[1..-1] || []
|
55
|
+
args = command_option_parser.parse!(command_argv,into:command_options)
|
56
|
+
|
57
|
+
cli_app = app_klass.new(global_options:, out:, err:, executor:)
|
58
|
+
cmd = command_klass.new(command_options:Brut::CLI::Options.new(command_options),global_options:, args:, out:, err:, executor:)
|
59
|
+
|
60
|
+
result = cli_app.execute!(cmd, project_root:@project_root)
|
61
|
+
|
62
|
+
if result.message
|
63
|
+
if !result.ok?
|
64
|
+
err.puts "error: #{result.message}"
|
65
|
+
if result.show_usage?
|
66
|
+
err.puts
|
67
|
+
show_command_help(global_option_parser:,command_option_parser:,command_klass:,out:)
|
68
|
+
end
|
69
|
+
else
|
70
|
+
out.puts result.message
|
71
|
+
end
|
72
|
+
end
|
73
|
+
result.to_i
|
74
|
+
rescue OptionParser::InvalidArgument => ex
|
75
|
+
flag = ex.args.map { |_| _.gsub(/=.*$/,"") }.join(", ")
|
76
|
+
err.puts "error: #{ex.reason} from #{flag}: value given is not one of the allowed values"
|
77
|
+
65
|
78
|
+
rescue OptionParser::ParseError => ex
|
79
|
+
err.puts "error: #{ex.message}"
|
80
|
+
65
|
81
|
+
rescue => ex
|
82
|
+
if ENV["BRUT_CLI_RAISE_ON_ERROR"] == "true"
|
83
|
+
raise
|
84
|
+
else
|
85
|
+
err.puts "error: #{ex.message}"
|
86
|
+
70
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def locate_command(remaining_argv:,app_klass:,err:,out:)
|
93
|
+
|
94
|
+
command_name = remaining_argv[0] || app_klass.default_command
|
95
|
+
|
96
|
+
if !command_name
|
97
|
+
return cli_usage_error("#{$0} requires a command")
|
98
|
+
end
|
99
|
+
|
100
|
+
if command_name == "help"
|
101
|
+
command_needing_help = remaining_argv[1]
|
102
|
+
if command_needing_help
|
103
|
+
command_klass = app_klass.commands.detect { |c| c.name_matches?(remaining_argv[1]) }
|
104
|
+
if command_klass
|
105
|
+
return show_cli_usage(command_klass)
|
106
|
+
end
|
107
|
+
return cli_usage_error("No such command '#{command_needing_help}'")
|
108
|
+
end
|
109
|
+
return show_cli_usage
|
110
|
+
end
|
111
|
+
|
112
|
+
command_klass = app_klass.commands.detect { |c| c.name_matches?(command_name) }
|
113
|
+
|
114
|
+
if !command_klass
|
115
|
+
return cli_usage_error("#{command_name} is not a known command")
|
116
|
+
end
|
117
|
+
[ continue_execution, command_klass ]
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
def show_global_help(app_klass:,out:)
|
122
|
+
option_parser = app_klass.option_parser
|
123
|
+
option_parser.banner = option_parser.banner % {
|
124
|
+
app: $0,
|
125
|
+
global_options: option_parser.top.list.length == 0 ? "" : "[global options]",
|
126
|
+
}
|
127
|
+
out.puts_no_prefix option_parser.banner
|
128
|
+
out.puts_no_prefix
|
129
|
+
out.puts_no_prefix " #{app_klass.description}"
|
130
|
+
out.puts_no_prefix
|
131
|
+
out.puts_no_prefix "GLOBAL OPTIONS"
|
132
|
+
out.puts_no_prefix
|
133
|
+
option_parser.summarize do |line|
|
134
|
+
out.puts_no_prefix line
|
135
|
+
end
|
136
|
+
if app_klass.commands.any?
|
137
|
+
out.puts_no_prefix
|
138
|
+
out.puts_no_prefix "COMMANDS"
|
139
|
+
out.puts_no_prefix
|
140
|
+
max_length = [ 4, app_klass.commands.map { |_| _.command_name.to_s.length }.max ].max
|
141
|
+
printf_string = " %-#{max_length}s - %s%s\n"
|
142
|
+
printf printf_string, "help", "Get help on a command",""
|
143
|
+
app_klass.commands.sort_by(&:command_name).each do |command|
|
144
|
+
default_message = if command.name_matches?(app_klass.default_command)
|
145
|
+
" (default)"
|
146
|
+
else
|
147
|
+
""
|
148
|
+
end
|
149
|
+
|
150
|
+
description = if command.description && command.description.kind_of?(Proc)
|
151
|
+
command.description.()
|
152
|
+
else
|
153
|
+
command.description
|
154
|
+
end
|
155
|
+
printf printf_string, command.command_name, command.description, default_message
|
156
|
+
end
|
157
|
+
end
|
158
|
+
out.puts_no_prefix
|
159
|
+
end
|
160
|
+
|
161
|
+
def show_command_help(global_option_parser:,command_option_parser:,command_klass:,out:)
|
162
|
+
banner = command_option_parser.banner % {
|
163
|
+
app: $0,
|
164
|
+
global_options: global_option_parser.top.list.length == 0 ? "" : "[global options]",
|
165
|
+
command_options: command_option_parser.top.list.length == 0 ? "" : "[command options]",
|
166
|
+
args: command_klass.args,
|
167
|
+
}
|
168
|
+
command_option_parser.banner = "Usage: #{banner}"
|
169
|
+
out.puts_no_prefix command_option_parser.banner
|
170
|
+
out.puts_no_prefix
|
171
|
+
out.puts_no_prefix " " + command_klass.description
|
172
|
+
out.puts_no_prefix
|
173
|
+
if command_klass.detailed_description
|
174
|
+
out.puts_no_prefix " " + command_klass.detailed_description.strip
|
175
|
+
out.puts_no_prefix
|
176
|
+
end
|
177
|
+
out.puts_no_prefix "GLOBAL OPTIONS"
|
178
|
+
out.puts_no_prefix
|
179
|
+
global_option_parser.summarize do |line|
|
180
|
+
out.puts_no_prefix line
|
181
|
+
end
|
182
|
+
if command_option_parser.top.list.length > 0
|
183
|
+
out.puts_no_prefix
|
184
|
+
out.puts_no_prefix "COMMAND OPTIONS"
|
185
|
+
if command_option_parser.summarize.any?
|
186
|
+
out.puts_no_prefix
|
187
|
+
end
|
188
|
+
command_option_parser.summarize do |line|
|
189
|
+
out.puts_no_prefix line
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def parse_global(app_klass:,out:)
|
195
|
+
option_parser = app_klass.option_parser
|
196
|
+
|
197
|
+
option_parser.on("-h", "--help", "Get help") do
|
198
|
+
show_global_help(app_klass:,out:)
|
199
|
+
return stop_execution
|
200
|
+
end
|
201
|
+
log_levels = [ "debug", "info", "warn", "error", "fatal" ]
|
202
|
+
if ENV["LOG_LEVEL"].to_s == ""
|
203
|
+
ENV["LOG_LEVEL"] = log_levels[-1]
|
204
|
+
end
|
205
|
+
option_parser.on("--log-level=LEVEL","Set log level. Allowed values: #{log_levels.join(', ')}. Default '#{ENV["LOG_LEVEL"]}'",log_levels) do |value|
|
206
|
+
ENV["LOG_LEVEL"] = value
|
207
|
+
end
|
208
|
+
option_parser.on("--verbose","Set log level to '#{log_levels[0]}', which will produce maximum output") do
|
209
|
+
ENV["LOG_LEVEL"] = log_levels[0]
|
210
|
+
end
|
211
|
+
|
212
|
+
hash = {}
|
213
|
+
remaining_argv = option_parser.order!(into:hash)
|
214
|
+
global_options = Brut::CLI::Options.new(hash)
|
215
|
+
[ continue_execution, remaining_argv, global_options, option_parser ]
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require "json"
|
2
|
+
require "fileutils"
|
3
|
+
|
4
|
+
require "brut/cli"
|
5
|
+
|
6
|
+
class Brut::CLI::Apps::BuildAssets < Brut::CLI::App
|
7
|
+
description "Build and manage code and assets destined for the browser, such as CSS, JS, or images"
|
8
|
+
requires_project_env
|
9
|
+
default_command :all
|
10
|
+
configure_only!
|
11
|
+
|
12
|
+
class All < Brut::CLI::Command
|
13
|
+
description "Build all assets"
|
14
|
+
opts.on("--[no-]clean","If set the metadata file used to map the files to their hashed values is deleted before assets are built")
|
15
|
+
|
16
|
+
def execute
|
17
|
+
if options.clean?(default: true)
|
18
|
+
asset_metadata_file = Brut.container.asset_metadata_file
|
19
|
+
out.puts "Removing #{asset_metadata_file}"
|
20
|
+
FileUtils.rm_f(asset_metadata_file)
|
21
|
+
end
|
22
|
+
delegate_to_commands(Images, JS, CSS)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Images < Brut::CLI::Command
|
27
|
+
description "Copy images to the public folder"
|
28
|
+
detailed_description %{
|
29
|
+
This is to ensure that any images your code references will end up in the public directory, so they are served properly. This is not for managing images that may be referenced in CSS files. See the `css` command for information on that.
|
30
|
+
}
|
31
|
+
|
32
|
+
def execute
|
33
|
+
src_dir = Brut.container.images_src_dir
|
34
|
+
dest_dir = Brut.container.images_root_dir
|
35
|
+
|
36
|
+
command = "rsync --archive --delete --verbose #{src_dir}/ #{dest_dir}"
|
37
|
+
system! command
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class CSS < Brut::CLI::Command
|
42
|
+
description "Builds a single CSS file suitable for sending to the browser"
|
43
|
+
opts.on("--clean","If set, any .css files hanging around from a prevous build are deleted. Not recommended in production environments")
|
44
|
+
|
45
|
+
detailed_description %{
|
46
|
+
This produces a hashed file in every environment, in order to keep environments consistent and reduce differences. If your CSS file references images, fonts, or other assets via url() or other CSS functions, those files will be hashed and copied into the output directory where CSS is served.
|
47
|
+
|
48
|
+
To ensure this happens correctly, your url() or other function must reference the file as a relative file from where your actual source CSS file is located. For example, a font named some-font.ttf would be in app/src/front_end/fonts and to reference this from app/src/front_end/css/index.css you'd use the url "../fonts/some-font.ttf"
|
49
|
+
}
|
50
|
+
|
51
|
+
def execute
|
52
|
+
css_bundle = Brut.container.css_bundle_output_dir / "styles.css"
|
53
|
+
css_bundle_source = Brut.container.front_end_src_dir / "css" / "index.css"
|
54
|
+
esbuild_metafile = Brut.container.tmp_dir / "build-css-meta.json"
|
55
|
+
asset_metadata_file = Brut.container.asset_metadata_file
|
56
|
+
|
57
|
+
if options.clean?
|
58
|
+
out.puts "Cleaning old CSS files from #{Brut.container.css_bundle_output_dir}"
|
59
|
+
Dir[Brut.container.css_bundle_output_dir / "*.*"].each do |file|
|
60
|
+
if File.file?(file)
|
61
|
+
out.puts "Deleting #{file}"
|
62
|
+
FileUtils.rm(file)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
command = "npx esbuild --loader:.ttf=copy --loader:.otf=copy --metafile=#{esbuild_metafile} --entry-names=[name]-[hash] --sourcemap --bundle #{css_bundle_source} --outfile=#{css_bundle}"
|
68
|
+
out.puts "Building CSS bundle '#{css_bundle}' with '#{command}'"
|
69
|
+
system!(command)
|
70
|
+
|
71
|
+
if !File.exist?(esbuild_metafile)
|
72
|
+
err.puts "'#{esbuild_metafile}' was not generated - cannot continue"
|
73
|
+
exit 1
|
74
|
+
end
|
75
|
+
|
76
|
+
asset_metadata = Brut::FrontEnd::AssetMetadata.new(asset_metadata_file:,out:)
|
77
|
+
asset_metadata.merge!(extension: ".css", esbuild_metafile:)
|
78
|
+
asset_metadata.save!
|
79
|
+
end
|
80
|
+
end
|
81
|
+
class JS < Brut::CLI::Command
|
82
|
+
description "Builds and bundles JavaScript destined for the browser"
|
83
|
+
opts.on("--clean","If set, any .js files hanging around from a prevous build are deleted. Not recommended in production environments")
|
84
|
+
opts.on("--output-file=FILE","Bundle to create that will be sent to the browser, relative to the JS public folder. Default is app.js")
|
85
|
+
opts.on("--source-file=FILE","Entry point used to create the bundle, relative to the source JS folder. Default is index.js")
|
86
|
+
|
87
|
+
def execute
|
88
|
+
js_bundle = Brut.container.js_bundle_output_dir / options.output_file(default: "app.js")
|
89
|
+
js_bundle_source = Brut.container.front_end_src_dir / "js" / options.source_file(default: "index.js")
|
90
|
+
esbuild_metafile = Brut.container.tmp_dir / "build-js-meta.json"
|
91
|
+
asset_metadata_file = Brut.container.asset_metadata_file
|
92
|
+
|
93
|
+
name_with_hash_regexp = /app\/public\/(?<path>.+)\/(?<name>.+)\-(?<hash>.+)\.js/
|
94
|
+
if options.clean?
|
95
|
+
out.puts "Cleaning old JS files from #{Brut.container.js_bundle_output_dir}"
|
96
|
+
Dir[Brut.container.js_bundle_output_dir / "*.*"].each do |file|
|
97
|
+
if File.file?(file)
|
98
|
+
out.puts "Deleting #{file}"
|
99
|
+
FileUtils.rm(file)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
command = "npx esbuild --metafile=#{esbuild_metafile} --entry-names=[name]-[hash] --sourcemap --bundle #{js_bundle_source} --outfile=#{js_bundle}"
|
105
|
+
env_for_command = {
|
106
|
+
"NODE_PATH" => (Brut.container.project_root / "lib").to_s, # Not needed once Brut is properly bundled
|
107
|
+
}
|
108
|
+
out.puts "Building JS bundle '#{js_bundle}' with '#{command}'"
|
109
|
+
system!(env_for_command,command)
|
110
|
+
|
111
|
+
if !File.exist?(esbuild_metafile)
|
112
|
+
err.puts "'#{esbuild_metafile}' was not generated - cannot continue"
|
113
|
+
exit 1
|
114
|
+
end
|
115
|
+
|
116
|
+
asset_metadata = Brut::FrontEnd::AssetMetadata.new(asset_metadata_file:,out:)
|
117
|
+
asset_metadata.merge!(extension: ".js", esbuild_metafile:)
|
118
|
+
asset_metadata.save!
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|