From 042bf1d9b1e1412b7b1b9d27523d50f587450372 Mon Sep 17 00:00:00 2001 From: Alexander Popov Date: Sun, 2 Aug 2020 19:40:42 +0300 Subject: [PATCH] Update application template, finally split toys into gems --- lib/flame/cli/new/app.rb | 13 +- spec/flame/cli/new/app_spec.rb | 316 +++++++++++++----- template/.editorconfig | 3 + template/.eslintignore | 2 +- template/.eslintrc.yaml | 30 +- template/.eslintrc.yml | 80 ----- template/.rubocop.yml | 39 ++- template/.ruby-version | 1 - template/.toys/.preload/.require.rb | 6 - .../.preload/require_application_config.rb | 7 - template/.toys/.preload/root_dir.rb | 3 - template/.toys/.toys.rb | 23 -- template/.toys/.toys.rb.erb | 72 ++++ template/.toys/benchmark.rb | 10 - template/.toys/config/check.rb.erb | 23 -- template/.toys/console.rb | 13 - template/.toys/database/.preload.rb.erb | 52 --- template/.toys/database/.toys.rb | 11 - template/.toys/database/create.rb | 12 - template/.toys/database/drop.rb | 21 -- .../database/dumps/.preload/dump_file.rb | 121 ------- template/.toys/database/dumps/create.rb | 30 -- template/.toys/database/dumps/list.rb | 7 - template/.toys/database/dumps/restore.rb | 52 --- .../migrations/.preload/migration_file.rb | 189 ----------- template/.toys/database/migrations/.toys.rb | 5 - template/.toys/database/migrations/check.rb | 16 - .../create/.preload/create_migration_file.rb | 7 - .../create/.preload/render_template.rb | 9 - .../.toys/database/migrations/create/.toys.rb | 5 - .../database/migrations/create/regular.rb | 9 - template/.toys/database/migrations/disable.rb | 10 - template/.toys/database/migrations/enable.rb | 10 - template/.toys/database/migrations/list.rb | 8 - .../.toys/database/migrations/reversion.rb | 10 - .../.toys/database/migrations/rollback.rb | 16 - template/.toys/database/migrations/run.rb | 51 --- template/.toys/database/psql.rb | 8 - template/.toys/deploy.rb | 26 -- .../.toys/generate/.preload/base_generator.rb | 38 --- template/.toys/generate/form.rb | 28 -- .../.toys/generate/form/template.rb.erb.erb | 24 -- template/.toys/generate/model.rb | 9 - .../.toys/generate/model/template.rb.erb.erb | 8 - template/.toys/locales/.preload/crowdin.rb | 10 - template/.toys/locales/.preload/locale.rb | 68 ---- template/.toys/locales/check.rb | 26 -- template/.toys/locales/download.rb | 9 - template/.toys/locales/lint.rb | 32 -- template/.toys/locales/upload.rb | 9 - template/.toys/routes.rb.erb | 13 - template/.toys/static.rb | 35 -- template/Gemfile | 76 +++-- template/README.md.erb | 58 ++-- template/application.rb.erb | 75 ++++- template/assets/scripts/.eslintrc.yaml | 13 + template/assets/scripts/.keep | 0 template/assets/scripts/application.js | 78 +++++ template/assets/styles/.keep | 0 template/assets/styles/_colors.scss | 21 ++ template/assets/styles/_fonts.scss | 3 + template/assets/styles/_sizes.scss | 15 + template/assets/styles/components/_flash.scss | 23 ++ .../assets/styles/components/_footer.scss | 3 + template/assets/styles/components/_forms.scss | 83 +++++ .../assets/styles/components/_header.scss | 15 + template/assets/styles/components/_page.scss | 82 +++++ template/assets/styles/lib/_breakpoints.scss | 38 +++ template/assets/styles/lib/_clear_fix.scss | 7 + .../lib/_disable_password_autocomplete.scss | 3 + template/assets/styles/lib/_headings.scss | 7 + .../styles/lib/_input_with_clear_button.scss | 36 ++ .../assets/styles/lib/_inputs_with_types.scss | 16 + .../styles/lib/_small_and_large_elements.scss | 57 ++++ .../assets/styles/lib/_sticky_footer.scss | 18 + template/assets/styles/main.scss | 265 +++++++++++++++ template/benchmark/.rubocop.yml | 18 +- template/benchmark/main.example.rb | 4 +- template/config.ru.erb | 35 +- template/config/base.rb.erb | 110 ++++++ template/config/config.rb.erb | 26 -- template/config/database.example.yaml | 5 - template/config/database.example.yaml.erb | 23 ++ template/config/full.rb.erb | 16 + template/config/mail.example.yaml.erb | 23 ++ template/config/processors/logger.rb.erb | 19 -- template/config/processors/mail.rb.erb | 93 ++++++ template/config/processors/r18n.rb.erb | 28 ++ template/config/processors/sentry.rb.erb | 59 ++++ template/config/processors/sequel.rb.bak.erb | 63 ---- template/config/processors/sequel.rb.erb | 40 +++ template/config/processors/server.rb.erb | 21 ++ template/config/processors/shrine.rb.erb | 43 +++ template/config/puma.rb | 79 ----- template/config/puma.rb.erb | 65 ++++ template/config/sentry.example.yaml.erb | 7 + template/config/server.example.yaml | 4 +- template/constants.rb.erb | 18 - template/controllers/_controller.rb.erb | 29 +- template/controllers/site/_controller.rb.erb | 11 + template/exe/setup.sh | 7 +- template/exe/setup/node.sh | 27 +- template/exe/setup/ruby.sh | 15 +- template/exe/update.sh | 4 +- template/filewatchers.yaml | 16 +- template/forms/.keep | 0 template/forms/_base.rb.erb | 65 ++++ template/lib/.keep | 0 template/lib/flame/raven_context.rb | 14 + template/mailers/_base.rb.erb | 81 +++++ template/mailers/mail/_base.rb.erb | 48 +++ template/mailers/mail/default.rb.erb | 29 ++ template/package.json | 57 ++-- template/rollup.config.js.erb | 6 +- template/server | 213 ------------ template/views/site/errors/400.html.erb.erb | 12 + template/views/site/errors/404.html.erb.erb | 7 + template/views/site/errors/500.html.erb.erb | 28 ++ template/views/site/layout.html.erb.erb | 75 ++++- 119 files changed, 2282 insertions(+), 1888 deletions(-) delete mode 100644 template/.eslintrc.yml delete mode 100644 template/.ruby-version delete mode 100644 template/.toys/.preload/.require.rb delete mode 100644 template/.toys/.preload/require_application_config.rb delete mode 100644 template/.toys/.preload/root_dir.rb delete mode 100644 template/.toys/.toys.rb create mode 100644 template/.toys/.toys.rb.erb delete mode 100644 template/.toys/benchmark.rb delete mode 100644 template/.toys/config/check.rb.erb delete mode 100644 template/.toys/console.rb delete mode 100644 template/.toys/database/.preload.rb.erb delete mode 100644 template/.toys/database/.toys.rb delete mode 100644 template/.toys/database/create.rb delete mode 100644 template/.toys/database/drop.rb delete mode 100644 template/.toys/database/dumps/.preload/dump_file.rb delete mode 100644 template/.toys/database/dumps/create.rb delete mode 100644 template/.toys/database/dumps/list.rb delete mode 100644 template/.toys/database/dumps/restore.rb delete mode 100644 template/.toys/database/migrations/.preload/migration_file.rb delete mode 100644 template/.toys/database/migrations/.toys.rb delete mode 100644 template/.toys/database/migrations/check.rb delete mode 100644 template/.toys/database/migrations/create/.preload/create_migration_file.rb delete mode 100644 template/.toys/database/migrations/create/.preload/render_template.rb delete mode 100644 template/.toys/database/migrations/create/.toys.rb delete mode 100644 template/.toys/database/migrations/create/regular.rb delete mode 100644 template/.toys/database/migrations/disable.rb delete mode 100644 template/.toys/database/migrations/enable.rb delete mode 100644 template/.toys/database/migrations/list.rb delete mode 100644 template/.toys/database/migrations/reversion.rb delete mode 100644 template/.toys/database/migrations/rollback.rb delete mode 100644 template/.toys/database/migrations/run.rb delete mode 100644 template/.toys/database/psql.rb delete mode 100644 template/.toys/deploy.rb delete mode 100644 template/.toys/generate/.preload/base_generator.rb delete mode 100644 template/.toys/generate/form.rb delete mode 100644 template/.toys/generate/form/template.rb.erb.erb delete mode 100644 template/.toys/generate/model.rb delete mode 100644 template/.toys/generate/model/template.rb.erb.erb delete mode 100644 template/.toys/locales/.preload/crowdin.rb delete mode 100644 template/.toys/locales/.preload/locale.rb delete mode 100644 template/.toys/locales/check.rb delete mode 100644 template/.toys/locales/download.rb delete mode 100644 template/.toys/locales/lint.rb delete mode 100644 template/.toys/locales/upload.rb delete mode 100644 template/.toys/routes.rb.erb delete mode 100644 template/.toys/static.rb create mode 100644 template/assets/scripts/.eslintrc.yaml delete mode 100644 template/assets/scripts/.keep create mode 100644 template/assets/scripts/application.js delete mode 100644 template/assets/styles/.keep create mode 100644 template/assets/styles/_colors.scss create mode 100644 template/assets/styles/_fonts.scss create mode 100644 template/assets/styles/_sizes.scss create mode 100644 template/assets/styles/components/_flash.scss create mode 100644 template/assets/styles/components/_footer.scss create mode 100644 template/assets/styles/components/_forms.scss create mode 100644 template/assets/styles/components/_header.scss create mode 100644 template/assets/styles/components/_page.scss create mode 100644 template/assets/styles/lib/_breakpoints.scss create mode 100644 template/assets/styles/lib/_clear_fix.scss create mode 100644 template/assets/styles/lib/_disable_password_autocomplete.scss create mode 100644 template/assets/styles/lib/_headings.scss create mode 100644 template/assets/styles/lib/_input_with_clear_button.scss create mode 100644 template/assets/styles/lib/_inputs_with_types.scss create mode 100644 template/assets/styles/lib/_small_and_large_elements.scss create mode 100644 template/assets/styles/lib/_sticky_footer.scss create mode 100644 template/assets/styles/main.scss create mode 100644 template/config/base.rb.erb delete mode 100644 template/config/config.rb.erb delete mode 100644 template/config/database.example.yaml create mode 100644 template/config/database.example.yaml.erb create mode 100644 template/config/full.rb.erb create mode 100644 template/config/mail.example.yaml.erb delete mode 100644 template/config/processors/logger.rb.erb create mode 100644 template/config/processors/mail.rb.erb create mode 100644 template/config/processors/r18n.rb.erb create mode 100644 template/config/processors/sentry.rb.erb delete mode 100644 template/config/processors/sequel.rb.bak.erb create mode 100644 template/config/processors/sequel.rb.erb create mode 100644 template/config/processors/server.rb.erb create mode 100644 template/config/processors/shrine.rb.erb delete mode 100755 template/config/puma.rb create mode 100644 template/config/puma.rb.erb create mode 100644 template/config/sentry.example.yaml.erb delete mode 100644 template/constants.rb.erb delete mode 100644 template/forms/.keep create mode 100644 template/forms/_base.rb.erb delete mode 100644 template/lib/.keep create mode 100644 template/lib/flame/raven_context.rb create mode 100644 template/mailers/_base.rb.erb create mode 100644 template/mailers/mail/_base.rb.erb create mode 100644 template/mailers/mail/default.rb.erb delete mode 100755 template/server create mode 100644 template/views/site/errors/400.html.erb.erb create mode 100644 template/views/site/errors/404.html.erb.erb create mode 100644 template/views/site/errors/500.html.erb.erb diff --git a/lib/flame/cli/new/app.rb b/lib/flame/cli/new/app.rb index ebe5986..aa7454b 100644 --- a/lib/flame/cli/new/app.rb +++ b/lib/flame/cli/new/app.rb @@ -12,8 +12,9 @@ class App < Clamp::Command def execute @app_name = app_name @module_name = @app_name.camelize - @short_module_name = @module_name - .split(/([[:upper:]][[:lower:]]*)/).map! { |s| s[0] }.join + @short_module_name = + @module_name.split(/([[:upper:]][[:lower:]]*)/).map! { |s| s[0] }.join + @domain_name = @module_name.downcase make_dir do copy_template @@ -46,7 +47,7 @@ def clean_dirs def render_templates puts 'Replace module names in template...' - Dir.glob('**/*.erb', File::FNM_DOTMATCH).each do |file| + Dir.glob('**/*.erb', File::FNM_DOTMATCH).sort.each do |file| file_pathname = Pathname.new(file) basename_pathname = file_pathname.sub_ext('') puts "- #{basename_pathname}" @@ -56,9 +57,13 @@ def render_templates end end + PERMISSIONS = {}.freeze + def grant_permissions + return unless PERMISSIONS.any? + puts 'Grant permissions to files...' - File.chmod 0o744, 'server' + PERMISSIONS.each { |file, permissions| File.chmod permissions, file } end end end diff --git a/spec/flame/cli/new/app_spec.rb b/spec/flame/cli/new/app_spec.rb index 0f8cc3f..63708f2 100644 --- a/spec/flame/cli/new/app_spec.rb +++ b/spec/flame/cli/new/app_spec.rb @@ -5,7 +5,7 @@ describe 'Flame::CLI::New::App' do subject(:execute_command) do - Bundler.with_original_env { `#{FLAME_CLI} new app #{app_name}` } + Bundler.with_unbundled_env { `#{FLAME_CLI} new app #{app_name}` } end let(:app_name) { 'foo_bar' } @@ -26,21 +26,35 @@ 'Copy template directories and files...', 'Clean directories...', 'Replace module names in template...', - '- config.ru', - '- constants.rb', + '- .toys/.toys.rb', + '- README.md', '- application.rb', + '- config.ru', + '- config/base.rb', + '- config/database.example.yaml', + '- config/full.rb', + '- config/mail.example.yaml', + '- config/processors/mail.rb', + '- config/processors/r18n.rb', + '- config/processors/sentry.rb', + '- config/processors/server.rb', + '- config/processors/sequel.rb', + '- config/processors/shrine.rb', + '- config/puma.rb', + '- config/sentry.example.yaml', + '- config/site.example.yaml', '- controllers/_controller.rb', '- controllers/site/_controller.rb', '- controllers/site/index_controller.rb', + '- forms/_base.rb', + '- mailers/_base.rb', + '- mailers/mail/_base.rb', + '- mailers/mail/default.rb', + '- rollup.config.js', '- routes.rb', '- views/site/index.html.erb', '- views/site/layout.html.erb', - '- rollup.config.js', - '- config/config.rb', - '- config/site.example.yaml', - '- config/processors/logger.rb', - '- config/processors/sequel.rb.bak', - 'Grant permissions to files...', + # 'Grant permissions to files...', 'Done!' ] end @@ -96,94 +110,113 @@ before { execute_command } - describe '.toys/config/check.rb' do + describe '.toys/.toys.rb' do let(:expected_words) do [ - 'ExampleFile.all(FB::Application.config[:config_dir])' + 'FB::Application', + 'expand FlameGenerateToys::Template, namespace: FooBar', + 'FB::Config::Base.new' ] end it { is_expected.to match_words(*expected_words) } end - describe '.toys/database/.preload.rb' do + describe 'application.rb' do let(:expected_words) do [ - '@db_config = FB::Application.config[:database]', - '@db_connection = FB::Application.db_connection' + 'config = FooBar::Config::Base.new', + 'FooBar.complete_config config', + 'module FooBar', + 'class Application < Flame::Application' ] end it { is_expected.to match_words(*expected_words) } end - describe '.toys/generate/form/template.rb.erb' do + describe 'config.ru' do let(:expected_words) do [ - 'module FooBar' + 'FB::Application.require_dirs FB::APP_DIRS', + 'if FB::Application.config[:session]', + 'use Rack::Session::Cookie, FB::Application.config[:session][:cookie]', + 'use Rack::CommonLogger, FB::Application.logger', + 'FB::App = FB::Application', + 'run FB::Application' ] end it { is_expected.to match_words(*expected_words) } end - describe '.toys/generate/model/template.rb.erb' do + describe 'config/base.rb' do let(:expected_words) do [ - 'module FooBar' + 'module FooBar', + '::FB = ::FooBar', + 'APP_DIRS =' ] end it { is_expected.to match_words(*expected_words) } end - describe '.toys/routes.rb' do + describe 'config/full.rb' do let(:expected_words) do [ - 'puts FB::Application.router.routes' + 'module FooBar', + 'FB::Config::Processors.const_get(processor_name).new config' ] end it { is_expected.to match_words(*expected_words) } end - describe 'application.rb' do + describe 'config/puma.rb' do let(:expected_words) do [ - 'module FooBar' + 'config = FooBar::Config::Base.new' ] end it { is_expected.to match_words(*expected_words) } end - describe 'config.ru' do + describe 'config/database.example.yaml' do let(:expected_words) do [ - 'FB::Application.require_dirs FB::APP_DIRS', - 'if FB::Application.config[:session]', - 'use Rack::Session::Cookie, ' \ - 'FB::Application.config[:session][:cookie]', - 'use Rack::CommonLogger, FB::Application.logger', - 'run FB::Application' + ":database: 'foo_bar'", + ":user: 'foo_bar'" + ] + end + + it { is_expected.to match_words(*expected_words) } + end + + describe 'config/mail.example.yaml' do + let(:expected_words) do + [ + ":name: 'FooBar.com'", + ":email: 'info@foobar.com'", + ":user_name: 'info@foobar.com'" ] end it { is_expected.to match_words(*expected_words) } end - describe 'config/config.rb' do + describe 'config/sentry.example.yaml' do let(:expected_words) do [ - 'FB::Application.config.instance_exec do', - 'FB::ConfigProcessors.const_get(processor_name).new self' + ':host: sentry.foobar.com' ] end it { is_expected.to match_words(*expected_words) } end - describe 'config/processors/logger.rb' do + describe 'config/processors/mail.rb' do let(:expected_words) do [ 'module FooBar' @@ -193,25 +226,51 @@ it { is_expected.to match_words(*expected_words) } end - describe 'config/processors/sequel.rb.bak' do + describe 'config/processors/r18n.rb' do let(:expected_words) do [ - 'module FooBar', - 'FB::Application.db_connection.extension extension_name', - 'FB::Application.db_connection.loggers << FB::Application.logger', - "FB::Application.db_connection.freeze unless ENV['RACK_CONSOLE']" + 'module FooBar' ] end it { is_expected.to match_words(*expected_words) } end - describe 'constants.rb' do + describe 'config/processors/sentry.rb' do let(:expected_words) do [ 'module FooBar', - '::FB = ::FooBar', - 'APP_DIRS =' + 'FB::APP_DIRS' + ] + end + + it { is_expected.to match_words(*expected_words) } + end + + describe 'config/processors/server.rb' do + let(:expected_words) do + [ + 'module FooBar' + ] + end + + it { is_expected.to match_words(*expected_words) } + end + + describe 'config/processors/sequel.rb' do + let(:expected_words) do + [ + 'module FooBar' + ] + end + + it { is_expected.to match_words(*expected_words) } + end + + describe 'config/processors/shrine.rb' do + let(:expected_words) do + [ + 'module FooBar' ] end @@ -256,8 +315,7 @@ [ '# FooBar', '`createuser -U postgres foo_bar`', - '`createdb -U postgres foo_bar -O foo_bar`', - '`psql -U postgres -c "CREATE EXTENSION citext" foo_bar`', + 'Run `exe/setup.sh`', 'Add UNIX-user for project: `adduser foo_bar`', 'Make symbolic link of project directory to `/var/www/foo_bar`' ] @@ -286,10 +344,109 @@ it { is_expected.to match_words(*expected_words) } end + describe 'forms/_base.rb' do + let(:expected_words) do + [ + 'module FooBar', + 'FB::Application.db_connection' + ] + end + + it { is_expected.to match_words(*expected_words) } + end + + describe 'mailers/_base.rb' do + let(:expected_words) do + [ + 'module FooBar', + '@from = FB::Application.config[:mail][:from]', + '@controller = FB::MailController.new', + ## https://github.com/rubocop-hq/rubocop/issues/8416 + # rubocop:disable Lint/InterpolationCheck + 'FB::Application.logger.info "#{mail.log_message} [#{index}/#{count}]..."', + 'File.join(FB::Application.config[:tmp_dir], "mailing_#{object_id}")' + # rubocop:enable Lint/InterpolationCheck + ] + end + + it { is_expected.to match_words(*expected_words) } + end + + describe 'mailers/mail/_base.rb' do + let(:expected_words) do + [ + 'module FooBar', + 'FB::Application.logger.error e' + ] + end + + it { is_expected.to match_words(*expected_words) } + end + + describe 'mailers/mail/default.rb' do + let(:expected_words) do + [ + 'module FooBar' + ] + end + + it { is_expected.to match_words(*expected_words) } + end + + describe 'views/site/errors/400.html.erb' do + let(:expected_words) do + [ + '

