rails_engine_toolkit 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +54 -0
- data/.github/workflows/release.yml +22 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +83 -0
- data/Rakefile +7 -0
- data/docs/ARCHITECTURE.md +18 -0
- data/docs/COMPATIBILITY.md +18 -0
- data/docs/CONTRIBUTING.md +26 -0
- data/docs/END_TO_END.md +26 -0
- data/docs/PUBLISHING.md +24 -0
- data/docs/RELEASE.md +32 -0
- data/docs/RELEASE_CHECKLIST.md +49 -0
- data/docs/TESTING.md +21 -0
- data/exe/engine-toolkit +6 -0
- data/lib/rails_engine_toolkit/actions/delete_engine_migration.rb +46 -0
- data/lib/rails_engine_toolkit/actions/init.rb +54 -0
- data/lib/rails_engine_toolkit/actions/install_engine_migrations.rb +83 -0
- data/lib/rails_engine_toolkit/actions/new_engine.rb +127 -0
- data/lib/rails_engine_toolkit/actions/new_engine_migration.rb +30 -0
- data/lib/rails_engine_toolkit/actions/new_engine_model.rb +30 -0
- data/lib/rails_engine_toolkit/actions/remove_engine.rb +78 -0
- data/lib/rails_engine_toolkit/actions/uninstall_engine_migrations.rb +79 -0
- data/lib/rails_engine_toolkit/actions/update_engine_readme.rb +60 -0
- data/lib/rails_engine_toolkit/cli.rb +110 -0
- data/lib/rails_engine_toolkit/config.rb +98 -0
- data/lib/rails_engine_toolkit/errors.rb +7 -0
- data/lib/rails_engine_toolkit/file_editor.rb +40 -0
- data/lib/rails_engine_toolkit/generators/install/install_generator.rb +67 -0
- data/lib/rails_engine_toolkit/project.rb +71 -0
- data/lib/rails_engine_toolkit/railtie.rb +9 -0
- data/lib/rails_engine_toolkit/route_inspector.rb +44 -0
- data/lib/rails_engine_toolkit/routes_rewriter.rb +63 -0
- data/lib/rails_engine_toolkit/templates/engine_readme.erb +37 -0
- data/lib/rails_engine_toolkit/templates/engine_toolkit_yml.erb +35 -0
- data/lib/rails_engine_toolkit/templates/gemspec.erb +25 -0
- data/lib/rails_engine_toolkit/templates/license.erb +3 -0
- data/lib/rails_engine_toolkit/templates.rb +12 -0
- data/lib/rails_engine_toolkit/utils.rb +56 -0
- data/lib/rails_engine_toolkit/version.rb +5 -0
- data/lib/rails_engine_toolkit.rb +33 -0
- data/rails_engine_toolkit.gemspec +31 -0
- data/spec/rails_engine_toolkit/cli_spec.rb +11 -0
- data/spec/rails_engine_toolkit/config_spec.rb +52 -0
- data/spec/rails_engine_toolkit/file_editor_spec.rb +26 -0
- data/spec/rails_engine_toolkit/install_engine_migrations_spec.rb +36 -0
- data/spec/rails_engine_toolkit/install_generator_spec.rb +26 -0
- data/spec/rails_engine_toolkit/new_engine_integration_spec.rb +59 -0
- data/spec/rails_engine_toolkit/new_engine_spec.rb +54 -0
- data/spec/rails_engine_toolkit/project_spec.rb +19 -0
- data/spec/rails_engine_toolkit/remove_engine_integration_spec.rb +40 -0
- data/spec/rails_engine_toolkit/remove_engine_spec.rb +72 -0
- data/spec/rails_engine_toolkit/route_inspector_spec.rb +20 -0
- data/spec/rails_engine_toolkit/routes_rewriter_spec.rb +36 -0
- data/spec/rails_engine_toolkit/uninstall_engine_migrations_spec.rb +35 -0
- data/spec/rails_engine_toolkit/update_engine_readme_spec.rb +32 -0
- data/spec/spec_helper.rb +30 -0
- data/test/fixtures/host_app/Gemfile +5 -0
- data/test/fixtures/host_app/config/application.rb +11 -0
- data/test/fixtures/host_app/config/boot.rb +3 -0
- data/test/fixtures/host_app/config/environment.rb +3 -0
- data/test/fixtures/host_app/config/routes.rb +2 -0
- metadata +140 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
|
|
5
|
+
module RailsEngineToolkit
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
|
8
|
+
namespace 'engine_toolkit:install'
|
|
9
|
+
|
|
10
|
+
def create_config_file
|
|
11
|
+
require 'rails_engine_toolkit'
|
|
12
|
+
|
|
13
|
+
root_path = Pathname(destination_root)
|
|
14
|
+
slug_default = RailsEngineToolkit::Utils.repo_slug_from_path(root_path)
|
|
15
|
+
name_default = RailsEngineToolkit::Utils.humanize_slug(slug_default)
|
|
16
|
+
|
|
17
|
+
say_status :info, 'Creating engine toolkit configuration', :blue
|
|
18
|
+
|
|
19
|
+
project_slug = ask_with_default('Project slug', slug_default)
|
|
20
|
+
project_name = ask_with_default('Project name', name_default)
|
|
21
|
+
project_url = ask_with_default('Project URL', RailsEngineToolkit::Utils.git_remote_url.to_s)
|
|
22
|
+
author_name = ask_with_default('Author name', RailsEngineToolkit::Utils.git_config('user.name').to_s)
|
|
23
|
+
author_email = ask_with_default('Author email', RailsEngineToolkit::Utils.git_config('user.email').to_s)
|
|
24
|
+
database = ask_with_default('Default database adapter', 'postgresql')
|
|
25
|
+
|
|
26
|
+
api_only = yes?('Use API-only engines by default? [Y/n] ', :green)
|
|
27
|
+
skip_asset_pipeline = yes?('Skip asset pipeline by default? [Y/n] ', :green)
|
|
28
|
+
mount_routes = yes?('Mount engine routes automatically? [Y/n] ', :green)
|
|
29
|
+
create_ddd_structure = yes?('Create DDD folders by default? [Y/n] ', :green)
|
|
30
|
+
|
|
31
|
+
config_content = RailsEngineToolkit::Templates.render('engine_toolkit_yml', {
|
|
32
|
+
project_slug: project_slug,
|
|
33
|
+
project_name: project_name,
|
|
34
|
+
project_url: project_url,
|
|
35
|
+
author_name: author_name,
|
|
36
|
+
author_email: author_email,
|
|
37
|
+
database: database,
|
|
38
|
+
api_only: api_only,
|
|
39
|
+
skip_asset_pipeline: skip_asset_pipeline,
|
|
40
|
+
mount_routes: mount_routes,
|
|
41
|
+
create_ddd_structure: create_ddd_structure
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
create_file 'config/engine_toolkit.yml', config_content
|
|
45
|
+
|
|
46
|
+
say ''
|
|
47
|
+
say 'Default configuration created:', :green
|
|
48
|
+
say " project.slug: #{project_slug}"
|
|
49
|
+
say " project.name: #{project_name}"
|
|
50
|
+
say " defaults.database: #{database}"
|
|
51
|
+
say " defaults.api_only: #{api_only}"
|
|
52
|
+
say " defaults.mount_routes: #{mount_routes}"
|
|
53
|
+
say " defaults.create_ddd_structure: #{create_ddd_structure}"
|
|
54
|
+
say ''
|
|
55
|
+
say 'Edit this file to customize the toolkit for your project:', :yellow
|
|
56
|
+
say " #{File.join(destination_root, 'config/engine_toolkit.yml')}", :yellow
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def ask_with_default(label, default)
|
|
62
|
+
value = ask("#{label} [#{default}]")
|
|
63
|
+
value.present? ? value : default
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsEngineToolkit
|
|
4
|
+
class Project
|
|
5
|
+
attr_reader :root
|
|
6
|
+
|
|
7
|
+
def initialize(root = Pathname.pwd)
|
|
8
|
+
@root = Pathname(root).expand_path
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def validate_root!
|
|
12
|
+
raise ValidationError, 'You must run this command from the Rails project root.' unless gemfile.file?
|
|
13
|
+
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def config
|
|
18
|
+
@config ||= Config.load(root)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def gemfile = root.join('Gemfile')
|
|
22
|
+
def routes_file = root.join('config/routes.rb')
|
|
23
|
+
def root_migrations_dir = root.join('db/migrate')
|
|
24
|
+
def config_file = root.join(Config::DEFAULT_PATH)
|
|
25
|
+
|
|
26
|
+
def engine_path(engine_name)
|
|
27
|
+
root.join("engines/#{engine_name}")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def engine_exists?(engine_name)
|
|
31
|
+
engine_path(engine_name).directory?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def broken_engine_references
|
|
35
|
+
return [] unless gemfile.file?
|
|
36
|
+
|
|
37
|
+
gemfile.read.lines.filter_map do |line|
|
|
38
|
+
match = line.match(%r{path:\s*["']engines/([^"']+)["']})
|
|
39
|
+
next unless match
|
|
40
|
+
|
|
41
|
+
engine_name = match[1]
|
|
42
|
+
engine_name unless engine_exists?(engine_name)
|
|
43
|
+
end.uniq
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def remove_broken_engine_references!(engine_names)
|
|
47
|
+
return if engine_names.empty?
|
|
48
|
+
|
|
49
|
+
content = gemfile.read
|
|
50
|
+
engine_names.each do |name|
|
|
51
|
+
content = content.lines.grep_v(%r{path:\s*["']engines/#{Regexp.escape(name)}["']}).join
|
|
52
|
+
end
|
|
53
|
+
gemfile.write(content)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def route_inspector
|
|
57
|
+
return nil unless routes_file.file?
|
|
58
|
+
|
|
59
|
+
RouteInspector.new(routes_file.read)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def matching_root_migrations_for_engine(engine_name)
|
|
63
|
+
return [] unless root_migrations_dir.directory?
|
|
64
|
+
|
|
65
|
+
engine_class = Utils.classify(engine_name)
|
|
66
|
+
root_migrations_dir.glob('*.rb').select do |file|
|
|
67
|
+
file.read.include?(engine_class) || file.read.include?("#{engine_name}_")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsEngineToolkit
|
|
4
|
+
class RouteInspector
|
|
5
|
+
Mount = Struct.new(:engine_class, :path, :line)
|
|
6
|
+
|
|
7
|
+
def initialize(content)
|
|
8
|
+
@content = content
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def mounts
|
|
12
|
+
return [] unless syntax_valid?
|
|
13
|
+
|
|
14
|
+
@content.lines.each_with_index.flat_map do |line, index|
|
|
15
|
+
extract_mounts_from_line(line, index + 1)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def includes_mount?(engine_class, path)
|
|
20
|
+
mounts.any? { |mount| mount.engine_class == engine_class && mount.path == path }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def syntax_valid?
|
|
24
|
+
!Ripper.sexp(@content).nil?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def extract_mounts_from_line(line, line_number)
|
|
30
|
+
patterns = [
|
|
31
|
+
/^\s*mount\s+([A-Z][A-Za-z0-9_:]+)::Engine,\s+at:\s+["']([^"']+)["']\s*$/,
|
|
32
|
+
/^\s*mount\(([^,]+)::Engine,\s*at:\s+["']([^"']+)["']\s*\)\s*$/,
|
|
33
|
+
/^\s*mount\s+([A-Z][A-Za-z0-9_:]+)::Engine\s*=>\s*["']([^"']+)["']\s*$/
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
patterns.filter_map do |pattern|
|
|
37
|
+
match = line.match(pattern)
|
|
38
|
+
next unless match
|
|
39
|
+
|
|
40
|
+
Mount.new(engine_class: match[1], path: match[2], line: line_number)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsEngineToolkit
|
|
4
|
+
class RoutesRewriter
|
|
5
|
+
def initialize(path)
|
|
6
|
+
@path = Pathname(path)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def add_mount(engine_class:, mount_path:)
|
|
10
|
+
raise ValidationError, "Routes file not found: #{@path}" unless @path.file?
|
|
11
|
+
|
|
12
|
+
content = @path.read
|
|
13
|
+
inspector = RouteInspector.new(content)
|
|
14
|
+
return false if inspector.includes_mount?(engine_class, mount_path)
|
|
15
|
+
|
|
16
|
+
mount_line = %( mount #{engine_class}::Engine, at: "#{mount_path}"\n)
|
|
17
|
+
|
|
18
|
+
updated = content.sub(/^(\s*Rails\.application\.routes\.draw do\s*\n)/) do |match|
|
|
19
|
+
"#{match}#{mount_line}"
|
|
20
|
+
end
|
|
21
|
+
raise CommandError, "Could not find Rails.application.routes.draw block in #{@path}" if updated == content
|
|
22
|
+
|
|
23
|
+
@path.write(updated)
|
|
24
|
+
true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def remove_mount(engine_class:, mount_path:)
|
|
28
|
+
raise ValidationError, "Routes file not found: #{@path}" unless @path.file?
|
|
29
|
+
|
|
30
|
+
original = @path.read
|
|
31
|
+
lines = original.lines.reject do |line|
|
|
32
|
+
mount_line?(line, engine_class: engine_class, mount_path: mount_path)
|
|
33
|
+
end
|
|
34
|
+
updated = lines.join
|
|
35
|
+
changed = updated != original
|
|
36
|
+
@path.write(updated) if changed
|
|
37
|
+
changed
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def mount_line?(line, engine_class:, mount_path:)
|
|
43
|
+
regexes = [
|
|
44
|
+
mount_comma_regex(engine_class, mount_path),
|
|
45
|
+
mount_parenthesized_regex(engine_class, mount_path),
|
|
46
|
+
mount_hash_rocket_regex(engine_class, mount_path)
|
|
47
|
+
]
|
|
48
|
+
regexes.any? { |regex| line.match?(regex) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def mount_comma_regex(engine_class, mount_path)
|
|
52
|
+
/^\s*mount\s+#{Regexp.escape(engine_class)}::Engine,\s+at:\s+["']#{Regexp.escape(mount_path)}["']\s*$/
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def mount_parenthesized_regex(engine_class, mount_path)
|
|
56
|
+
/^\s*mount\(\s*#{Regexp.escape(engine_class)}::Engine\s*,\s*at:\s+["']#{Regexp.escape(mount_path)}["']\s*\)\s*$/
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def mount_hash_rocket_regex(engine_class, mount_path)
|
|
60
|
+
/^\s*mount\s+#{Regexp.escape(engine_class)}::Engine\s*=>\s*["']#{Regexp.escape(mount_path)}["']\s*$/
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# <%= engine_class %>
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
<%= description %>
|
|
5
|
+
|
|
6
|
+
## Responsibilities
|
|
7
|
+
- Describe the main responsibilities of this bounded context.
|
|
8
|
+
|
|
9
|
+
## Owned tables
|
|
10
|
+
- None defined yet.
|
|
11
|
+
|
|
12
|
+
## Public endpoints
|
|
13
|
+
- None defined yet.
|
|
14
|
+
|
|
15
|
+
## Integration points
|
|
16
|
+
- None defined yet.
|
|
17
|
+
|
|
18
|
+
## Dependencies
|
|
19
|
+
- Rails
|
|
20
|
+
- PostgreSQL
|
|
21
|
+
|
|
22
|
+
## Commands
|
|
23
|
+
|
|
24
|
+
### Generate a model
|
|
25
|
+
```bash
|
|
26
|
+
bundle exec engine-toolkit new_engine_model <%= engine_name %> example_entity name:string
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Generate a migration
|
|
30
|
+
```bash
|
|
31
|
+
bundle exec engine-toolkit new_engine_migration <%= engine_name %> CreateExampleEntities
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Update owned tables
|
|
35
|
+
```bash
|
|
36
|
+
bundle exec engine-toolkit update_engine_readme <%= engine_name %>
|
|
37
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
project:
|
|
2
|
+
name: "<%= project_name %>"
|
|
3
|
+
slug: "<%= project_slug %>"
|
|
4
|
+
url: "<%= project_url %>"
|
|
5
|
+
|
|
6
|
+
author:
|
|
7
|
+
name: "<%= author_name %>"
|
|
8
|
+
email: "<%= author_email %>"
|
|
9
|
+
|
|
10
|
+
defaults:
|
|
11
|
+
database: "<%= database %>"
|
|
12
|
+
api_only: <%= api_only %>
|
|
13
|
+
skip_asset_pipeline: <%= skip_asset_pipeline %>
|
|
14
|
+
skip_action_mailbox: true
|
|
15
|
+
skip_action_text: true
|
|
16
|
+
skip_active_storage: true
|
|
17
|
+
skip_hotwire: true
|
|
18
|
+
skip_jbuilder: true
|
|
19
|
+
skip_system_test: true
|
|
20
|
+
mount_routes: <%= mount_routes %>
|
|
21
|
+
create_ddd_structure: <%= create_ddd_structure %>
|
|
22
|
+
|
|
23
|
+
metadata:
|
|
24
|
+
license: "MIT"
|
|
25
|
+
ruby_version: ">= 3.2"
|
|
26
|
+
rails_version: ">= 8.1.2"
|
|
27
|
+
|
|
28
|
+
ddd:
|
|
29
|
+
folders:
|
|
30
|
+
- app/use_cases
|
|
31
|
+
- app/services
|
|
32
|
+
- app/policies
|
|
33
|
+
- app/serializers
|
|
34
|
+
- app/repositories
|
|
35
|
+
- app/contracts
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require_relative "lib/<%= engine_name %>/version"
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "<%= engine_name %>"
|
|
5
|
+
spec.version = <%= engine_class %>::VERSION
|
|
6
|
+
spec.authors = ["<%= author_name %>"]
|
|
7
|
+
spec.email = ["<%= author_email %>"]
|
|
8
|
+
spec.summary = "<%= summary %>"
|
|
9
|
+
spec.description = "<%= description %>"
|
|
10
|
+
|
|
11
|
+
spec.homepage = "<%= project_url %>"
|
|
12
|
+
spec.license = "<%= license %>"
|
|
13
|
+
|
|
14
|
+
spec.required_ruby_version = "<%= ruby_version %>"
|
|
15
|
+
|
|
16
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
17
|
+
spec.metadata["source_code_uri"] = "<%= project_url %>"
|
|
18
|
+
spec.metadata["changelog_uri"] = "<%= project_url %>"
|
|
19
|
+
|
|
20
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
21
|
+
Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
spec.add_dependency "rails", "<%= rails_version %>"
|
|
25
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsEngineToolkit
|
|
4
|
+
module Templates
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def render(name, locals = {})
|
|
8
|
+
template = Pathname(__dir__).join("templates/#{name}.erb").read
|
|
9
|
+
ERB.new(template, trim_mode: '-').result_with_hash(locals)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsEngineToolkit
|
|
4
|
+
module Utils
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def snake_case?(value)
|
|
8
|
+
value.is_a?(String) && value.match?(/\A[a-z0-9_]+\z/)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def classify(snake_name)
|
|
12
|
+
snake_name.split('_').map(&:capitalize).join
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def humanize_slug(slug)
|
|
16
|
+
slug.split('_').map(&:capitalize).join(' ')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def repo_slug_from_path(pathname)
|
|
20
|
+
File.basename(Pathname(pathname).expand_path.to_s)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def ask(prompt, default: nil, input: $stdin, output: $stdout)
|
|
24
|
+
shown = default.nil? || default.to_s.empty? ? "#{prompt}: " : "#{prompt} [#{default}]: "
|
|
25
|
+
output.print(shown)
|
|
26
|
+
answer = input.gets&.chomp
|
|
27
|
+
return default if answer.nil? || answer.empty?
|
|
28
|
+
|
|
29
|
+
answer
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def ask_yes_no(prompt, default: false, input: $stdin, output: $stdout)
|
|
33
|
+
suffix = default ? ' [Y/n]: ' : ' [y/N]: '
|
|
34
|
+
output.print("#{prompt}#{suffix}")
|
|
35
|
+
answer = input.gets&.chomp.to_s.strip.downcase
|
|
36
|
+
return default if answer.empty?
|
|
37
|
+
|
|
38
|
+
%w[y yes].include?(answer)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def safe_system(*cmd, chdir:)
|
|
42
|
+
success = system(*cmd, chdir: chdir.to_s)
|
|
43
|
+
raise CommandError, "Command failed: #{cmd.join(' ')}" unless success
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def git_config(key)
|
|
47
|
+
value = `git config #{key} 2>/dev/null`.to_s.strip
|
|
48
|
+
value.empty? ? nil : value
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def git_remote_url
|
|
52
|
+
value = `git remote get-url origin 2>/dev/null`.to_s.strip
|
|
53
|
+
value.empty? ? nil : value
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'erb'
|
|
5
|
+
require 'pathname'
|
|
6
|
+
require 'fileutils'
|
|
7
|
+
require 'thor'
|
|
8
|
+
require 'ripper'
|
|
9
|
+
|
|
10
|
+
require_relative 'rails_engine_toolkit/version'
|
|
11
|
+
require_relative 'rails_engine_toolkit/errors'
|
|
12
|
+
require_relative 'rails_engine_toolkit/utils'
|
|
13
|
+
require_relative 'rails_engine_toolkit/config'
|
|
14
|
+
require_relative 'rails_engine_toolkit/file_editor'
|
|
15
|
+
require_relative 'rails_engine_toolkit/route_inspector'
|
|
16
|
+
require_relative 'rails_engine_toolkit/routes_rewriter'
|
|
17
|
+
require_relative 'rails_engine_toolkit/project'
|
|
18
|
+
require_relative 'rails_engine_toolkit/templates'
|
|
19
|
+
require_relative 'rails_engine_toolkit/actions/init'
|
|
20
|
+
require_relative 'rails_engine_toolkit/actions/new_engine'
|
|
21
|
+
require_relative 'rails_engine_toolkit/actions/update_engine_readme'
|
|
22
|
+
require_relative 'rails_engine_toolkit/actions/new_engine_migration'
|
|
23
|
+
require_relative 'rails_engine_toolkit/actions/delete_engine_migration'
|
|
24
|
+
require_relative 'rails_engine_toolkit/actions/new_engine_model'
|
|
25
|
+
require_relative 'rails_engine_toolkit/actions/install_engine_migrations'
|
|
26
|
+
require_relative 'rails_engine_toolkit/actions/uninstall_engine_migrations'
|
|
27
|
+
require_relative 'rails_engine_toolkit/actions/remove_engine'
|
|
28
|
+
require_relative 'rails_engine_toolkit/cli'
|
|
29
|
+
|
|
30
|
+
module RailsEngineToolkit
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
require_relative 'rails_engine_toolkit/railtie' if defined?(Rails::Railtie)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/rails_engine_toolkit/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'rails_engine_toolkit'
|
|
7
|
+
spec.version = RailsEngineToolkit::VERSION
|
|
8
|
+
spec.authors = ['Your Name']
|
|
9
|
+
spec.email = ['you@example.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'Reusable CLI and generators for Rails engines'
|
|
12
|
+
spec.description =
|
|
13
|
+
'Creates and manages internal Rails engines with configurable conventions, safe file mutations, parser-assisted route inspection, engine-specific migration installation, and Rails install generators.'
|
|
14
|
+
spec.homepage = 'https://github.com/your-org/rails_engine_toolkit'
|
|
15
|
+
spec.license = 'MIT'
|
|
16
|
+
spec.required_ruby_version = '>= 3.2'
|
|
17
|
+
|
|
18
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
19
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
|
20
|
+
spec.metadata['changelog_uri'] = "#{spec.homepage}/releases"
|
|
21
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
22
|
+
|
|
23
|
+
spec.files = Dir.glob('{exe,lib,spec,test,docs,.github}/**/*') +
|
|
24
|
+
%w[README.md LICENSE.txt rails_engine_toolkit.gemspec Gemfile Rakefile]
|
|
25
|
+
spec.bindir = 'exe'
|
|
26
|
+
spec.executables = ['engine-toolkit']
|
|
27
|
+
spec.require_paths = ['lib']
|
|
28
|
+
|
|
29
|
+
spec.add_dependency 'railties', '>= 8.1.0'
|
|
30
|
+
spec.add_dependency 'thor', '>= 1.3'
|
|
31
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe RailsEngineToolkit::Config do
|
|
6
|
+
it 'loads valid configuration' do
|
|
7
|
+
in_tmpdir do |dir|
|
|
8
|
+
write(dir.join('config/engine_toolkit.yml'), <<~YAML)
|
|
9
|
+
project:
|
|
10
|
+
name: "Transport System"
|
|
11
|
+
slug: "transport_system"
|
|
12
|
+
url: "https://example.test/repo"
|
|
13
|
+
author:
|
|
14
|
+
name: "Juan"
|
|
15
|
+
email: "juan@example.com"
|
|
16
|
+
defaults:
|
|
17
|
+
database: "postgresql"
|
|
18
|
+
api_only: true
|
|
19
|
+
mount_routes: true
|
|
20
|
+
create_ddd_structure: true
|
|
21
|
+
metadata:
|
|
22
|
+
license: "MIT"
|
|
23
|
+
ruby_version: ">= 3.2"
|
|
24
|
+
rails_version: ">= 8.1.2"
|
|
25
|
+
YAML
|
|
26
|
+
|
|
27
|
+
config = described_class.load(dir)
|
|
28
|
+
expect(config.project_name).to eq('Transport System')
|
|
29
|
+
expect(config.project_slug).to eq('transport_system')
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'rejects invalid database adapter' do
|
|
34
|
+
in_tmpdir do |dir|
|
|
35
|
+
write(dir.join('config/engine_toolkit.yml'), <<~YAML)
|
|
36
|
+
project:
|
|
37
|
+
name: "Transport System"
|
|
38
|
+
slug: "transport_system"
|
|
39
|
+
url: ""
|
|
40
|
+
author:
|
|
41
|
+
name: "Juan"
|
|
42
|
+
email: "juan@example.com"
|
|
43
|
+
defaults:
|
|
44
|
+
database: "oracle"
|
|
45
|
+
metadata:
|
|
46
|
+
license: "MIT"
|
|
47
|
+
YAML
|
|
48
|
+
|
|
49
|
+
expect { described_class.load(dir) }.to raise_error(RailsEngineToolkit::ValidationError, /defaults.database/)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe RailsEngineToolkit::FileEditor do
|
|
6
|
+
it 'removes matching lines safely' do
|
|
7
|
+
in_tmpdir do |dir|
|
|
8
|
+
path = dir.join('routes.rb')
|
|
9
|
+
write(path, "a\nb\nc\n")
|
|
10
|
+
changed = described_class.remove_lines_matching(path, /^b$/)
|
|
11
|
+
expect(changed).to be(true)
|
|
12
|
+
expect(path.read).to eq("a\nc\n")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'inserts content after first match' do
|
|
17
|
+
in_tmpdir do |dir|
|
|
18
|
+
path = dir.join('routes.rb')
|
|
19
|
+
write(path, "Rails.application.routes.draw do\nend\n")
|
|
20
|
+
changed = described_class.insert_after_first_match(path, /Rails\.application\.routes\.draw do\s*\n/,
|
|
21
|
+
" mount Auth::Engine, at: \"/auth\"\n")
|
|
22
|
+
expect(changed).to be(true)
|
|
23
|
+
expect(path.read).to include('mount Auth::Engine, at: "/auth"')
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe RailsEngineToolkit::Actions::InstallEngineMigrations do
|
|
6
|
+
it 'copies only the selected engine migrations into root db/migrate' do
|
|
7
|
+
in_tmpdir do |dir|
|
|
8
|
+
write(dir.join('Gemfile'), "source \"https://rubygems.org\"\n")
|
|
9
|
+
write(dir.join('engines/auth/db/migrate/20260101000000_create_auth_credentials.rb'),
|
|
10
|
+
'class CreateAuthCredentials; end')
|
|
11
|
+
write(dir.join('engines/billing/db/migrate/20260101000001_create_billing_invoices.rb'),
|
|
12
|
+
'class CreateBillingInvoices; end')
|
|
13
|
+
|
|
14
|
+
out = StringIO.new
|
|
15
|
+
described_class.new(['auth'], stdin: StringIO.new, stdout: out, stderr: StringIO.new, root: dir).call
|
|
16
|
+
|
|
17
|
+
root_migrations = dir.join('db/migrate').glob('*.rb').map { |f| f.basename.to_s }
|
|
18
|
+
expect(root_migrations.any? { |name| name.end_with?('create_auth_credentials.rb') }).to be(true)
|
|
19
|
+
expect(root_migrations.any? { |name| name.end_with?('create_billing_invoices.rb') }).to be(false)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'does not duplicate already installed migrations' do
|
|
24
|
+
in_tmpdir do |dir|
|
|
25
|
+
write(dir.join('Gemfile'), "source \"https://rubygems.org\"\n")
|
|
26
|
+
write(dir.join('engines/auth/db/migrate/20260101000000_create_auth_credentials.rb'),
|
|
27
|
+
'class CreateAuthCredentials; end')
|
|
28
|
+
write(dir.join('db/migrate/20260102000000_create_auth_credentials.rb'), 'class CreateAuthCredentials; end')
|
|
29
|
+
|
|
30
|
+
out = StringIO.new
|
|
31
|
+
described_class.new(['auth'], stdin: StringIO.new, stdout: out, stderr: StringIO.new, root: dir).call
|
|
32
|
+
|
|
33
|
+
expect(out.string).to include('No new migrations')
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'rails_engine_toolkit/generators/install/install_generator'
|
|
5
|
+
|
|
6
|
+
RSpec.describe RailsEngineToolkit::Generators::InstallGenerator do
|
|
7
|
+
it 'creates config and prints summary in a host app' do
|
|
8
|
+
in_tmpdir do |dir|
|
|
9
|
+
write(dir.join('Gemfile'), "source \"https://rubygems.org\"\n")
|
|
10
|
+
|
|
11
|
+
generator = described_class.new
|
|
12
|
+
allow(generator).to receive(:destination_root).and_return(dir.to_s)
|
|
13
|
+
allow(generator).to receive(:ask).and_return('', '', '', '', '', 'postgresql')
|
|
14
|
+
allow(generator).to receive(:yes?).and_return(true, true, true, true)
|
|
15
|
+
output = StringIO.new
|
|
16
|
+
allow(generator).to receive(:say) { |message = '', *_args| output.puts(message) }
|
|
17
|
+
allow(generator).to receive(:say_status) { |_status, message, *_args| output.puts(message) }
|
|
18
|
+
|
|
19
|
+
generator.create_config_file
|
|
20
|
+
|
|
21
|
+
expect(dir.join('config/engine_toolkit.yml')).to exist
|
|
22
|
+
expect(output.string).to include('Default configuration created:')
|
|
23
|
+
expect(output.string).to include('config/engine_toolkit.yml')
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|