<%= t.error.bad_request.title %>

', + '

<%= t.error.bad_request.subtitle %>

', + '<%= t.error.bad_request.text %>', + '', + '<%= t.button.back %>' + ] + end + + it { is_expected.to match_words(*expected_words) } + end + + describe 'views/site/errors/404.html.erb' do + let(:expected_words) do + [ + '

<%= t.error.page.itself.not_found %>

', + '
', + '<%= t.button.home %>' + ] + end + + it { is_expected.to match_words(*expected_words) } + end + + describe 'views/site/errors/500.html.erb' do + let(:expected_words) do + [ + '
', + '

<%= t.error.unexpected_error.title %>

', + '

<%= t.error.unexpected_error.subtitle %>

', + '<%= t.error.unexpected_error.text %>', + "<% if config[:environment] == 'development' %>", + '

<%==', + '%>

<%', + '%>

<%=', + '%>

<% end %><%=', + '%>
', + '<% end %>', + '
', + '<%= t.button.back %>' + ] + end + + it { is_expected.to match_words(*expected_words) } + end + describe 'views/site/layout.html.erb' do let(:expected_words) do [ - '<%= FB::Application.config[:site][:site_name] %>' + '<%= config[:site][:site_name] %>', + '<% if Raven.configuration.environments.include?(config[:environment]) &&', + "environment: '<%= config[:environment] %>'," ] end @@ -299,13 +456,13 @@ describe 'generates RuboCop-satisfying app' do subject do - Bundler.with_original_env do + Bundler.with_unbundled_env do system 'bundle exec rubocop' end end before do - Bundler.with_original_env do + Bundler.with_unbundled_env do execute_command Dir.chdir app_name @@ -323,8 +480,8 @@ describe 'generates working app' do subject do - Bundler.with_original_env do - pid = spawn './server start' + Bundler.with_unbundled_env do + pid = spawn 'toys server start' Process.detach pid @@ -338,29 +495,35 @@ response = Net::HTTP.get URI("http://127.0.0.1:#{port}/") rescue Errno::ECONNREFUSED => e sleep 1 - retry if number_of_attempts < 10 + retry if number_of_attempts < 20 raise e end response ensure - Bundler.with_original_env { `./server stop` } + Bundler.with_unbundled_env { `toys server stop` } Process.wait pid end end - before do - Bundler.with_original_env do - ENV['RACK_ENV'] = 'development' + around do |example| + ## HACK: https://github.com/dazuma/toys/issues/57 + original_toys_file_name = "#{__dir__}/../../../../.toys.rb" + File.rename original_toys_file_name, "#{original_toys_file_name}.bak" + + example.run + File.rename "#{original_toys_file_name}.bak", original_toys_file_name + end + + before do + Bundler.with_unbundled_env do execute_command Dir.chdir app_name - %w[server session site].each do |config| - FileUtils.cp( - "config/#{config}.example.yaml", "config/#{config}.yaml" - ) + Dir['config/*.example.yaml'].each do |config_example_file_name| + FileUtils.cp config_example_file_name, config_example_file_name.sub('.example', '') end ## HACK for testing while some server is running @@ -369,7 +532,7 @@ File.read('config/server.yaml').sub('port: 3000', "port: #{port}") ) - system 'bundle install' + system 'exe/setup.sh' end end @@ -386,38 +549,13 @@ result end - let(:expected_response) do - <<~RESPONSE - - - - - FooBar - - -

Hello, world!

- - - - RESPONSE - end - - it { is_expected.to eq expected_response } - end - - describe 'grants `./server` file execution permissions' do - subject { File.stat('server').mode.to_s(8)[3..5] } - - before do - execute_command - - Dir.chdir app_name - end - - after do - Dir.chdir '..' + let(:expected_response_lines) do + [ + 'FooBar', + '

Hello, world!

' + ] end - it { is_expected.to eq '744' } + it { is_expected.to include(*expected_response_lines) } end end diff --git a/template/.editorconfig b/template/.editorconfig index 5326ee1..9fab75a 100644 --- a/template/.editorconfig +++ b/template/.editorconfig @@ -7,9 +7,12 @@ end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true +max_line_length = 100 [*.y{a,}ml] indent_style = space +indent_size = 2 [*.md] indent_style = space +indent_size = 4 diff --git a/template/.eslintignore b/template/.eslintignore index 9c77fea..45a1997 100644 --- a/template/.eslintignore +++ b/template/.eslintignore @@ -1 +1 @@ -public/scripts/app/compiled/ +public/scripts/application/compiled/ diff --git a/template/.eslintrc.yaml b/template/.eslintrc.yaml index 3921eda..9cc83bb 100644 --- a/template/.eslintrc.yaml +++ b/template/.eslintrc.yaml @@ -13,8 +13,8 @@ rules: - error - unix max-len: - - warn - - code: 80 + - error + - code: 100 tabWidth: 2 ignoreUrls: true quotes: @@ -36,6 +36,9 @@ rules: space-before-function-paren: - warn - never + function-paren-newline: + - warn + - consistent space-before-blocks: - warn - always @@ -58,13 +61,20 @@ rules: - allow: - error - warn + arrow-body-style: + - warn + arrow-parens: + - warn + - as-needed + arrow-spacing: + - warn overrides: - files: - - "*.config.js" - env: - browser: false - node: true - es6: true - parserOptions: - sourceType: module + - files: + - "*.config.js" + env: + browser: false + node: true + es6: true + parserOptions: + sourceType: module diff --git a/template/.eslintrc.yml b/template/.eslintrc.yml deleted file mode 100644 index 765d0a0..0000000 --- a/template/.eslintrc.yml +++ /dev/null @@ -1,80 +0,0 @@ -extends: 'eslint:recommended' -env: - browser: true -rules: - indent: - - error - - tab - - SwitchCase: 1 - no-mixed-spaces-and-tabs: - - error - - smart-tabs - linebreak-style: - - error - - unix - max-len: - - error - - code: 80 - tabWidth: 2 - ignoreUrls: true - quotes: - - warn - - single - - avoidEscape: true - semi: - - error - - always - no-multi-spaces: - - error - keyword-spacing: - - warn - - overrides: - catch: - after: false - brace-style: - - error - space-before-function-paren: - - warn - - never - function-paren-newline: - - warn - - consistent - space-before-blocks: - - warn - - always - block-spacing: - - warn - - always - key-spacing: - - warn - object-curly-spacing: - - warn - - always - space-infix-ops: - - warn - space-in-parens: - - warn - no-unused-vars: - - warn - no-console: - - warn - - allow: - - error - - warn - arrow-body-style: - - warn - arrow-parens: - - warn - - as-needed - arrow-spacing: - - warn - -overrides: - - files: - - "*.config.js" - env: - browser: false - node: true - es6: true - parserOptions: - sourceType: module diff --git a/template/.rubocop.yml b/template/.rubocop.yml index 4d2d8bd..6e41d99 100644 --- a/template/.rubocop.yml +++ b/template/.rubocop.yml @@ -1,27 +1,38 @@ -Layout/Tab: - Enabled: false +require: + - rubocop-performance + - rubocop-rspec + +Layout/IndentationStyle: + EnforcedStyle: tabs IndentationWidth: 2 Layout/IndentationWidth: Width: 1 +Layout/LineLength: + Max: 100 Layout/MultilineMethodCallIndentation: EnforcedStyle: indented +Layout/MultilineOperationIndentation: + EnforcedStyle: indented Layout/ArgumentAlignment: EnforcedStyle: with_fixed_indentation - -Style/HashEachMethods: - Enabled: true -Style/HashTransformKeys: - Enabled: true -Style/HashTransformValues: - Enabled: true - -Lint/RaiseException: - Enabled: true -Lint/StructNewOverride: - Enabled: true +Layout/ParameterAlignment: + EnforcedStyle: with_fixed_indentation +Layout/FirstArgumentIndentation: + EnforcedStyle: consistent +Layout/FirstParameterIndentation: + EnforcedStyle: consistent +Layout/FirstArrayElementIndentation: + EnforcedStyle: consistent +Layout/FirstHashElementIndentation: + EnforcedStyle: consistent +Layout/MultilineArrayBraceLayout: + EnforcedStyle: new_line +Layout/MultilineHashBraceLayout: + EnforcedStyle: new_line AllCops: TargetRubyVersion: 2.6 + NewCops: enable Metrics/BlockLength: Exclude: diff --git a/template/.ruby-version b/template/.ruby-version deleted file mode 100644 index 338a5b5..0000000 --- a/template/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -2.6.6 diff --git a/template/.toys/.preload/.require.rb b/template/.toys/.preload/.require.rb deleted file mode 100644 index d5af821..0000000 --- a/template/.toys/.preload/.require.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'bundler/setup' -Bundler.setup :system, :toys, :database - -require 'pry-byebug' diff --git a/template/.toys/.preload/require_application_config.rb b/template/.toys/.preload/require_application_config.rb deleted file mode 100644 index 4972de9..0000000 --- a/template/.toys/.preload/require_application_config.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -private - -def require_application_config - require_relative '../../config/config' -end diff --git a/template/.toys/.preload/root_dir.rb b/template/.toys/.preload/root_dir.rb deleted file mode 100644 index c4056b5..0000000 --- a/template/.toys/.preload/root_dir.rb +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -ROOT_DIR = File.expand_path "#{__dir__}/../../" diff --git a/template/.toys/.toys.rb b/template/.toys/.toys.rb deleted file mode 100644 index 86b4846..0000000 --- a/template/.toys/.toys.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -include :exec, exit_on_nonzero_status: true unless include?(:exec) - -subtool_apply do - ## https://github.com/dazuma/toys/issues/23#issuecomment-589031428 - # include :bundler unless include?(:bundler) - include :exec, exit_on_nonzero_status: true unless include?(:exec) -end - -require 'benchmark_toys_template' -expand BenchmarkToysTemplate - -alias_tool :db, :database - -alias_tool :psql, 'database:psql' - -alias_tool :c, :console - -alias_tool :bench, :benchmark -alias_tool :b, :benchmark - -alias_tool :g, :generate diff --git a/template/.toys/.toys.rb.erb b/template/.toys/.toys.rb.erb new file mode 100644 index 0000000..524e03e --- /dev/null +++ b/template/.toys/.toys.rb.erb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +include :bundler, static: true + +config_dir = "#{__dir__}/../config" + +require "#{config_dir}/base" + +require 'benchmark_toys' +expand BenchmarkToys::Template +alias_tool :b, :benchmark + +application_proc = proc do + require_relative '../application' + <%= @short_module_name %>::Application +end + +require 'config_toys' +expand ConfigToys::Template, config_dir: config_dir + +db_connection_proc = proc do + application_proc.call.db_connection +end + +require 'sequel_migrations_toys' +expand SequelMigrationsToys::Template, db_connection_proc: db_connection_proc + +require 'psql_toys' +expand PSQLToys::Template, + db_config_proc: (proc do + ## For full config, not base + application_proc.call.config[:database] + end), + db_connection_proc: db_connection_proc, + db_extensions: %w[citext pgcrypto].freeze + +alias_tool :db, :database + +require 'flame_deploy_toys' +expand FlameDeployToys::Template, config_dir: config_dir + +require 'flame_generate_toys' +expand FlameGenerateToys::Template, namespace: <%= @module_name %> + +require 'flame_routes_toys' +expand FlameRoutesToys::Template, application_proc: application_proc + +require 'flame_server_toys' +expand FlameServerToys::Template, + config_proc: (proc do + <%= @short_module_name %>::Config::Base.new + end) + +require 'locales_toys' +expand LocalesToys::Template + +# tool :locales do +# require 'crowdin_toys' +# expand CrowdinToys::Template +# end + +require 'rack_console_toys' +expand RackConsoleToys::Template + +require 'static_files_toys' +expand StaticFilesToys::Template + +tool :rubocop do + def run + exec 'rubocop' + end +end diff --git a/template/.toys/benchmark.rb b/template/.toys/benchmark.rb deleted file mode 100644 index df67889..0000000 --- a/template/.toys/benchmark.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -desc 'Benchmark code' - -def run - ExampleFile.new("#{ROOT_DIR}/benchmark/main.example.rb") - .actualize_regular_file - - sh 'ruby benchmark/main.rb' -end diff --git a/template/.toys/config/check.rb.erb b/template/.toys/config/check.rb.erb deleted file mode 100644 index 1682a35..0000000 --- a/template/.toys/config/check.rb.erb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -desc 'Check config files' - -def run - check_editor_environment_variable - - require_application_config - - example_files.each(&:actualize_regular_file) -end - -private - -def example_files - ExampleFile.all(<%= @short_module_name %>::Application.config[:config_dir]) -end - -def check_editor_environment_variable - return unless ENV['EDITOR'].to_s.empty? - - abort '`EDITOR` environment variable is empty, see README' -end diff --git a/template/.toys/console.rb b/template/.toys/console.rb deleted file mode 100644 index b8aced9..0000000 --- a/template/.toys/console.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -desc 'Start interactive console' - -optional_arg :environment, default: 'development' - -def run - require 'rack/console' - - ARGV.clear - ENV['RACK_CONSOLE'] = 'true' - Rack::Console.new(environment: environment).start -end diff --git a/template/.toys/database/.preload.rb.erb b/template/.toys/database/.preload.rb.erb deleted file mode 100644 index 1ffae21..0000000 --- a/template/.toys/database/.preload.rb.erb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true - -def db_config - return @db_config if defined?(@db_config) - - require_application_config - - @db_config = <%= @short_module_name %>::Application.config[:database] -end - -def db_connection - return @db_connection if defined?(@db_connection) - - require_application_config - - @db_connection = <%= @short_module_name %>::Application.db_connection -end - -def db_access - @db_access ||= - { '-U' => db_config[:user], '-h' => db_config[:host] } - .compact.map { |key, value| "#{key} #{value}" }.join(' ') -end - -def pgpass_line - @pgpass_line ||= - db_config - .fetch_values(:host, :port, :database, :user, :password) { |_key| '*' } - .join(':') -end - -## Constants for DB - -DB_DIR = File.join(ROOT_DIR, 'db') -DB_MIGRATIONS_DIR = File.join(DB_DIR, 'migrations') -DB_DUMPS_DIR = File.join(DB_DIR, 'dumps') - -PGPASS_FILE = File.expand_path '~/.pgpass' - -DB_EXTENSIONS = %w[citext pgcrypto].freeze - -def update_pgpass - pgpass_lines = - File.exist?(PGPASS_FILE) ? File.read(PGPASS_FILE).split($RS) : [] - - return if pgpass_lines&.include? pgpass_line - - File.write PGPASS_FILE, pgpass_lines.push(pgpass_line, nil).join($RS) - File.chmod(0o600, PGPASS_FILE) -end - -# db_connection.loggers << Logger.new($stdout) diff --git a/template/.toys/database/.toys.rb b/template/.toys/database/.toys.rb deleted file mode 100644 index 23af514..0000000 --- a/template/.toys/database/.toys.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -alias_tool :migrate, 'migrations:run' - -alias_tool :migrations, 'migrations:list' - -alias_tool :dump, 'dumps:create' - -alias_tool :dumps, 'dumps:list' - -alias_tool :restore, 'dumps:restore' diff --git a/template/.toys/database/create.rb b/template/.toys/database/create.rb deleted file mode 100644 index d83e7a6..0000000 --- a/template/.toys/database/create.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -desc 'Create empty DB' - -def run - sh "createdb -U postgres #{db_config[:database]}" \ - " -O #{db_config[:user]}" - DB_EXTENSIONS.each do |db_extension| - sh "psql -U postgres -c 'CREATE EXTENSION #{db_extension}'" \ - " #{db_config[:database]}" - end -end diff --git a/template/.toys/database/drop.rb b/template/.toys/database/drop.rb deleted file mode 100644 index 89a36c6..0000000 --- a/template/.toys/database/drop.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -desc 'Drop DB' - -flag :force, '-f', '--[no-]force' -flag :question, '-q', '--[no-]question', default: true - -def run - if question - if Question.new( - "Drop #{db_config[:database]} ?", %w[yes no] - ).answer == 'no' - abort 'OK' - end - end - - exec_tool 'db:dump' unless force - - db_connection.disconnect - sh "dropdb --if-exists #{db_access} #{db_config[:database]}" -end diff --git a/template/.toys/database/dumps/.preload/dump_file.rb b/template/.toys/database/dumps/.preload/dump_file.rb deleted file mode 100644 index 1ab0c23..0000000 --- a/template/.toys/database/dumps/.preload/dump_file.rb +++ /dev/null @@ -1,121 +0,0 @@ -# frozen_string_literal: true - -require 'paint' - -## Class for single DB dump file -class DumpFile - DB_DUMP_TIMESTAMP = '%Y-%m-%d_%H-%M' - - DB_DUMP_TIMESTAMP_REGEXP_MAP = { - 'Y' => '\d{4}', - 'm' => '\d{2}', - 'd' => '\d{2}', - 'H' => '\d{2}', - 'M' => '\d{2}' - }.freeze - - missing_keys = - DB_DUMP_TIMESTAMP.scan(/%(\w)/).flatten - - DB_DUMP_TIMESTAMP_REGEXP_MAP.keys - - if missing_keys.any? - raise "`DB_DUMP_TIMESTAMP_REGEXP_MAP` doesn't contain keys" \ - " #{missing_keys} for `DB_DUMP_TIMESTAMP`" - end - - DB_DUMP_TIMESTAMP_REGEXP = - DB_DUMP_TIMESTAMP_REGEXP_MAP - .each_with_object(DB_DUMP_TIMESTAMP.dup) do |(key, value), result| - result.gsub! "%#{key}", value - end - - DB_DUMP_FORMATS = %w[custom plain].freeze - - DB_DUMP_EXTENSIONS = { - 'plain' => '.sql', - 'custom' => '.dump' - }.freeze - - missing_formats = DB_DUMP_FORMATS.reject do |db_dump_format| - DB_DUMP_EXTENSIONS[db_dump_format] - end - - if missing_formats.any? - raise "`DB_DUMP_EXTENSIONS` has no keys for #{missing_formats}" \ - ' from `DB_DUMP_FORMATS`' - end - - class << self - def db_dump_regexp - return unless db_config - return @db_dump_regexp if defined?(@db_dump_regexp) - - regexp_escaped_db_dump_extensions = - DB_DUMP_EXTENSIONS.values.map do |db_dump_extension| - Regexp.escape(db_dump_extension) - end - - @db_dump_regexp = /^ - #{DB_DUMPS_DIR}#{Regexp.escape(File::SEPARATOR)} - #{db_config[:database]}_#{DB_DUMP_TIMESTAMP_REGEXP} - (#{regexp_escaped_db_dump_extensions.join('|')}) - $/xo - end - - def all - Dir[File.join(DB_DUMPS_DIR, '*')] - .select { |file| file.match?(db_dump_regexp) } - .map! { |file| new filename: file } - .sort! - end - end - - attr_reader :version, :timestamp, :format - - def initialize(filename: nil, format: 'custom') - if filename - @extension = File.extname(filename) - @format = DB_DUMP_EXTENSIONS.key(@extension) - self.version = filename[/#{DB_DUMP_TIMESTAMP_REGEXP}/o] - else - @format = format - @extension = DB_DUMP_EXTENSIONS[@format] - self.timestamp = Time.now - end - end - - def <=>(other) - timestamp <=> other.timestamp - end - - def to_s - "#{readable_timestamp} #{format}" - end - - def print - puts to_s - end - - def path - File.join( - DB_DUMPS_DIR, - "#{db_config[:database]}_#{version}#{@extension}" - ) - end - - private - - def version=(value) - @version = value - @timestamp = Time.strptime(version, DB_DUMP_TIMESTAMP) - end - - def timestamp=(value) - @timestamp = value - @version = timestamp.strftime(DB_DUMP_TIMESTAMP) - end - - def readable_timestamp - Paint[timestamp.strftime('%F %R'), :cyan] - end -end diff --git a/template/.toys/database/dumps/create.rb b/template/.toys/database/dumps/create.rb deleted file mode 100644 index 92006dd..0000000 --- a/template/.toys/database/dumps/create.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -desc 'Make DB dump' - -flag :format, '-f', '--format=VALUE', - default: DumpFile::DB_DUMP_FORMATS.first, - handler: (lambda do |value, _previous| - DumpFile::DB_DUMP_FORMATS.find do |db_dump_format| - db_dump_format.start_with? value - end - end) - -def run - require 'benchmark' - - update_pgpass - - sh "mkdir -p #{DB_DUMPS_DIR}" - time = Benchmark.realtime do - sh "pg_dump #{db_access} -F#{format.chr}" \ - " #{db_config[:database]} > #{filename}" - end - puts "Done in #{time.round(2)} s." -end - -private - -def filename - DumpFile.new(format: format).path -end diff --git a/template/.toys/database/dumps/list.rb b/template/.toys/database/dumps/list.rb deleted file mode 100644 index 1ccb978..0000000 --- a/template/.toys/database/dumps/list.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -desc 'List DB dumps' - -def run - DumpFile.all.each(&:print) -end diff --git a/template/.toys/database/dumps/restore.rb b/template/.toys/database/dumps/restore.rb deleted file mode 100644 index ed8f30e..0000000 --- a/template/.toys/database/dumps/restore.rb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true - -desc 'Restore DB dump' - -optional_arg :step, accept: Integer, default: -1 - -def run - update_pgpass - - @dump_file = DumpFile.all[step] - - abort 'Dump file not found' unless @dump_file - - if Question.new("Restore #{@dump_file} ?", %w[yes no]).answer == 'no' - abort 'Okay' - end - - drop_if_exists - - exec_tool 'db:create' - - restore -end - -private - -def drop_if_exists - return unless sh( - "psql #{db_access} -l | grep '^\s#{db_config[:database]}\s'" - ) - - exec_tool 'db:dump' - exec_tool 'db:drop' -end - -def restore - case @dump_file.format - when 'custom' - pg_restore - when 'plain' - sh "psql #{db_access}" \ - " #{db_config[:database]} < #{@dump_file.path}" - else - raise 'Unknown DB dump file format' - end -end - -def pg_restore - sh "pg_restore #{db_access} -n public" \ - " -d #{db_config[:database]} #{@dump_file.path}" \ - ' --jobs=4 --clean --if-exists' -end diff --git a/template/.toys/database/migrations/.preload/migration_file.rb b/template/.toys/database/migrations/.preload/migration_file.rb deleted file mode 100644 index 9d1c77b..0000000 --- a/template/.toys/database/migrations/.preload/migration_file.rb +++ /dev/null @@ -1,189 +0,0 @@ -# frozen_string_literal: true - -require 'memery' -require 'paint' -require 'time' - -## Migration file -class MigrationFile # rubocop:disable Metrics/ClassLength - CONTENT = proc do |content| - content ||= +<<~DEFAULT - change do - end - DEFAULT - - ## /^(?!$)/ - searches for a position that starts at the - ## "start of line" position and - ## is not followed by the "end of line" position - - <<~STR - # frozen_string_literal: true - - Sequel.migration do - #{content.gsub!(/^(?!$)/, "\t").strip!} - end - STR - end - - DISABLING_EXT = '.bak' - - class << self - include Memery - - def database_schema_migrations - @database_schema_migrations ||= - db_connection[:schema_migrations].select_map(:filename) - end - - def find(query, only_one: true, enabled: true, disabled: true) - filenames = Dir[File.join(DB_MIGRATIONS_DIR, "*#{query}*")] - filenames.select! { |filename| File.file? filename } - files = filenames.map { |filename| new filename: filename }.sort! - files.reject!(&:disabled) unless disabled - files.select!(&:disabled) unless enabled - - return files unless only_one - return files.first if files.size < 2 - - raise 'More than one file mathes the query' - end - - memoize def applied - database_schema_migrations.map { |one| new filename: one } - end - - memoize def existing - find '*', only_one: false, disabled: false - end - - memoize def applied_not_existing - existing_names = existing.map(&:basename) - - applied.reject do |one| - existing_names.include? one.basename - end - end - - memoize def existing_not_applied - existing.reject do |one| - database_schema_migrations.include? one.basename - end - end - end - - attr_accessor :version, :name, :disabled - - def initialize(filename: nil, name: nil, content: nil) - self.filename = filename - self.name = name if name - @content = content - end - - ## Accessors - - def basename - File.basename(@filename) - end - - def filename=(value) - parse_filename value if value.is_a? String - @filename = value - end - - def name=(value) - @name = value.tr(' ', '_').downcase - end - - def disabled=(value) - @disabled = - case value - when String - [DISABLING_EXT, DISABLING_EXT[1..-1]].include? value - else - value - end - end - - def <=>(other) - version <=> other.version - end - - def applied? - self.class.database_schema_migrations.include?(basename) - end - - ## Behavior - - def print - datetime = Time.parse(version).strftime('%F %R') - - puts [ - Paint["[#{version}]", :white], - Paint[datetime, disabled ? :white : :cyan], - Paint[fullname, disabled ? :white : :default], - (Paint['(not applied)', :red] unless applied?) - ].join(' ') - end - - def generate - self.version = new_version - FileUtils.mkdir_p File.dirname new_filename - File.write new_filename, CONTENT.call(@content) - puts "Migration #{relative_filename} created." - end - - def reversion - rename version: new_version - end - - def disable - abort 'Migration already disabled' if disabled - - rename disabled: true - - puts "Migration #{relative_filename} disabled." - end - - def enable - abort 'Migration already enabled' unless disabled - - rename disabled: false - - puts "Migration #{relative_filename} enabled." - end - - private - - def fullname - result = name.tr('_', ' ').capitalize - disabled ? "- #{result} (disabled)" : result - end - - def parse_filename(value = @filename) - basename = File.basename value - self.version, parts = basename.split('_', 2) - self.name, _ext, self.disabled = parts.split('.') - end - - def new_version - Time.now.strftime('%Y%m%d%H%M%S') - end - - def rename(vars = {}) - vars.each { |key, value| send :"#{key}=", value } - - return unless @filename.is_a? String - - File.rename @filename, new_filename - self.filename = new_filename - end - - def new_filename - new_basename = "#{version}_#{name}.rb#{DISABLING_EXT if disabled}" - File.join DB_MIGRATIONS_DIR, new_basename - end - - def relative_filename - new_filename.gsub("#{ROOT_DIR}/", '') - end -end diff --git a/template/.toys/database/migrations/.toys.rb b/template/.toys/database/migrations/.toys.rb deleted file mode 100644 index c419274..0000000 --- a/template/.toys/database/migrations/.toys.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -alias_tool :create, 'create:regular' - -alias_tool :new, :create diff --git a/template/.toys/database/migrations/check.rb b/template/.toys/database/migrations/check.rb deleted file mode 100644 index 13b229f..0000000 --- a/template/.toys/database/migrations/check.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -desc 'Check applied migrations' - -def run - if MigrationFile.applied_not_existing.any? - puts 'Applied, but not existing' - MigrationFile.applied_not_existing.each(&:print) - puts "\n" if MigrationFile.existing_not_applied.any? - end - - return unless MigrationFile.existing_not_applied.any? - - puts 'Existing, but not applied' - MigrationFile.existing_not_applied.each(&:print) -end diff --git a/template/.toys/database/migrations/create/.preload/create_migration_file.rb b/template/.toys/database/migrations/create/.preload/create_migration_file.rb deleted file mode 100644 index 3e5b2e7..0000000 --- a/template/.toys/database/migrations/create/.preload/create_migration_file.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -def create_migration_file(name, content = nil) - file = MigrationFile.new name: name, content: content - - file.generate -end diff --git a/template/.toys/database/migrations/create/.preload/render_template.rb b/template/.toys/database/migrations/create/.preload/render_template.rb deleted file mode 100644 index a8a5814..0000000 --- a/template/.toys/database/migrations/create/.preload/render_template.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -def render_template(filename) - require 'erb' - filename = File.join DB_MIGRATIONS_DIR, 'templates', "#{filename}.rb.erb" - renderer = ERB.new(File.read(filename)) - renderer.filename = filename - renderer.result(binding) -end diff --git a/template/.toys/database/migrations/create/.toys.rb b/template/.toys/database/migrations/create/.toys.rb deleted file mode 100644 index 9636bf8..0000000 --- a/template/.toys/database/migrations/create/.toys.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -alias_tool :audits, :audits_table - -alias_tool :subscription, :subscription_type diff --git a/template/.toys/database/migrations/create/regular.rb b/template/.toys/database/migrations/create/regular.rb deleted file mode 100644 index 17aa588..0000000 --- a/template/.toys/database/migrations/create/regular.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -desc 'Create regular migration' - -required_arg :name - -def run - create_migration_file name -end diff --git a/template/.toys/database/migrations/disable.rb b/template/.toys/database/migrations/disable.rb deleted file mode 100644 index 7e78ae6..0000000 --- a/template/.toys/database/migrations/disable.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -desc 'Disable migration' - -required_arg :filename - -def run - file = MigrationFile.find filename - file.disable -end diff --git a/template/.toys/database/migrations/enable.rb b/template/.toys/database/migrations/enable.rb deleted file mode 100644 index 037ad91..0000000 --- a/template/.toys/database/migrations/enable.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -desc 'Enable migration' - -required_arg :filename - -def run - file = MigrationFile.find filename - file.enable -end diff --git a/template/.toys/database/migrations/list.rb b/template/.toys/database/migrations/list.rb deleted file mode 100644 index ece8f29..0000000 --- a/template/.toys/database/migrations/list.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -desc 'Show all migrations' - -def run - files = MigrationFile.find '*', only_one: false - files.each(&:print) -end diff --git a/template/.toys/database/migrations/reversion.rb b/template/.toys/database/migrations/reversion.rb deleted file mode 100644 index cd01bd5..0000000 --- a/template/.toys/database/migrations/reversion.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -desc 'Change version of migration to latest' - -required_arg :filename - -def run - file = MigrationFile.find filename - file.reversion -end diff --git a/template/.toys/database/migrations/rollback.rb b/template/.toys/database/migrations/rollback.rb deleted file mode 100644 index 59e2764..0000000 --- a/template/.toys/database/migrations/rollback.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -desc 'Rollback the database N steps' - -optional_arg :step, accept: Integer, default: 1 - -def run - exec_tool 'db:dump' - - file = MigrationFile.find('*', only_one: false)[-1 - step.abs] - - ## https://github.com/dazuma/toys/issues/33 - exec_tool ['db:migrations:run', "--target=#{file.version}"] - - puts "Rolled back to #{file.basename}" -end diff --git a/template/.toys/database/migrations/run.rb b/template/.toys/database/migrations/run.rb deleted file mode 100644 index e4c549d..0000000 --- a/template/.toys/database/migrations/run.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -desc 'Run migrations' - -flag :target, '-t', '--target=VERSION' -flag :current, '-c', '--current=VERSION', default: 'current' -flag :force, '-f', '--force', desc: 'Allow missing migration files' - -def run - exec_tool 'db:dump' unless ENV['SKIP_DB_DUMP'] - - ## https://github.com/jeremyevans/sequel/issues/1182#issuecomment-217696754 - require 'sequel' - %i[migration inflector].each { |extension| Sequel.extension extension } - - require_application_config - - Sequel::Migrator.run db_connection, DB_MIGRATIONS_DIR, options -end - -private - -def options - result = { allow_missing_migration_files: force } - - return result.merge! target_options if target - - puts 'Migrating to latest' - result -end - -def target_options - target_version = - if target == '0' - puts 'Migrating all the way down' - target - else - find_target_file_version - end - - { current: current.to_i, target: target_version.to_i } -end - -def find_target_file_version - file = MigrationFile.find target, disabled: false - - abort 'Migration with this version not found' if file.nil? - - puts "Migrating from #{current} to #{file.basename}" - file.version -end diff --git a/template/.toys/database/psql.rb b/template/.toys/database/psql.rb deleted file mode 100644 index 819b28b..0000000 --- a/template/.toys/database/psql.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -desc 'Start psql' - -def run - update_pgpass - sh "psql #{db_access} #{db_config[:database]}" -end diff --git a/template/.toys/deploy.rb b/template/.toys/deploy.rb deleted file mode 100644 index 36a9087..0000000 --- a/template/.toys/deploy.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -desc 'Deploy to production server' - -long_desc <<~DESC - Command for deploy code from git to server - - Example: - Update from git master branch - toys deploy - Update from git development branch - toys deploy development -DESC - -optional_arg :branch, default: :master - -def run - require 'yaml' - - servers = YAML.load_file File.join(ROOT_DIR, 'config', 'deploy.yml') - - servers.each do |server| - update_command = "cd #{server[:path]} && exe/update.sh #{branch}" - sh "ssh -t #{server[:ssh]} 'bash --login -c \"#{update_command}\"'" - end -end diff --git a/template/.toys/generate/.preload/base_generator.rb b/template/.toys/generate/.preload/base_generator.rb deleted file mode 100644 index 926de23..0000000 --- a/template/.toys/generate/.preload/base_generator.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -require 'gorilla_patch/inflections' - -## Class for common code of generation -class BaseGenerator - class << self - def template(type) - (@templates ||= {})[type] ||= - ERB.new File.read "#{__dir__}/../#{type}/template.rb.erb" - end - end - - attr_reader :camelized_name - - using GorillaPatch::Inflections.from_sequel - - def initialize(type, name) - @template = self.class.template(type) - @camelized_name = name.camelize - @relative_file_path = "#{type.to_s.pluralize}/#{name}.rb" - @file_path = File.join ROOT_DIR, @relative_file_path - - return unless File.exist?(@file_path) - - raise ArgumentError, "File #{name} already exists" - end - - def write(**locals) - FileUtils.mkdir_p File.dirname @file_path - File.write @file_path, @template.result_with_hash( - camelized_name: camelized_name, - **locals - ) - - puts "#{@relative_file_path} created" - end -end diff --git a/template/.toys/generate/form.rb b/template/.toys/generate/form.rb deleted file mode 100644 index 4dfde43..0000000 --- a/template/.toys/generate/form.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -desc 'Generate form' - -required_arg :name - -def run - generator = BaseGenerator.new(:form, name) - - *modules, class_name = generator.camelized_name.split('::') - - generator.write( - modules: modules, - class_name: class_name, - class_indentation: "\t" * modules.size, - parent_form: model_forms.include?(class_name) ? class_name : 'Base' - ) -end - -private - -using GorillaPatch::Inflections - -def model_forms - Dir["#{ROOT_DIR}/forms/_model/*.rb"].map do |file| - File.basename(file, '.rb').sub(/^_/, '').camelize - end -end diff --git a/template/.toys/generate/form/template.rb.erb.erb b/template/.toys/generate/form/template.rb.erb.erb deleted file mode 100644 index 8b001f1..0000000 --- a/template/.toys/generate/form/template.rb.erb.erb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module <%= @module_name %> - module Forms\<\% - modules.map.with_index do |module_name, index| - \%\> - \<\%= "\t" * index %>module <\%= module_name - \%\>\<\% - end - \%\> - \<\%= - class_indentation - \%\>class \<\%= class_name \%\> < Forms::Model::\<\%= parent_form \%\> - \<\%= - class_indentation - \%\>end\<\% - modules.map.with_index do |_module_name, index| - reverse_index = modules.size - index.next - \%\> - \<\%= "\t" * reverse_index \%\>end\<\% - end -\%\> - end -end diff --git a/template/.toys/generate/model.rb b/template/.toys/generate/model.rb deleted file mode 100644 index 163060d..0000000 --- a/template/.toys/generate/model.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -desc 'Generate model' - -required_arg :name - -def run - BaseGenerator.new(:model, name).write -end diff --git a/template/.toys/generate/model/template.rb.erb.erb b/template/.toys/generate/model/template.rb.erb.erb deleted file mode 100644 index e8a8fef..0000000 --- a/template/.toys/generate/model/template.rb.erb.erb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -module <%= @module_name %> - module Models - class \<\%\= camelized_name \%\> < Sequel::Model - end - end -end diff --git a/template/.toys/locales/.preload/crowdin.rb b/template/.toys/locales/.preload/crowdin.rb deleted file mode 100644 index ad71415..0000000 --- a/template/.toys/locales/.preload/crowdin.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -def crowdin(command, branch) - crowdin_config_file = File.join('config', 'crowdin.yml') - - branch ||= `git branch | grep -e "^*" | cut -d' ' -f 2`.strip - - sh "crowdin --config #{crowdin_config_file} -b #{branch.tr('/', '_')} " + - command -end diff --git a/template/.toys/locales/.preload/locale.rb b/template/.toys/locales/.preload/locale.rb deleted file mode 100644 index c2d7dd9..0000000 --- a/template/.toys/locales/.preload/locale.rb +++ /dev/null @@ -1,68 +0,0 @@ -# frozen_string_literal: true - -require 'yaml' - -## Class for Locale file -class Locale - attr_reader :code, :hash - - EXT = '.yml' - - def self.load - Dir[File.join(ROOT_DIR, 'locales', "*#{EXT}")].map do |file| - new File.basename(file, EXT), YAML.load_file(file) - end - end - - def initialize(code, hash) - @code = code - @hash = hash - end - - ## Compares generic values and returns rich diff - class HashCompare - def initialize(hash, other_hash) - @hash = hash - @other_hash = other_hash - end - - def different_keys - @hash.each_with_object({}) do |(key, value), result| - difference = generic_difference(value, @other_hash[key]) - next if difference.nil? || difference.empty? - - result[key] = difference - end - end - - private - - def generic_difference(value, other_value) - return value if value.class != other_value.class - - case value - when Hash - self.class.new(value, other_value).different_keys - when Array - differences_in_array(value, other_value) - when nil - value - end - end - - def differences_in_array(array, other_array) - return array if array.size != other_array.size - - array.zip(other_array).map do |object, other_object| - next if !object.is_a?(Hash) || !other_object.is_a?(Hash) - - difference = self.class.new(object, other_object).different_keys - difference unless difference.empty? - end.compact - end - end - - def diff(other) - HashCompare.new(hash, other.hash).different_keys - end -end diff --git a/template/.toys/locales/check.rb b/template/.toys/locales/check.rb deleted file mode 100644 index e6613e7..0000000 --- a/template/.toys/locales/check.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -desc 'Check locales' - -def run - require 'yaml' - require 'json' - - locales = Locale.load - - locales.each_with_index do |locale, ind| - locales[ind..-1].each do |other_locale| - next if locale == other_locale - - compare_locales(locale, other_locale) - compare_locales(other_locale, locale) - end - end -end - -private - -def compare_locales(locale, other_locale) - puts "#{locale.code.upcase} -> #{other_locale.code.upcase}:\n\n" - puts locale.diff(other_locale).to_yaml -end diff --git a/template/.toys/locales/download.rb b/template/.toys/locales/download.rb deleted file mode 100644 index db8f7fc..0000000 --- a/template/.toys/locales/download.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -desc 'Download files for translation' - -optional_arg :branch - -def run - crowdin 'download translations', branch -end diff --git a/template/.toys/locales/lint.rb b/template/.toys/locales/lint.rb deleted file mode 100644 index 4ed2806..0000000 --- a/template/.toys/locales/lint.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -desc 'Lint locales files' - -def run - require 'yaml' - - Dir["#{ROOT_DIR}/locales/*.y{a,}ml"].each do |locale_file| - ## `YAML.safe_load` doesn't work with anchors - # rubocop:disable Security/YAMLLoad - next if locale_file.end_with?('zh-CN.yml') - - locale = YAML.load File.read locale_file - # rubocop:enable Security/YAMLLoad - errors = %w[notice warning error].reduce([]) do |result, type| - result.concat deep_lint_periods locale[type] - end - errors.map! { |error| "* `#{error}`" } - raise "There is no period in:\n#{errors.join("\n")}" if errors.any? - end -end - -private - -def deep_lint_periods(value, errors = []) - if value.is_a?(Hash) - value.each_value { |nested_value| send __method__, nested_value, errors } - else - errors << value unless value.end_with?('.') # chinese dot for Chinese - end - errors -end diff --git a/template/.toys/locales/upload.rb b/template/.toys/locales/upload.rb deleted file mode 100644 index 7ac6a69..0000000 --- a/template/.toys/locales/upload.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -desc 'Upload files for translation' - -optional_arg :branch - -def run - crowdin 'upload sources', branch -end diff --git a/template/.toys/routes.rb.erb b/template/.toys/routes.rb.erb deleted file mode 100644 index 05cf9a3..0000000 --- a/template/.toys/routes.rb.erb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -desc 'Print application routes' - -def run - require 'rack' - ENV['RACK_ENV'] ||= 'development' - - ## Properly loads environment - Rack::Builder.parse_file File.join(ROOT_DIR, 'config.ru') - - puts <%= @short_module_name %>::Application.router.routes -end diff --git a/template/.toys/static.rb b/template/.toys/static.rb deleted file mode 100644 index 4a12c09..0000000 --- a/template/.toys/static.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -tool :check do - desc 'Check static files' - - GREP_OPTIONS = '--exclude-dir={\.git,log,node_modules,extra} --color=always' - - def run - Dir[File.join(ROOT_DIR, 'public', '**', '*')].each do |file| - next if File.directory? file - - basename = File.basename(file) - - puts "Looking for #{basename}..." - - found = `grep -ir '#{basename}' #{ROOT_DIR} #{GREP_OPTIONS}` - - next unless found.empty? && File.dirname(file) != @skipping_dir - - ask_question file - end - end - - private - - def ask_question(file) - filename = file.sub(ROOT_DIR, '') - case Question.new("Delete #{filename} ?", %w[yes no skip]).answer - when 'yes' - `git rm #{file.gsub(' ', '\ ')}` - when 'skip' - @skipping_dir = File.dirname(file) - end - end -end diff --git a/template/Gemfile b/template/Gemfile index b1611c5..8a01804 100644 --- a/template/Gemfile +++ b/template/Gemfile @@ -6,27 +6,29 @@ group :system do gem 'bundler' gem 'gorilla_patch' # gem 'httpx' - gem 'memery' + gem 'alt_memery' gem 'pry-byebug' # gem 'rubyzip' end group :server do - gem 'flame', github: 'AlexWayfer/flame' + # gem 'flame', github: 'AlexWayfer/flame' + gem 'flame', path: '~/Projects/ruby/flame' gem 'flame-flash', github: 'AlexWayfer/flame-flash' gem 'puma' gem 'rack-contrib' + gem 'rack_csrf', require: 'rack/csrf' ## https://github.com/mwpastore/rack-protection-maximum_cookie/pull/4 gem 'rack-protection-maximum_cookie', github: 'AlexWayfer/rack-protection-maximum_cookie', branch: 'update_rack_dependency' gem 'rack-slashenforce' gem 'rack-utf8_sanitizer' - gem 'rack_csrf', require: 'rack/csrf' end group :development do gem 'filewatcher' + gem 'letter_opener' end group :linter do @@ -36,31 +38,34 @@ group :linter do gem 'rubocop-rspec' end -# group :database do -# gem 'pg' -# gem 'sequel' -# gem 'sequel-enum_values', require: 'sequel/plugins/enum_values' -# gem 'sequel_pg', require: 'sequel' -# gem 'sequel_secure_password' -# end +group :database do + gem 'pg' + gem 'sequel' + gem 'sequel-enum_values', require: 'sequel/plugins/enum_values' + gem 'sequel_pg', require: 'sequel' + gem 'sequel_secure_password' +end -# group :translations do -# gem 'flame-r18n', github: 'AlexWayfer/flame-r18n' -# gem 'r18n-core', github: 'r18n/r18n' -# end +group :translations do + gem 'flame-r18n', github: 'AlexWayfer/flame-r18n' + gem 'r18n-core', github: 'r18n/r18n' +end -# group :forms do -# gem 'formalism', github: 'AlexWayfer/formalism' -# end +group :forms do + gem 'formalism' + gem 'formalism-model_forms' + gem 'formalism-r18n_errors' + gem 'formalism-sequel_transactions' +end group :views do gem 'erubi', require: 'tilt/erubi' end -# group :mails do -# gem 'email_address' -# gem 'mail' -# end +group :mails do + gem 'email_address' + gem 'mail' +end # group :test do # gem 'database_cleaner' @@ -72,6 +77,7 @@ end group :others do # gem 'faker' + gem 'flame-raven_context' # gem 'kramdown' # gem 'retries', # gem 'money-oxr', @@ -79,25 +85,25 @@ group :others do # github: 'AlexWayfer/money-oxr', branch: 'add_flock_for_cache' # ## https://github.com/ooyala/retries/pull/9 # github: 'AlexWayfer/retries', branch: 'v2' - # gem 'sentry-raven' - # gem 'shrine' + gem 'shrine' # gem 'tzinfo' # gem 'tzinfo-data' ## https://github.com/biola/Voight-Kampff/pull/38 - # gem 'voight_kampff', github: 'AlexWayfer/Voight-Kampff', branch: 'v2' + gem 'voight_kampff', github: 'AlexWayfer/Voight-Kampff', branch: 'v2' end group :toys do - gem 'benchmark_toys_template', path: '~/Projects/ruby/benchmark_toys_template' - gem 'diffy' - gem 'paint' - gem 'pry' - gem 'pry-doc' - gem 'rack-console' gem 'toys' -end -# group :benchmark do -# gem 'benchmark-ips' -# gem 'benchmark-memory' -# end + gem 'benchmark_toys' + gem 'config_toys' + gem 'flame_deploy_toys' + gem 'flame_generate_toys' + gem 'flame_routes_toys' + gem 'flame_server_toys' + gem 'locales_toys' + gem 'psql_toys' + gem 'rack_console_toys' + gem 'sequel_migrations_toys' + gem 'static_files_toys' +end diff --git a/template/README.md.erb b/template/README.md.erb index 6be4c89..ff8ef13 100644 --- a/template/README.md.erb +++ b/template/README.md.erb @@ -2,30 +2,25 @@ ## Tech stack -* [**Flame** framework](https://github.com/AlexWayfer/flame) -* [**Sequel** ORM](https://sequel.jeremyevans.net/) -* [**Puma** web-server](https://puma.io/) +* [**Flame** web framework](https://github.com/AlexWayfer/flame) +* [**Sequel** ORM for relational databases](https://sequel.jeremyevans.net/) +* [**Puma** web server](https://puma.io/) ----- ## Deployment -1. Install PostgreSQL version `11` +1. Install PostgreSQL version `12`. 2. Database setup - 1. Create project user: + 1. Create a project user: `createuser -U postgres <%= @app_name %>` (with `-P` for network-open databases) - 2. Create project database: - `createdb -U postgres <%= @app_name %> -O <%= @app_name %>` - 3. Enable required extensions (like `citext` or `pgcrypto`), like: - `psql -U postgres -c "CREATE EXTENSION citext" <%= @app_name %>` -3. Install [`rbenv`](https://github.com/rbenv/rbenv) -4. Install [`nodenv`](https://github.com/nodenv/nodenv) -5. Install [Yarn version 1](https://classic.yarnpkg.com/) -6. Clone this repository and checkout to directory -7. Set the [`EDITOR` environment variable][1] (`nano`, `vim`, `mcedit`, etc.) -8. Run `exe/setup.sh` to install Ruby (with gems), Node (with modules), - fill configs and run database migrations +3. Install [`rbenv`](https://github.com/rbenv/rbenv). +4. Install [`nodenv`](https://github.com/nodenv/nodenv). +5. Clone this repository and checkout to directory. +6. Set the [`EDITOR` environment variable][1] (`nano`, `vim`, `mcedit`, etc.). +7. Run `exe/setup.sh` to install Ruby (with gems), Node (with modules), + fill configs, create database (if needed) and run database migrations. [1]: https://en.wikibooks.org/wiki/Guide_to_Unix/Environment_Variables#EDITOR @@ -47,10 +42,7 @@ ### Server management -For management server state use `./server` tool (it has usage help). - -`./server devel` is useful on development environment, because application -is restarting on code (or configs) changes. +For management server state use `toys server` command. ### Ruby console @@ -74,12 +66,26 @@ toys psql ## Database migrations +### Create migration + +```shell +toys db migrations new migration_name +``` + +### List migrations + +```shell +toys db migrations +# toys db migrations list +``` + ### Run migrations To latest: ```shell toys db migrate +# toys db migrations run ``` To specific version (forward or backward): @@ -88,10 +94,12 @@ To specific version (forward or backward): toys db migrate --target=part_of_target_migration_name_or_version ``` -### Create migration +### Rollback migrations + +`N` is a number of migrations to rollback relatively to the latest existing. ```shell -toys db migrations new migration_name +toys db migrations rollback N ``` ----- @@ -104,8 +112,8 @@ toys db migrations new migration_name exe/update.sh ``` -It will update `master` branch, update `bundle`, -stop `server`, run `migrations` and start `server`. +It will update `master` branch, update bundle, stop server, run migrations +and start server. ### Remotely @@ -114,4 +122,4 @@ toys deploy ``` It will run [`exe/update.sh` command](#locally) remotely -through `ssh` connection from `deploy.yml` configuration file. +through `ssh` connection from `deploy.yaml` configuration file. diff --git a/template/application.rb.erb b/template/application.rb.erb index e4842f1..e5afc3c 100644 --- a/template/application.rb.erb +++ b/template/application.rb.erb @@ -1,5 +1,36 @@ # frozen_string_literal: true +require_relative 'config/base' +config = <%= @module_name %>::Config::Base.new + +## Require gems +require 'bundler/setup' +Bundler.require( + :system, :server, :database, + :translations, :forms, :views, :assets, :mails, :others, + config[:environment] +) + +require 'erubi/capture_end' +require 'voight_kampff/rack_request' + +## Require libs + +### For $RS constant: +### http://ruby-doc.org/stdlib/libdoc/English/rdoc/English.html +require 'English' + +require 'logger' + +### For PP.pp (differs from `pp` by arguments (width)) +require 'pp' + +# require 'money/bank/google_currency' + +## Load full application config, with dependencies from Bundler +require_relative 'config/full' +<%= @module_name %>.complete_config config + module <%= @module_name %> ## Class for application class Application < Flame::Application @@ -8,30 +39,40 @@ module <%= @module_name %> memoize def logger ::Logger.new( - if (config[:environment] == 'development') || - ENV['RACK_CONSOLE'] || - (File.basename($PROGRAM_NAME) == 'toys') - $stdout - else - File.join(config[:logs_dir], 'stdout.log') - end + development_or_toys ? $stdout : File.join(config[:stdout_file]) ) end def db_connection - ::Sequel.connect config[:database] - rescue Sequel::DatabaseConnectionError => e - logger.error e.full_message - nil + connection = ::Sequel.connect config[:database] + + Config::Processors::Sequel::EXTENSIONS.each do |extension_name| + connection.extension extension_name + end + + connection.loggers << logger if development_or_toys + + ## Freeze DB (not for `toys console`) + connection.freeze unless ENV['RACK_CONSOLE'] + + connection end memoize :db_connection - # memoize def shrine_uploaders - # { - # image: Class.new(::Shrine), - # document: Class.new(::Shrine) - # } - # end + memoize def shrine_uploaders + { + # image: Class.new(::Shrine), + # document: Class.new(::Shrine) + } + end + + private + + memoize def development_or_toys + (config[:environment] == 'development') || (File.basename($PROGRAM_NAME) == 'toys') + end end end end + +<%= @short_module_name %>::Application.config = config diff --git a/template/assets/scripts/.eslintrc.yaml b/template/assets/scripts/.eslintrc.yaml new file mode 100644 index 0000000..4b47c3e --- /dev/null +++ b/template/assets/scripts/.eslintrc.yaml @@ -0,0 +1,13 @@ +extends: '../../.eslintrc.yaml' +env: + es6: true +parserOptions: + sourceType: module +# globals: + ## Libs: + # google: true + # Cccombo: true + +rules: + no-var: + - error diff --git a/template/assets/scripts/.keep b/template/assets/scripts/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/template/assets/scripts/application.js b/template/assets/scripts/application.js new file mode 100644 index 0000000..dfe2e91 --- /dev/null +++ b/template/assets/scripts/application.js @@ -0,0 +1,78 @@ +import 'core-js/stable/string/includes'; + +// https://github.com/zloirock/core-js/issues/618#issuecomment-522618151 +// https://github.com/zloirock/core-js/issues/619#issuecomment-522624271 +import 'core-js/stable/symbol/iterator'; + +import 'core-js/stable/array/from'; +import 'core-js/stable/object/entries'; +import 'core-js/stable/dom-collections/for-each'; + +export class App { + document_ready() { + // Prevent double form submission + document.querySelectorAll('form').forEach( + form => { + form.addEventListener('submit', event => { + if (form.submitting == true) { + event.preventDefault(); + event.stopImmediatePropagation(); + } else { + form.submitting = true; + } + }); + } + ); + + // Prevent `.` typing in number inputs with step = 1 + document.querySelectorAll('input[type="number"][step="1"]').forEach( + input => input.addEventListener('keypress', event => { + if (event.key == '.') { + event.preventDefault(); + return false; + } + }) + ); + + // `input[type="number"]` should allow only numbers + // https://stackoverflow.com/a/469362/2630849 + const number_input_filter = value => /^-?\d*[.,]?\d*$/.test(value); + document.querySelectorAll('input[type="number"]').forEach(number_input => { + [ + 'input', 'keydown', 'keyup', 'mousedown', 'mouseup', 'select', + 'contextmenu', 'drop' + ].forEach(event_type => { + number_input.addEventListener(event_type, () => { + if (number_input_filter(number_input.value)) { + number_input.oldValue = number_input.value; + number_input.oldSelectionStart = number_input.selectionStart; + number_input.oldSelectionEnd = number_input.selectionEnd; + } else if ( + Object.prototype.hasOwnProperty.call(number_input, 'oldValue') + ) { + number_input.value = number_input.oldValue; + number_input.setSelectionRange( + number_input.oldSelectionStart, number_input.oldSelectionEnd + ); + } + }); + }); + }); + + // `button` is not focused while clicked in macOS and iOS + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus + document.querySelectorAll('button[type="submit"][name]').forEach(button => { + const form = button.form; + const input = form.querySelector(`input[name="${button.name}"][value="${button.value}"]`); + if (input) return; + + button.addEventListener('click', () => { + const input = document.createElement('input'); + input.type = 'hidden'; + input.name = button.name; + input.value = button.value; + form.appendChild(input); + }); + }); + } +} diff --git a/template/assets/styles/.keep b/template/assets/styles/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/template/assets/styles/_colors.scss b/template/assets/styles/_colors.scss new file mode 100644 index 0000000..ea6b05d --- /dev/null +++ b/template/assets/styles/_colors.scss @@ -0,0 +1,21 @@ +$accent_background_color: #f24900; +$accent_text_color: darken($accent_background_color, 15%); + +$footer_background_color: #2a2a2a; +$footer_text_color: white; + +// $tertiary_background_color: #ffffff; + +$danger_background_color: lighten(red, 15%); +$danger_text_color: red; +$error_color: $danger_text_color; +$warning_background_color: orange; +$warning_text_color: #e67e00; // darken(darkorange, 5%); +$success_background_color: #2db92d; // darken(limegreen, 5%); +$success_text_color: green; +$highlight_background_color: #eee; +$disabled_text_color: gray; + +$link_text_color: darken($accent_text_color, 10%); + +$input_border_color: #ccc; diff --git a/template/assets/styles/_fonts.scss b/template/assets/styles/_fonts.scss new file mode 100644 index 0000000..7cb7bb5 --- /dev/null +++ b/template/assets/styles/_fonts.scss @@ -0,0 +1,3 @@ +// Russian Ruble sign somewhy doesn't display on iOS 13. Azerbaijani manat too. + +$main_font_family: -apple-system, BlinkMacSystemFont, sans-serif; diff --git a/template/assets/styles/_sizes.scss b/template/assets/styles/_sizes.scss new file mode 100644 index 0000000..8d8a28d --- /dev/null +++ b/template/assets/styles/_sizes.scss @@ -0,0 +1,15 @@ +$container_max_width: 1200px; + +// $icon_size: 1em; + +$common_border_width: 1px; + +$common_border_radius: 0.5em; + +$input_border_radius: $common_border_radius; +$input_border_width: $common_border_width; +$input_vertical_padding: 0.375em; +$input_padding: $input_vertical_padding 0.75em; +$input_line_height: 1.4em; + +$button_border_radius: $input_border_radius; diff --git a/template/assets/styles/components/_flash.scss b/template/assets/styles/components/_flash.scss new file mode 100644 index 0000000..6efc736 --- /dev/null +++ b/template/assets/styles/components/_flash.scss @@ -0,0 +1,23 @@ +.flash { + margin: 1em 0; + padding: 0 1em; + text-align: center; + font-weight: 600; + line-height: 1.4em; + + &::first-letter { + text-transform: capitalize; + } + + &.notice { + color: black; + } + + &.warning { + color: $warning_text_color; + } + + &.error { + color: $error_color; + } +} diff --git a/template/assets/styles/components/_footer.scss b/template/assets/styles/components/_footer.scss new file mode 100644 index 0000000..546c12f --- /dev/null +++ b/template/assets/styles/components/_footer.scss @@ -0,0 +1,3 @@ +> footer { + background-color: $footer_background_color; +} diff --git a/template/assets/styles/components/_forms.scss b/template/assets/styles/components/_forms.scss new file mode 100644 index 0000000..531a3d4 --- /dev/null +++ b/template/assets/styles/components/_forms.scss @@ -0,0 +1,83 @@ +$form_vertical_label_margin: 0.8em; +$form_vertical_input_margin: 0.4em; + +form { + label { + font-weight: bold; + + > p { + font-weight: normal; + } + } + + input.with_clear_button + button { + margin-bottom: 0; + } + + p.error { + height: 0; + overflow: hidden; + margin: 0; + padding-left: 0.4em; + color: lighten($error_color, 15%); + } + + .invalid { + input { + border: $input_border_width solid lighten($error_color, 15%); + box-shadow: 0 0 3px lighten($error_color, 15%); + } + + & ~ .error { + height: inherit; + margin-top: 0.4em; + } + } + + &.inline { + &, + label { + display: inline-block; + } + } + + &.vertical { + text-align: center; + + label, + ul, + table, + p, + article { + text-align: left; + } + + label, + table { + margin-bottom: $form_vertical_label_margin; + } + + label { + display: block; + // Because of inputs outline + // padding: 5px; + + #{all_horizontal_inputs()}, + textarea, + select, + .cccombo { + display: block; + width: 100%; + margin-top: $form_vertical_input_margin; + } + + > label { + margin-bottom: $form_vertical_input_margin; + + &:first-of-type { + margin-top: $form_vertical_input_margin; + } + } + } + } +} diff --git a/template/assets/styles/components/_header.scss b/template/assets/styles/components/_header.scss new file mode 100644 index 0000000..11e5507 --- /dev/null +++ b/template/assets/styles/components/_header.scss @@ -0,0 +1,15 @@ +$header_logo_height: 66px; +$header_vertical_padding: 15px; +$mobile_header_logo_height: 42px; +$mobile_header_vertical_padding: 7px; + +> header { + background: $accent_background_color; + padding: $mobile_header_vertical_padding 0; + z-index: 7; + + &, + a { + color: black; + } +} diff --git a/template/assets/styles/components/_page.scss b/template/assets/styles/components/_page.scss new file mode 100644 index 0000000..3e9bdfe --- /dev/null +++ b/template/assets/styles/components/_page.scss @@ -0,0 +1,82 @@ +.page { + max-width: 800px; + padding: 20px 15px; + margin: 0 auto; + text-align: justify; + + #{headings()}, + pre { + text-align: left; + } + + &.error { + &, + #{headings()} { + text-align: center; + } + + pre { + #{headings()} { + text-align: left; + } + + padding: 0.8em 1.6em; + max-height: 40em; + overflow: auto; + background: #f5f5f5; + border: $common_border_width solid #ddd; + + .sql { + white-space: normal; + line-height: inherit; + } + } + + /** + * There is a bug in stylelint which marks vw as unsupported when + * only vmax is unsupported + * https://github.com/anandthakker/doiuse/issues/83 + */ + /* stylelint-disable plugin/no-unsupported-browser-features */ + &.development { + max-width: none; + + padding: { + left: 3vw; + right: 3vw; + } + } + /* stylelint-enable plugin/no-unsupported-browser-features */ + } + + table { + border-collapse: collapse; + + td { + border: $common_border_width solid #bbb; + padding: 10px 15px; + } + } + + pre, + code { + background-color: lighten($accent_background_color, 40%); + padding: 0.2em 0.4em; + } + + code { + display: inline-block; + } + + pre { + overflow: auto; + } + + pre code { + padding: 0; + } + + li { + margin: 0.6em 0; + } +} diff --git a/template/assets/styles/lib/_breakpoints.scss b/template/assets/styles/lib/_breakpoints.scss new file mode 100644 index 0000000..5cfa06d --- /dev/null +++ b/template/assets/styles/lib/_breakpoints.scss @@ -0,0 +1,38 @@ +$breakpoints: ( + // Extra small devices (portrait phones, less than 576px) + // No media query since this is the default + // Small devices (landscape phones, 576px and up) + sm: 576px, + // Medium devices (tablets, 768px and up) + md: 768px, + // Large devices (desktops, 992px and up) + lg: 992px, + // Extra large devices (large desktops, 1200px and up) + xl: 1200px +); + +@mixin media-to($breakpoint) { + @media screen and (max-width: map-get($breakpoints, $breakpoint) - 1px) { + @content; + } +} + +@mixin media-from($breakpoint) { + @media screen and (min-width: map-get($breakpoints, $breakpoint)) { + @content; + } +} + +// @each $breakpoint, $size in $breakpoints { +// .show-to-#{$breakpoint} { +// @include media-from($breakpoint) { +// display: none; +// }; +// } +// +// .show-from-#{$breakpoint} { +// @include media-to($breakpoint) { +// display: none; +// }; +// } +// } diff --git a/template/assets/styles/lib/_clear_fix.scss b/template/assets/styles/lib/_clear_fix.scss new file mode 100644 index 0000000..af182f7 --- /dev/null +++ b/template/assets/styles/lib/_clear_fix.scss @@ -0,0 +1,7 @@ +@mixin clear-fix { + &::after { + content: ''; + display: block; + clear: both; + } +} diff --git a/template/assets/styles/lib/_disable_password_autocomplete.scss b/template/assets/styles/lib/_disable_password_autocomplete.scss new file mode 100644 index 0000000..936db8f --- /dev/null +++ b/template/assets/styles/lib/_disable_password_autocomplete.scss @@ -0,0 +1,3 @@ +.disable_password_autocomplete { + display: none; +} diff --git a/template/assets/styles/lib/_headings.scss b/template/assets/styles/lib/_headings.scss new file mode 100644 index 0000000..e97c54b --- /dev/null +++ b/template/assets/styles/lib/_headings.scss @@ -0,0 +1,7 @@ +@function headings($from: 1, $to: 6) { + @if $from == $to { + @return 'h#{$from}'; + } @else { + @return 'h#{$from},' + headings($from + 1, $to); + } +} diff --git a/template/assets/styles/lib/_input_with_clear_button.scss b/template/assets/styles/lib/_input_with_clear_button.scss new file mode 100644 index 0000000..b0c6d77 --- /dev/null +++ b/template/assets/styles/lib/_input_with_clear_button.scss @@ -0,0 +1,36 @@ +$input_with_clear_button_width: 2.4em; + +@mixin padding_for_inputs_with_clear_button { + &.with_clear_button { + padding-right: $input_with_clear_button_width + 0.2em; + + @content; + } +} + +input { + @include padding_for_inputs_with_clear_button { + + button { + $outline_with: 1px; + + position: absolute; + right: $outline_with; + top: $outline_with; + width: $input_with_clear_button_width; + height: calc(100% - #{$outline_with} * 2); + padding: 0.2em; + color: darkgray; + background: none; + border: none; + + &:focus { + background: none; + + outline: { + style: auto; + width: $outline_with; + } + } + } + } +} diff --git a/template/assets/styles/lib/_inputs_with_types.scss b/template/assets/styles/lib/_inputs_with_types.scss new file mode 100644 index 0000000..ea820f5 --- /dev/null +++ b/template/assets/styles/lib/_inputs_with_types.scss @@ -0,0 +1,16 @@ +@function inputs_with_types($types...) { + $result: (); + + @each $type in $types { + $result: append($result, 'input[type="' + $type + '"]', comma); + } + + @return $result; +} + +@function all_horizontal_inputs() { + @return inputs_with_types( + 'text', 'email', 'password', 'number', 'url', 'search', 'date', 'time', + 'file' + ); +} diff --git a/template/assets/styles/lib/_small_and_large_elements.scss b/template/assets/styles/lib/_small_and_large_elements.scss new file mode 100644 index 0000000..6a4dd65 --- /dev/null +++ b/template/assets/styles/lib/_small_and_large_elements.scss @@ -0,0 +1,57 @@ +@mixin small-and-large-elements( + // Probably here should be something like `$large_element_container`, + // for example, for Cccombo input like this: + // https://gitlab.gettransport.com/site/site/merge_requests/80/diffs#28551ac9184a7e08499c6a0b6a17f78126a4defa_124_124 + // Discussion: + // https://gitlab.gettransport.com/site/site/merge_requests/522#note_324681 + $small_element: '> button', $large_element: '> input', + $small_element_on_right: false, $inline: false +) { + display: if($inline, inline-flex, flex); + align-items: stretch; + + #{$large_element}, + #{$small_element} { + border-radius: $input_border_radius; + + // Fix for Safari + margin: { + top: 0; + bottom: 0; + } + + &:focus { + z-index: 2; + } + } + + #{$large_element} { + flex-grow: 2; + + &, + > * { + width: 100%; + } + } + + #{$small_element} { + flex-grow: 0; + } + + // Left element + #{if($small_element_on_right, $large_element, $small_element)} { + margin-right: -$common_border_width; + + border: { + top-right-radius: 0; + bottom-right-radius: 0; + } + } + + #{if($small_element_on_right, $small_element, $large_element)} { + border: { + top-left-radius: 0; + bottom-left-radius: 0; + } + } +} diff --git a/template/assets/styles/lib/_sticky_footer.scss b/template/assets/styles/lib/_sticky_footer.scss new file mode 100644 index 0000000..a872aa5 --- /dev/null +++ b/template/assets/styles/lib/_sticky_footer.scss @@ -0,0 +1,18 @@ +@mixin sticky_footer($main_height: auto) { + @at-root html { + height: 100%; + } + + min-height: 100%; + display: flex; + flex-direction: column; + + > main { + min-height: $main_height; + + flex: { + grow: 1; + shrink: 0; + } + } +} diff --git a/template/assets/styles/main.scss b/template/assets/styles/main.scss new file mode 100644 index 0000000..8e434f6 --- /dev/null +++ b/template/assets/styles/main.scss @@ -0,0 +1,265 @@ +@import 'lib/breakpoints'; +@import 'lib/headings'; +@import 'lib/clear_fix'; +@import 'lib/inputs_with_types'; +@import 'lib/disable_password_autocomplete'; +@import 'lib/sticky_footer'; + +* { + position: relative; + // outline: none; + + &:not(svg) { + box-sizing: border-box; + } +} + +body { + @import 'colors'; + @import 'sizes'; + @import 'fonts'; + + margin: 0; + font-family: $main_font_family; + + .hidden { + display: none !important; + } + + :disabled { + cursor: not-allowed; + } + + .container { + max-width: $container_max_width; + padding: 0 15px; + margin: 0 auto; + } + + a { + text-decoration: none; + color: $link_text_color; + + &:hover { + text-decoration: underline; + } + + &[href^="mailto:"] { + white-space: nowrap; + } + } + + label, + a { + &.button { + @extend button; + + display: inline-block; + } + } + + // .icon { + // &.baseline { + // // https://blog.prototypr.io/align-svg-icons-to-text-and-say-goodbye-to-font-icons-d44b3d7b26b4#6e0c + // bottom: -0.125em; + // } + // } + + @mixin custom-background-color($background-color) { + background: $background-color; + border-color: $background-color; + + &:focus { + $focus_color: darken($background-color, 10%); + + background: $focus_color; + border-color: $focus_color; + } + + &:disabled { + $disabled_color: mix($background-color, #ddd); + + background: $disabled_color; + border-color: $disabled_color; + } + } + + @import 'lib/small_and_large_elements'; + @import 'lib/input_with_clear_button'; + + // https://stackoverflow.com/a/23211766/2630849 + #{inputs_with_types('text', 'email', 'password', 'number', 'url', 'search')}, + textarea { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + } + + #{all_horizontal_inputs()}, + textarea, + select, + .cccombo > button { + &:disabled { + color: #333; + background: #f5f5f5; + } + } + + #{all_horizontal_inputs()}, + textarea, + select, + button { + // Fix for Safari :see_no_evil: + margin: 0; + padding: $input_padding; + + font: { + size: inherit; + family: inherit; + } + + line-height: $input_line_height; + color: black; + background: white; + border: $input_border_width solid $input_border_color; + border-radius: $input_border_radius; + } + + textarea { + border-radius: $common_border_radius; + } + + select { + $svg_size: 24; + + appearance: none; + + background: { + image: url( + "data:image/svg+xml;utf8," + ); + repeat: no-repeat; + position-x: 100%; + position-y: center; + } + + padding-right: #{$svg_size}px; + + white-space: normal; + } + + @mixin primary { + @include custom-background-color($accent_background_color); + + color: white; + } + + @mixin warning { + @include custom-background-color($warning_background_color); + + color: white; + } + + @mixin danger { + @include custom-background-color($danger_background_color); + + color: white; + } + + @mixin success { + @include custom-background-color($success_background_color); + + color: white; + } + + @mixin secondary { + @include custom-background-color($footer_background_color); + + color: white; + } + + // @mixin tertiary { + // @include custom-background-color($tertiary_background_color); + // } + + @mixin light { + @include custom-background-color(white); + + color: $accent_text_color; + border-color: $accent_background_color; + + &:focus { + border-color: $accent_background_color; + } + } + + button { + @include custom-background-color(white); + + border-color: $input_border_color; + border-radius: $button_border_radius; + + &.small { + font-size: 0.875em; + padding: 0.2em 0.7em; + } + + &.large { + font: { + size: 115%; + weight: bold; + } + + text-transform: uppercase; + padding: 0.5em 0.8em; + } + + &.primary { + @include primary; + } + + &.warning { + @include warning; + } + + &.danger { + @include danger; + } + + &.success { + @include success; + } + + &.secondary { + @include secondary; + } + + // &.tertiary { + // @include tertiary; + // } + + &.light { + @include light; + } + } + + .cccombo > button { + border-radius: $input_border_radius; + } + + #{all_horizontal_inputs()} { + @include padding_for_inputs_with_clear_button { + + button:focus { + outline-color: darken($highlight_background_color, 15%); + } + } + } + + @import 'components/header'; + @import 'components/footer'; + @import 'components/flash'; + @import 'components/forms'; + @import 'components/page'; + + @include sticky_footer; +} diff --git a/template/benchmark/.rubocop.yml b/template/benchmark/.rubocop.yml index 97f8f6b..0be8f4b 100644 --- a/template/benchmark/.rubocop.yml +++ b/template/benchmark/.rubocop.yml @@ -1,17 +1,13 @@ +inherit_from: ../.rubocop.yml + +Layout/IndentationStyle: + EnforcedStyle: spaces + IndentationWidth: ~ +Layout/IndentationWidth: + Width: 2 Layout/EmptyLines: Enabled: false Layout/EmptyLineBetweenDefs: Enabled: false Metrics/MethodLength: Enabled: false - -Lint/RaiseException: - Enabled: true -Lint/StructNewOverride: - Enabled: true -Style/HashEachMethods: - Enabled: true -Style/HashTransformKeys: - Enabled: true -Style/HashTransformValues: - Enabled: true diff --git a/template/benchmark/main.example.rb b/template/benchmark/main.example.rb index cba9f4b..fba27c5 100644 --- a/template/benchmark/main.example.rb +++ b/template/benchmark/main.example.rb @@ -21,9 +21,9 @@ require 'memery' require 'gorilla_patch' -require 'sequel' +# require 'sequel' # require 'shrine' -require 'r18n-core' +# require 'r18n-core' # require 'money' # require_relative 'config/config' diff --git a/template/config.ru.erb b/template/config.ru.erb index f332676..0ac4bd8 100644 --- a/template/config.ru.erb +++ b/template/config.ru.erb @@ -1,38 +1,10 @@ # frozen_string_literal: true -require_relative 'constants' - -environment = ENV['RACK_ENV'].to_sym - -## Require gems -require 'bundler/setup' -Bundler.require( - :system, :server, :database, - :translations, :forms, :views, :assets, :mails, :others, - environment -) - -require 'erubi/capture_end' -# require 'voight_kampff/rack_request' - -## Require libs - -### For $RS constant: -### http://ruby-doc.org/stdlib/libdoc/English/rdoc/English.html -require 'English' - -require 'logger' - -### For PP.pp (differs from `pp` by arguments (width)) -require 'pp' - -# require 'money/bank/google_currency' - ## Require application require_relative 'application' ## Require dirs -<%= @short_module_name %>::Application.require_dirs <%= @short_module_name %>::APP_DIRS, ignore: [%r{lib/\w+/spec/}] +<%= @short_module_name %>::Application.require_dirs <%= @short_module_name %>::APP_DIRS, ignore: [%r{config/puma.rb}, %r{lib/\w+/spec/}] ## Require routes require_relative 'routes' @@ -50,7 +22,8 @@ use Rack::CommonLogger, <%= @short_module_name %>::Application.logger ## Aliases for rack-console if ENV['RACK_CONSOLE'] - # <%= @short_module_name %>::Acc = <%= @short_module_name %>::Account + <%= @short_module_name %>::App = <%= @short_module_name %>::Application + ## Some models, for example end ## Remove invalid UTF-8 characters from requests @@ -60,7 +33,7 @@ use Rack::UTF8Sanitizer # use Rack::RemoveTrailingSlashes ## Parse body as pointed out in Content-type -use Rack::PostBodyContentTypeParser +use Rack::JSONBodyParser ## CSRF ## Rescued and reported by `lowlevel_error_handler` in Puma config diff --git a/template/config/base.rb.erb b/template/config/base.rb.erb new file mode 100644 index 0000000..4972030 --- /dev/null +++ b/template/config/base.rb.erb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +require 'yaml' + +module <%= @module_name %> + ::<%= @short_module_name %> = ::<%= @module_name %> + + APP_DIRS = [ + 'lib', + 'config', + 'models', + # 'policies', + 'helpers', + 'mailers', + # 'actions', + 'forms', + # 'view_objects', + 'controllers' + ].freeze + + module Config + ## Class for config like a Hash with helper methods. + ## It was a part of the Flame, but some places, like Puma config, + ## need for this without Bundler (dependecies): + ## https://github.com/puma/puma/issues/2319 + class Base < Hash + ## Create an instance of application config + def initialize + populate_dirs + + self[:stdout_file] = "#{self[:logs_dir]}/out" + self[:stderr_file] = "#{self[:logs_dir]}/err" + + require_relative 'processors/server' + Processors::Server.new self + + %i[session site].each do |config_name| + load_yaml config_name, required: true + end + end + + ## Method for loading YAML-files from config directory + ## @param name [Symbol] + ## file base name (extension is `.yml` or '.yaml') + ## @example Load SMTP file without extension, by Symbol + ## config.load_yaml(:smtp) + def load_yaml(name, required: false) + file_name = "#{name}.y{a,}ml" + + file_path = find_config_file file_name, required: required + + return unless file_path + + self[name] = YAML.load_file(file_path) + end + + private + + def populate_dirs + self[:root_dir] = File.realpath "#{__dir__}/.." + + %i[config logs public tmp views].each do |dir_name| + self[:"#{dir_name}_dir"] = "#{self[:root_dir]}/#{dir_name}" + end + + self[:pids_dir] = "#{self[:tmp_dir]}/pids" + end + + def find_config_file(file_name, required:) + file_path = nil + + loop do + file_path = Dir[File.join(self[:config_dir], file_name)].first + break if file_path + + config_relative_dir = self[:config_dir].sub(self[:root_dir], '') + puts "Config file '#{file_name}' not found in '#{config_relative_dir}'" + + next if ask_to_check_config_files + + required ? abort : break + end + + file_path + end + + def ask_to_check_config_files + highline.choose do |menu| + menu.layout = :one_line + + menu.prompt = 'Do you want to check config files? ' + + menu.choice(:yes) do + system 'toys config check' + true + end + + menu.choice(:no) { false } + end + end + + def highline + @highline ||= begin + require 'highline' + HighLine.new + end + end + end + end +end diff --git a/template/config/config.rb.erb b/template/config/config.rb.erb deleted file mode 100644 index 4d6b56d..0000000 --- a/template/config/config.rb.erb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -require_relative '../application' - -## Threads (for mails) -Thread.abort_on_exception = true - -using GorillaPatch::Inflections - -<%= @short_module_name %>::Application.config.instance_exec do - %i[server session site].each do |config_name| - load_yaml config_name - end - - [ - 'Logger' - # 'R18n' - # 'Mail' - # 'Sequel' - # 'Money' - # 'Shrine' - ].each do |processor_name| - require_relative "processors/#{processor_name.underscore}" - <%= @short_module_name %>::ConfigProcessors.const_get(processor_name).new self - end -end diff --git a/template/config/database.example.yaml b/template/config/database.example.yaml deleted file mode 100644 index 6fa33a4..0000000 --- a/template/config/database.example.yaml +++ /dev/null @@ -1,5 +0,0 @@ -:adapter: 'postgres' -:host: 'localhost' -:database: 'database' -:user: 'user' -# :password: 'password' diff --git a/template/config/database.example.yaml.erb b/template/config/database.example.yaml.erb new file mode 100644 index 0000000..08ccefb --- /dev/null +++ b/template/config/database.example.yaml.erb @@ -0,0 +1,23 @@ +default: &default + :adapter: 'postgres' + # :host: 'localhost' + :database: '<%= @app_name %>' + :user: '<%= @app_name %>' + # :password: 'password' + :superuser: 'postgres' + + # :servers: + # :replica: + # :host: 'localhost' + # :database: 'replica' + # :user: 'replicauser' + # :password: 'replicapassword' + +production: + <<: *default + +demo: + <<: *default + +development: + <<: *default diff --git a/template/config/full.rb.erb b/template/config/full.rb.erb new file mode 100644 index 0000000..d8cace6 --- /dev/null +++ b/template/config/full.rb.erb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +## Config split into `base` and `full` because of `prune_bundler`: +## https://github.com/puma/puma/issues/2319 +module <%= @module_name %> + class << self + using GorillaPatch::Inflections + + def complete_config(config) + %w[Sentry R18n Mail Sequel Shrine].each do |processor_name| + require_relative "processors/#{processor_name.underscore}" + <%= @short_module_name %>::Config::Processors.const_get(processor_name).new config + end + end + end +end diff --git a/template/config/mail.example.yaml.erb b/template/config/mail.example.yaml.erb new file mode 100644 index 0000000..4524f61 --- /dev/null +++ b/template/config/mail.example.yaml.erb @@ -0,0 +1,23 @@ +:from: + :name: '<%= @module_name %>.com' + :email: 'info@<%= @domain_name %>.com' + +:site_url: + :scheme: http + :host: localhost + :port: 3000 + +## Remove `_` in the beginning of key for enabling +## If nothing enabled letter_opener would be used + +:_smtp: + :address: 'smtp.gmail.com' + :port: 587 + :user_name: 'info@<%= @domain_name %>.com' + :password: 'your_password' + :authentication: 'plain' + :enable_starttls_auto: true + :content_type: 'text/html; charset=UTF-8' + +:_send_grid: + :api_key: 'your_api_key' diff --git a/template/config/processors/logger.rb.erb b/template/config/processors/logger.rb.erb deleted file mode 100644 index ee89d99..0000000 --- a/template/config/processors/logger.rb.erb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -## Logger configuration and methods -module <%= @module_name %> - module ConfigProcessors - ## Logger configuration - class Logger - def initialize(config) - @config = config - - @config[:logs_dir] = proc do - File.join( - self[:root_dir], self[:server][self[:environment]][:logs_dir] - ) - end - end - end - end -end diff --git a/template/config/processors/mail.rb.erb b/template/config/processors/mail.rb.erb new file mode 100644 index 0000000..9ad8ba4 --- /dev/null +++ b/template/config/processors/mail.rb.erb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'mail' + +require_relative '../../mailers/_base' + +module <%= @module_name %> + module Config + module Processors + ## Configuration for mails + class Mail + def initialize(config) + @config = config + + ## Threads (for mails) + Thread.abort_on_exception = true + + @mail_config = @config.load_yaml :mail + return unless @mail_config + + @config[:mails_dir] = proc { File.join self[:views_dir], 'mails' } + + validate_site_url + + configure_appropriate_provider + end + + private + + def configure_appropriate_provider + if @mail_config[:send_grid] + configure_send_grid + elsif @mail_config[:smtp] + configure_smtp + elsif @config[:environment] != 'test' + configure_letter_opener + end + end + + def configure_send_grid + require_relative '../../mailers/mail/send_grid' + + Mailers::Mail.activated = Mailers::Mail::SendGrid + + Mailers::Mail::SendGrid.client = SendGrid::API.new( + api_key: @mail_config[:send_grid][:api_key], + host: 'https://api.sendgrid.com' + ).client + end + + def configure_smtp + activate_default_mail + + smtp_config = @mail_config[:smtp] + + ::Mail.defaults do + delivery_method :smtp, smtp_config if smtp_config + end + end + + def configure_letter_opener + require 'letter_opener' + + activate_default_mail + + letter_opener_dir = File.join @config[:tmp_dir], 'letter_opener' + + ::Mail.defaults do + delivery_method LetterOpener::DeliveryMethod, location: letter_opener_dir + end + end + + def activate_default_mail + require_relative '../../mailers/mail/default' + + Mailers::Mail.activated = Mailers::Mail::Default + end + + def validate_site_url + site_url = @mail_config[:site_url] + + unless Rack::Request::DEFAULT_PORTS.key?(site_url[:scheme]) + raise 'Incorrect scheme of site URL in mail config' + end + + return if site_url[:host] + + raise 'Host of site URL in mail config is required' + end + end + end + end +end diff --git a/template/config/processors/r18n.rb.erb b/template/config/processors/r18n.rb.erb new file mode 100644 index 0000000..b055f9d --- /dev/null +++ b/template/config/processors/r18n.rb.erb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'flame/r18n' + +module <%= @module_name %> + module Config + module Processors + ## Configuration for R18n + class R18n + def initialize(config) + config[:locales_dir] = File.join config[:root_dir], 'locales' + + ::R18n.default_places = config[:locales_dir] + + ::R18n::I18n.default = 'en-US' + + ::R18n::Filters.on(:named_variables) + + ::R18n::Filters.add(::R18n::Untranslated, :raise_untranslated) do |content| + request_context = Flame::RavenContext.new(:translations, key: content) + Raven.capture_message(*request_context.exception_with_context) + content + end + end + end + end + end +end diff --git a/template/config/processors/sentry.rb.erb b/template/config/processors/sentry.rb.erb new file mode 100644 index 0000000..4948fe9 --- /dev/null +++ b/template/config/processors/sentry.rb.erb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'raven' + +module <%= @module_name %> + module Config + module Processors + ## Configuration for Sentry + class Sentry + STATIC_CONFIG = { + environments: %w[production demo].freeze, + ## The default timeouts are too short on slow networks. + timeout: 10, + open_timeout: 10, + app_dirs_pattern: + <%= @short_module_name %>::APP_DIRS + .map { |app_dir| Regexp.escape(app_dir) } + .join('|') + .yield_self { |regexp_template| /(#{regexp_template})/ } + .freeze + }.freeze + + def initialize(config) + @config = config + + @sentry_config = @config.load_yaml :sentry + return unless @sentry_config + + @sentry_config['front-end'][:url] = sentry_url project: 'front-end' + + configure_raven + end + + private + + def sentry_url(project:) + uri = URI::HTTPS.build( + userinfo: @sentry_config[project][:api_key], host: @sentry_config[:host] + ) + uri.merge!(@sentry_config[project][:project_id].to_s) + uri.to_s + end + + def configure_raven + Raven.configure do |raven_config| + STATIC_CONFIG.each { |field, value| raven_config.public_send("#{field}=", value) } + + raven_config.current_environment = @config[:environment] + raven_config.dsn = sentry_url project: 'back-end' + raven_config.processors -= [ + Raven::Processor::Cookies, + Raven::Processor::PostData + ] + end + end + end + end + end +end diff --git a/template/config/processors/sequel.rb.bak.erb b/template/config/processors/sequel.rb.bak.erb deleted file mode 100644 index 1d050bb..0000000 --- a/template/config/processors/sequel.rb.bak.erb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -require 'yaml' - -require 'sequel' - -## Sequel configuration and methods -module <%= @module_name %> - module ConfigProcessors - ## Database initialize for Sequel - class Sequel - def initialize(config) - @config = config - - @config.load_yaml :database - - @config[:database] = - @config[:database][ENV.fetch('RACK_ENV', 'development')] - env_db_name = ENV['DB_NAME'] - @config[:database][:database] = env_db_name if env_db_name - - configure_sequel - rescue Flame::Errors::ConfigFileNotFoundError - nil - end - - private - - EXTENSIONS = %i[ - error_sql - pg_enum - pg_json - pg_timestamptz - server_block - ].freeze - - PLUGINS = %i[ - timestamps - json_serializer - dataset_associations - association_multi_add_remove - values_methods - ].freeze - - def configure_sequel - ::Sequel::Model.raise_on_save_failure = false - - EXTENSIONS.each do |extension_name| - <%= @short_module_name %>::Application.db_connection.extension extension_name - end - - PLUGINS.each { |plugin_name| ::Sequel::Model.plugin plugin_name } - - if @config[:environment] == 'development' - <%= @short_module_name %>::Application.db_connection.loggers << <%= @short_module_name %>::Application.logger - end - - ## Freeze DB (not for `toys console`) - <%= @short_module_name %>::Application.db_connection.freeze unless ENV['RACK_CONSOLE'] - end - end - end -end diff --git a/template/config/processors/sequel.rb.erb b/template/config/processors/sequel.rb.erb new file mode 100644 index 0000000..2a77180 --- /dev/null +++ b/template/config/processors/sequel.rb.erb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'sequel' + +## Sequel configuration and methods +module <%= @module_name %> + module Config + module Processors + ## Database initialize for Sequel + class Sequel + EXTENSIONS = %i[ + error_sql + pg_enum + pg_json + pg_timestamptz + server_block + ].freeze + + PLUGINS = %i[ + timestamps + json_serializer + dataset_associations + association_multi_add_remove + ].freeze + + def initialize(config) + return unless config.load_yaml :database + + config[:database] = config[:database][config[:environment]] + env_db_name = ENV['DB_NAME'] + config[:database][:database] = env_db_name if env_db_name + + ::Sequel::Model.raise_on_save_failure = false + + PLUGINS.each { |plugin_name| ::Sequel::Model.plugin plugin_name } + end + end + end + end +end diff --git a/template/config/processors/server.rb.erb b/template/config/processors/server.rb.erb new file mode 100644 index 0000000..df2ed70 --- /dev/null +++ b/template/config/processors/server.rb.erb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module <%= @module_name %> + module Config + module Processors + ## Server (Puma) config + class Server + def initialize(config) + server_config = config.load_yaml :server, required: true + + environment = config[:environment] = + ENV['RACK_ENV'] ||= server_config[:environment] || 'development' + + server_config = config[:server] = server_config[environment] + + server_config[:puma_pid_file] = "#{config[:pids_dir]}/#{server_config[:puma_pid_file]}" + end + end + end + end +end diff --git a/template/config/processors/shrine.rb.erb b/template/config/processors/shrine.rb.erb new file mode 100644 index 0000000..543f2f1 --- /dev/null +++ b/template/config/processors/shrine.rb.erb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'shrine' +require 'shrine/storage/file_system' + +module <%= @module_name %> + module Config + module Processors + ## Shrine configuration + class Shrine + PLUGINS = %i[ + sequel + cached_attachment_data + rack_file + determine_mime_type + ].freeze + + def initialize(config) + @config = config + + configure_storages + + PLUGINS.each { |plugin_name| ::Shrine.plugin plugin_name } + end + + private + + def configure_storages + ::Shrine.storages = { + ## temporary + cache: ::Shrine::Storage::FileSystem.new( + @config[:public_dir], prefix: 'uploads/cache' + ), + ## permanent + store: ::Shrine::Storage::FileSystem.new( + @config[:public_dir], prefix: 'uploads/store' + ) + } + end + end + end + end +end diff --git a/template/config/puma.rb b/template/config/puma.rb deleted file mode 100755 index 1e38526..0000000 --- a/template/config/puma.rb +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env puma -# frozen_string_literal: true - -require 'etc' -require 'yaml' - -config = YAML.load_file(File.join(__dir__, 'server.yaml')) - -environment = ENV['RACK_ENV'] || config[:environment] -env_config = config[environment] - -root_dir = File.join(__dir__, '..') -directory root_dir - -prune_bundler - -rackup 'config.ru' - -require 'fileutils' - -raise 'Unknown directory for pid files!' unless env_config[:pids_dir] - -pids_dir = File.join root_dir, env_config[:pids_dir] -FileUtils.mkdir_p pids_dir - -pidfile File.join pids_dir, env_config[:pid_file] -state_path File.join pids_dir, 'puma.state' - -raise 'Unknown directory for log files!' unless env_config[:logs_dir] - -logs_dir = File.join root_dir, env_config[:logs_dir] -FileUtils.mkdir_p logs_dir - -if env_config[:daemonize] - stdout_redirect( - File.join(logs_dir, 'stdout'), - File.join(logs_dir, 'stderr'), - true # append to file - ) -end - -environment environment - -# preload_app! if config['environment'] != 'production' - -cores = Etc.nprocessors -workers_count = env_config[:workers_count] || (cores < 2 ? 1 : 2) - -workers workers_count -worker_timeout env_config[:daemonize] ? 15 : 1_000_000 -threads 0, env_config[:threads_count] || 4 -daemonize env_config[:daemonize] - -lowlevel_error_handler do |_exception, _env| - # request_context = Raven::RequestContext.new( - # :puma, env: env, exception: exception - # ) - # Raven.capture_exception( - # *request_context.exception_with_context - # ) - ## Rack response - [ - 500, - {}, - [ - <<~BODY - An error has occurred, and engineers have been informed. Please reload the page. If you continue to have problems, contact us. - BODY - ] - ] -end - -# bind 'unix://' + File.join(%w[tmp sockets puma.sock]) -env_config[:binds].each do |type, value| - value = "#{value[:host]}:#{value[:port]}" if type == :tcp - FileUtils.mkdir_p File.join(root_dir, File.dirname(value)) if type == :unix - bind "#{type}://#{value}" -end -# activate_control_app 'tcp://0.0.0.0:3000' diff --git a/template/config/puma.rb.erb b/template/config/puma.rb.erb new file mode 100644 index 0000000..0039af7 --- /dev/null +++ b/template/config/puma.rb.erb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'fileutils' +require 'etc' + +require_relative 'base' + +config = <%= @module_name %>::Config::Base.new + +server_config = config[:server] + +environment config[:environment] + +directory config[:root_dir] + +prune_bundler + +rackup 'config.ru' + +pids_dir = config[:pids_dir] + +FileUtils.mkdir_p pids_dir + +pidfile File.join pids_dir, 'puma.pid' +state_path File.join pids_dir, 'puma.state' + +FileUtils.mkdir_p config[:logs_dir] + +if server_config[:daemonize] + stdout_redirect( + config[:stdout_file], + config[:stderr_file], + true # append to file + ) +end + +cores = Etc.nprocessors +workers_count = server_config[:workers_count] || (cores < 2 ? 1 : 2) + +workers workers_count +worker_timeout server_config[:daemonize] ? 15 : 1_000_000 +threads 0, server_config[:threads_count] || 4 +daemonize server_config[:daemonize] + +lowlevel_error_handler do |_exception, _env| + request_context = Flame::RavenContext.new(:puma, env: env, exception: exception) + Raven.capture_exception(*request_context.exception_with_context) + ## Rack response + [ + 500, + {}, + [ + <<~BODY + An error has occurred, and engineers have been informed. Please reload the page. If you continue to have problems, contact us. + BODY + ] + ] +end + +server_config[:binds].each do |type, value| + value = "#{value[:host]}:#{value[:port]}" if type == :tcp + FileUtils.mkdir_p File.join(config[:root_dir], File.dirname(value)) if type == :unix + bind "#{type}://#{value}" +end +# activate_control_app 'tcp://0.0.0.0:3000' diff --git a/template/config/sentry.example.yaml.erb b/template/config/sentry.example.yaml.erb new file mode 100644 index 0000000..e2d9a0f --- /dev/null +++ b/template/config/sentry.example.yaml.erb @@ -0,0 +1,7 @@ +:host: sentry.<%= @domain_name %>.com +back-end: + :api_key: abcdef + :project_id: 1 +front-end: + :api_key: ghijkl + :project_id: 2 diff --git a/template/config/server.example.yaml b/template/config/server.example.yaml index 215caa2..bc893f6 100644 --- a/template/config/server.example.yaml +++ b/template/config/server.example.yaml @@ -2,15 +2,13 @@ # :environment: production :default: &default + :puma_pid_file: 'puma.pid' :binds: :tcp: :host: '0.0.0.0' :port: 3000 # :unix: 'tmp/sockets/puma.sock' :daemonize: false - :pids_dir: tmp/pids - :logs_dir: logs - :pid_file: puma.pid # :workers_count: 1 # :threads_count: 4 diff --git a/template/constants.rb.erb b/template/constants.rb.erb deleted file mode 100644 index 8d0a0d2..0000000 --- a/template/constants.rb.erb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module <%= @module_name %> - ::<%= @short_module_name %> = ::<%= @module_name %> - - APP_DIRS = %w[ - lib - config - models - # policies - helpers - # mailers - # actions - forms - # view_objects - controllers - ].freeze -end diff --git a/template/controllers/_controller.rb.erb b/template/controllers/_controller.rb.erb index 2eefde2..f60a227 100644 --- a/template/controllers/_controller.rb.erb +++ b/template/controllers/_controller.rb.erb @@ -3,12 +3,39 @@ module <%= @module_name %> ## Base controller for any others controllers class Controller < Flame::Controller - # include Flame::R18n::Initialization + include Flame::R18n::Initialization + R18n::Filters.on(:named_variables) + + protected + + def not_found + unless request.bot? + request_context = Flame::RavenContext.new(:not_found, controller: self) + Raven.capture_message(*request_context.exception_with_context) + end + super + end + + def server_error(exception) + # if config[:environment] == 'production' + # affected_account = nil # authenticated.account + # Mailers::Error::Internal.new(exception, request, params, affected_account).send! + # end + + request_context = Flame::RavenContext.new(:server, controller: self, exception: exception) + Raven.capture_exception(*request_context.exception_with_context) + + super + end private def logger <%= @short_module_name %>::Application.logger end + + def csrf_tag + Rack::Csrf.tag(request.env) + end end end diff --git a/template/controllers/site/_controller.rb.erb b/template/controllers/site/_controller.rb.erb index 2636321..85bab4d 100644 --- a/template/controllers/site/_controller.rb.erb +++ b/template/controllers/site/_controller.rb.erb @@ -13,6 +13,17 @@ module <%= @module_name %> response.headers[Rack::CONTENT_TYPE] = 'text/html; charset=utf-8' super end + + def server_error(exception) + @exception = exception + super + end + + def default_body + render "errors/#{status}" + rescue Flame::Errors::TemplateNotFoundError + "

#{super}

" + end end end end diff --git a/template/exe/setup.sh b/template/exe/setup.sh index cfedcf3..ef62a8a 100755 --- a/template/exe/setup.sh +++ b/template/exe/setup.sh @@ -4,12 +4,13 @@ CURRENT_DIR=`dirname "$0"` . $CURRENT_DIR/_common.sh -exe git checkout $1 -exe git pull origin $1 +# exe git checkout $1 +# exe git pull origin $1 exe $CURRENT_DIR/setup/ruby.sh exe toys config check -exe toys db migrate +# exe toys db create +# exe toys db migrate exe $CURRENT_DIR/setup/node.sh diff --git a/template/exe/setup/node.sh b/template/exe/setup/node.sh index 2334544..cce3cc9 100755 --- a/template/exe/setup/node.sh +++ b/template/exe/setup/node.sh @@ -2,22 +2,21 @@ . `dirname "$0"`/../_common.sh -if [ ! -f ".node-version" ] -then - echo "File .node-version not found." - exit 0 -fi - -if [ "$(cat .node-version)" != "$(node -v | tr -d 'v')" ] +if [ ! -f ".node-version" ] || [ "$(cat .node-version)" != "$(node -v | tr -d 'v')" ] then exe git -C ~/.nodenv/plugins/node-build pull - exe nodenv install -s -fi - -## Please, install Yarn manually: https://classic.yarnpkg.com/en/docs/install -if ! yarn check -then exe yarn install + if [ ! -f ".node-version" ] + then + echo "File '.node-version' not found. Installing last stable version..." + latest_version=$(nodenv install -l | grep '^[[:digit:]]\+\.[[:digit:]]\+\.[[:digit:]]\+$' | tail -1) + exe nodenv install -s $latest_version + exe nodenv local $latest_version + else + exe nodenv install -s + fi fi -exe yarn build +exe npm install + +exe npm run build diff --git a/template/exe/setup/ruby.sh b/template/exe/setup/ruby.sh index 0690572..421f0ed 100755 --- a/template/exe/setup/ruby.sh +++ b/template/exe/setup/ruby.sh @@ -2,13 +2,22 @@ . `dirname "$0"`/../_common.sh -if [ "$(cat .ruby-version)" != "$(ruby -e "puts RUBY_VERSION")" ] +if [ ! -f ".ruby-version" ] || [ "$(cat .ruby-version)" != "$(ruby -e "puts RUBY_VERSION")" ] then exe git -C ~/.rbenv/plugins/ruby-build pull - exe rbenv install -s + + if [ ! -f ".ruby-version" ] + then + echo "File '.ruby-version' not found. Installing last stable version..." + latest_version=$(rbenv install -l | grep '^[[:digit:]]\+\.[[:digit:]]\+\.[[:digit:]]\+$' | tail -1) + exe rbenv install -s $latest_version + exe rbenv local $latest_version + else + exe rbenv install -s + fi fi -if [ \ +if [ ! -f "Gemfile.lock" ] || [ \ "$(tail -n 1 Gemfile.lock | tr -d [:blank:])" != \ "$(bundler -v | cut -d ' ' -f 3)" \ ] diff --git a/template/exe/update.sh b/template/exe/update.sh index c289ed2..993f24c 100755 --- a/template/exe/update.sh +++ b/template/exe/update.sh @@ -4,8 +4,8 @@ CURRENT_DIR=`dirname "$0"` . $CURRENT_DIR/_common.sh -exe $CURRENT_DIR/../server stop +exe toys server stop exe $CURRENT_DIR/setup.sh "$@" -exe $CURRENT_DIR/../server start +exe toys server start diff --git a/template/filewatchers.yaml b/template/filewatchers.yaml index 0e6547a..2176609 100644 --- a/template/filewatchers.yaml +++ b/template/filewatchers.yaml @@ -2,11 +2,11 @@ :exclude: '**/{spec/**/*,config/**/*.example*}' :command: bundle exec pumactl restart -F config/puma.rb -# - :pattern: 'assets/styles/**/*' -# :command: yarn build:styles -# -# - :pattern: '{assets/scripts/**/*,{.babelrc,webpack.config.js}}' -# :command: yarn build:scripts -# -# - :pattern: 'package.json' -# :command: yarn install && yarn build:scripts +- :pattern: '{assets/styles/**/*,postcss.config.js}' + :command: npm run build:styles + +- :pattern: '{assets/scripts/**/*,babel.config.json,rollup.config.js}' + :command: npm run build:scripts + +- :pattern: 'package.json' + :command: npm install && npm run build diff --git a/template/forms/.keep b/template/forms/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/template/forms/_base.rb.erb b/template/forms/_base.rb.erb new file mode 100644 index 0000000..3146c52 --- /dev/null +++ b/template/forms/_base.rb.erb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'formalism/sequel_transactions' + +require 'formalism/r18n_errors' +require 'formalism/r18n_errors/validation_helpers' + +## Define base form and model forms via gem +module <%= @module_name %> + module Forms + ## Base class for forms + class Base < Formalism::Form + extend Forwardable + def_delegators 'self.class', :instance_name + + include Memery + include Formalism::SequelTransactions + include Formalism::R18nErrors + include Formalism::R18nErrors::ValidationHelpers + + class << self + include Memery + + using GorillaPatch::Inflections + + memoize def instance_name + name.split('::')[2].underscore.to_sym + end + end + + def initialize(params = {}) + @errors_key = instance_name + @mailers = [] + + super + end + + def before_retry + clear_memery_cache! + + @mailers.clear + + super + end + + def send_mails! + nested_forms.each_value(&__method__) + + @mailers.each(&:send!) + end + + private + + def db_connection + <%= @short_module_name %>::Application.db_connection + end + + def after_db_transaction_commit + send_mails! + end + end + end + + Formalism::ModelForms.define_for_project self +end diff --git a/template/lib/.keep b/template/lib/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/template/lib/flame/raven_context.rb b/template/lib/flame/raven_context.rb new file mode 100644 index 0000000..99a48b9 --- /dev/null +++ b/template/lib/flame/raven_context.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'flame/raven_context' + +module Flame + ## Redefine `user` object + class RavenContext + private + + def user + # @controller&.send(:authenticated)&.account + end + end +end diff --git a/template/mailers/_base.rb.erb b/template/mailers/_base.rb.erb new file mode 100644 index 0000000..ed7a29a --- /dev/null +++ b/template/mailers/_base.rb.erb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'memery' +require 'r18n-core/helpers' + +require_relative 'mail/_base' + +module <%= @module_name %> + ## Module for mailers + module Mailers + ## Base class for sending mails + class Base + INTERVAL = 0.5 + + include ::R18n::Helpers + include Memery + + using GorillaPatch::Inflections + + def initialize(**args) + @from = <%= @short_module_name %>::Application.config[:mail][:from] + @args = args + @mails = [] + + original_locale = R18n.get + add_mails + R18n.thread_set original_locale + end + + def send! + File.write lock_file, '' + Thread.new do + send_mails + ensure + File.delete lock_file + end + end + + private + + memoize def path_parts + self.class.name.underscore.split('/').drop(2) + end + + def add_mail(type, account, **view_args) + return if ENV['RACK_ENV'] == 'test' + + @controller = <%= @short_module_name %>::MailController.new + @controller.recipient = account + @controller.path_parts = [*path_parts, type.to_s] + body = @controller.render_mail @args.merge(view_args) + @mails << initialize_mail(account, body) + end + + def initialize_mail(account, body) + Mailers::Mail.activated.new( + from: @from, + to: account.email, + subject: @controller.mail_subject, + body: body + ) + end + + def send_mails + @mails.each.with_index(1) do |mail, index| + <%= @short_module_name %>::Application.logger.info "#{mail.log_message} [#{index}/#{count}]..." + mail.send! + sleep INTERVAL + end + end + + memoize def lock_file + File.join(<%= @short_module_name %>::Application.config[:tmp_dir], "mailing_#{object_id}") + end + + memoize def count + @mails.count + end + end + end +end diff --git a/template/mailers/mail/_base.rb.erb b/template/mailers/mail/_base.rb.erb new file mode 100644 index 0000000..727f44e --- /dev/null +++ b/template/mailers/mail/_base.rb.erb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module <%= @module_name %> + module Mailers + ## Module for mail objects + module Mail + class << self + attr_accessor :activated + end + + ## Base class for generating emails + class Base + def initialize(from:, to:, subject:, body:); end + + def send! + Retries.run( + max_tries: 5, base_sleep_seconds: 1, max_sleep_seconds: 30 + ) do + sending_behavior + end + rescue StandardError => e + <%= @short_module_name %>::Application.logger.error e + Raven.capture_exception( + e, logger: :email, extra: { failed_recipient: to } + ) + end + + def log_message + "Sending email '#{subject}' to #{to}" + end + + private + + def subject + @mail.subject + end + + def to + @mail.to + end + + def sending_behavior + raise 'Sending behavior should be defined' + end + end + end + end +end diff --git a/template/mailers/mail/default.rb.erb b/template/mailers/mail/default.rb.erb new file mode 100644 index 0000000..e02cab7 --- /dev/null +++ b/template/mailers/mail/default.rb.erb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module <%= @module_name %> + module Mailers + module Mail + ## Class for generating emails by `mail` gem + class Default < Mail::Base + def initialize(from:, to:, subject:, body:) + super + @mail = ::Mail.new do + from "#{from[:name]} <#{from[:email]}>" + to to + subject subject + html_part do + content_type 'text/html; charset=UTF-8' + body body + end + end + end + + private + + def sending_behavior + @mail.deliver! + end + end + end + end +end diff --git a/template/package.json b/template/package.json index 8e24b5b..de24c95 100644 --- a/template/package.json +++ b/template/package.json @@ -1,40 +1,41 @@ { "private": true, "dependencies": { - "@babel/plugin-transform-object-assign": "^7.8.3", - "@babel/preset-env": "^7.8.7", - "@rollup/plugin-commonjs": "^11.0.2", - "@rollup/plugin-json": "^4.0.2", - "@rollup/plugin-node-resolve": "^7.1.1", - "autoprefixer": "^9.7.4", - "core-js": "^3.6.4", - "postcss-cli": "^7.1.0", - "postcss-flexbugs-fixes": "^4.2.0", - "promise-polyfill": "^8.1.3", - "rollup": "^1.20.0", - "rollup-plugin-babel": "^4.4.0", - "sass": "^1.26.2", - "simplify-js": "^1.2.4", - "whatwg-fetch": "^3.0.0" + "@babel/core": "*", + "@babel/plugin-transform-object-assign": "*", + "@babel/preset-env": "*", + "@rollup/plugin-babel": "*", + "@rollup/plugin-commonjs": "*", + "@rollup/plugin-json": "*", + "@rollup/plugin-node-resolve": "*", + "autoprefixer": "*", + "core-js": "*", + "postcss-cli": "*", + "postcss-flexbugs-fixes": "*", + "promise-polyfill": "*", + "rollup": "*", + "sass": "*", + "simplify-js": "*", + "whatwg-fetch": "*" }, "devDependencies": { - "stylelint-config-standard": "^20.0.0", - "stylelint-no-unsupported-browser-features": "^4.0.0" - }, - "optionalDependencies": { - "@babel/core": "^7.8.7", - "eslint": "^6.8.0", - "stylelint": "^13.2.1" + "eslint": "*", + "remark-cli": "*", + "remark-preset-lint-recommended": "*", + "stylelint": "*", + "stylelint-config-standard": "*", + "stylelint-no-unsupported-browser-features": "*" }, "scripts": { "build:styles": "sass assets/styles/main.scss public/styles/main.css -s compressed && postcss public/styles/main.css -o public/styles/main.css", - "build:scripts": "rollup assets/scripts/app.js -o public/scripts/app/compiled/app.js -c", - "build": "yarn build:styles && yarn build:scripts", + "build:scripts": "rollup assets/scripts/application.js -o public/scripts/application/compiled/application.js -c", + "build": "npm run build:styles && npm run build:scripts", "clean:styles": "rm -f public/styles/main.css public/styles/main.css.map", - "clean:scripts": "rm -rvf public/scripts/app/compiled", - "clean": "yarn clean:styles && yarn clean:scripts", + "clean:scripts": "rm -rvf public/scripts/application/compiled", + "clean": "npm run clean:styles && npm run clean:scripts", + "lint:docs": "remark .", "lint:styles": "stylelint assets/styles/", - "lint:scripts": "eslint assets/scripts/ public/scripts/app/ ./*.js", - "lint": "yarn lint:styles; styles_lint_result=$?; yarn lint:scripts && exit $styles_lint_result" + "lint:scripts": "eslint assets/scripts/ ./*.js", + "lint": "npm run lint:docs; docs_lint_result=$?; npm run lint:styles; styles_lint_result=$?; npm run lint:scripts && $styles_lint_result && $docs_lint_result" } } diff --git a/template/rollup.config.js.erb b/template/rollup.config.js.erb index 0aa1b25..7ab4d53 100644 --- a/template/rollup.config.js.erb +++ b/template/rollup.config.js.erb @@ -1,7 +1,7 @@ import json from '@rollup/plugin-json'; import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; -import babel from 'rollup-plugin-babel'; +import babel from '@rollup/plugin-babel'; export default { output: { @@ -16,6 +16,8 @@ export default { browser: true }), commonjs(), - babel() + babel({ + babelHelpers: 'bundled' + }) ] }; diff --git a/template/server b/template/server deleted file mode 100755 index 63c193b..0000000 --- a/template/server +++ /dev/null @@ -1,213 +0,0 @@ -#!/usr/bin/env ruby - -# frozen_string_literal: true - -require 'shellwords' -require 'English' -require 'yaml' -require 'fileutils' - -SERVER_CONFIG = YAML.load_file File.join(__dir__, 'config', 'server.yaml') - -ENVIRONMENT = ENV['RACK_ENV'] || SERVER_CONFIG[:environment] - -puts "ENVIRONMENT : #{ENVIRONMENT}" - -PUMA_PID_FILE = File.join( - __dir__, *SERVER_CONFIG[ENVIRONMENT].values_at(:pids_dir, :pid_file) -) - -## Class for pids files -class PidsFile - def initialize(name, *pids) - @file = File.join PIDS_DIR, "#{name}.pids" - @pids = pids.any? ? pids : read - end - - def dump(pids = @pids) - FileUtils.mkdir_p PIDS_DIR - puts "PidsFile dump : #{pids} => #{@file}" - File.write @file, pids.join($RS) - end - - def read - return unless File.exist?(@file) - - @pids = File.read(@file).split($RS).map(&:to_i) - puts "PidsFile read : #{@pids}" - @pids - end - - def kill_each(pids = @pids) - Array(pids).each do |pid| - puts "PidsFile kill : #{pid}" - Process.kill('TERM', -Process.getpgid(pid)) - rescue Errno::ESRCH - puts "Process #{pid} doesn't exist" - end - self - end - - def delete - return unless File.exist?(@file) - - File.delete @file - end -end - -PidsFile::PIDS_DIR = SERVER_CONFIG[ENVIRONMENT][:pids_dir] - -def show_usage - puts <<~USAGE - Usage: ./server COMMAND - - COMMAND is one of: - start - Start server - stop - Stop server - kill - Kill server (and filewatcher) - restart - Restart server - monitor - Show logs - devel, dev - Restart and monitor server - status, ps - Show processes of server - USAGE -end - -def bash(command, print: true) - puts command if print - system bash_command(command) || abort -end - -def bash_command(command) - escaped_command = Shellwords.escape(command) - "bash -c #{escaped_command}" -end - -def bash_spawn(command, print: true) - puts "spawn #{command}" if print - spawn bash_command(command), pgroup: true -end - -def server(command) - abort unless bash "#{__dir__}/exe/setup/ruby.sh" - if %i[start restart].include?(command) && - !bash("#{__dir__}/exe/setup/node.sh") - abort - end - if %i[stop restart].include?(command) - filewatcher_pids_file = PidsFile.new(:filewatcher) - filewatcher_pids_file.kill_each.delete - end - web_server(command) -end - -def puma_command(command) - bash "bundle exec pumactl #{command}" -end - -def web_server(command) - if ENVIRONMENT == 'development' && command == :restart - return development_restart - end - - waiting_mailing_lock if %i[stop restart phased-restart].include?(command) - puma_command command -end - -def development_restart - filewatcher_pids = - development_filewatchers.map { |command| bash_spawn command } - - PidsFile.new(:filewatcher, filewatcher_pids).dump - - puma_command File.exist?(PUMA_PID_FILE) ? 'restart' : 'start' -rescue SystemExit, Interrupt => e - PidsFile.new(:filewatcher).kill_each.delete - - raise e -end - -def filewatcher_command(pattern, execute, exclude: nil) - <<-CMD.split.join(' ') - bundle exec " - filewatcher - '#{pattern}' - #{"--exclude '#{exclude}'" unless exclude.nil?} - '#{execute}' - " - CMD -end - -def development_filewatchers - YAML.load_file(File.join(__dir__, 'filewatchers.yaml')).map do |args| - filewatcher_command args[:pattern], args[:command], exclude: args[:exclude] - end -end - -def waiting_mailing_lock - while Dir[File.join(__dir__, 'tmp', 'mailing_*')].any? - puts "\e[31m\e[1mMails sending in progress!\e[22m\e[0m\nWaiting..." - sleep 1 - end -end - -def monitor_server - bash "tail -f #{File.join(__dir__, %w[logs {stdout,stderr}])}" -end - -def dependencies_check - bash('bundle check || bundle install') # && bash('yarn install') -end - -def assets_build - puts 'Assets not enabled.' - true - # bash 'yarn build', print: false -end - -def ps_with_grep(pattern) - bash "ps aux | grep #{pattern} --color", print: false -end - -def server_ps - puts - puts 'Filewatcher:' - puts - ps_with_grep '[f]ilewatcher' - puts - puts 'Puma:' - puts - ps_with_grep '[p]uma[\ :]' -end - -## Runtime -case ARGV[0] -when 'start' - server :start -when 'stop' - server :stop - PidsFile.new(:filewatcher).kill_each.delete -when 'restart' - server :restart -when 'phased-restart' - server :'phased-restart' -when 'kill' - server :stop - bash 'pkill -f filewatcher' - bash 'pkill -f puma' -when 'monitor' - monitor_server -when 'devel', 'dev' - bash 'toys config check' - server :restart - if SERVER_CONFIG[ENVIRONMENT][:daemonize] - puts 'Waiting for logs...' - sleep 1.5 - monitor_server - end -when 'ps', 'status' - server_ps -else - puts "Unknown command #{ARGV[0]}" - puts - show_usage -end diff --git a/template/views/site/errors/400.html.erb.erb b/template/views/site/errors/400.html.erb.erb new file mode 100644 index 0000000..9330d12 --- /dev/null +++ b/template/views/site/errors/400.html.erb.erb @@ -0,0 +1,12 @@ +
diff --git a/template/views/site/errors/404.html.erb.erb b/template/views/site/errors/404.html.erb.erb new file mode 100644 index 0000000..037041a --- /dev/null +++ b/template/views/site/errors/404.html.erb.erb @@ -0,0 +1,7 @@ +
+

<%= "<\%= t.error.page.itself.not_found %\>" %>

+
+ " %>"> + <%= "<\%= t.button.home %\>" %> + +
diff --git a/template/views/site/errors/500.html.erb.erb b/template/views/site/errors/500.html.erb.erb new file mode 100644 index 0000000..e51cb11 --- /dev/null +++ b/template/views/site/errors/500.html.erb.erb @@ -0,0 +1,28 @@ +
" %>"> +

<%= "<\%= t.error.unexpected_error.title %\>" %>

+

<%= "<\%= t.error.unexpected_error.subtitle %\>" %>

+ +

+ <%= "<\%= t.error.unexpected_error.text %\>" %> +

+ + <%= "<\% if config[:environment] == 'development' %\>" %> +

<%= "<\%==" %> + "#{@exception.class} - #{@exception.message}" + <%= "%\>" %>

<%= "<\%" %> + if @exception.respond_to? :sql + <%= "%\>" %>

<%= "<\%=" %> + @exception.sql + <%= "%\>" %>

<%= "<\% end %\>" %><%= "<\%=" %> + highlighted_backtrace_for @exception + <%= "%\>" %>
+ <%= "<\% end %\>" %> + +
+ + " %>"> + <%= "<\%= t.button.back %\>" %> + + +

+
diff --git a/template/views/site/layout.html.erb.erb b/template/views/site/layout.html.erb.erb index 6d41992..36e29c7 100644 --- a/template/views/site/layout.html.erb.erb +++ b/template/views/site/layout.html.erb.erb @@ -2,9 +2,80 @@ - <%= "\<%= #{@short_module_name}::Application.config[:site][:site_name] %\>" %> + + + <%= "<\%= config[:site][:site_name] %\>" %> + + <%= "<\%" %> + %i[ + main + ].each do |name| + <%= "%\>" %> + " %>" /> + <%= "<\% end %\>" %> + + <%= "<\% if Raven.configuration.environments.include?(config[:environment]) && !request.bot? %\>" %> + + + + <%= "<\% end %\>" %> + + <%= "<\%" %> + { + libs: %w[], + # dom4.max + # modernizr-custom + # svgxuse.min + # cccombo + # ], + 'application/compiled': %w[application] + } + .each do |dir, file_names| + <%= "%\>" %> + <%= "<\% file_names.each do |name| %\>" %> + + <%= "<\% end %\>" %> + <%= "<\% end %\>" %> - <%= "\<%= yield %\>" %> + <%= "<\%= yield %\>" %>