hanami-cli 2.2.0 → 2.3.0.beta1

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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +9 -0
  3. data/CHANGELOG.md +43 -0
  4. data/Gemfile +3 -1
  5. data/hanami-cli.gemspec +2 -0
  6. data/lib/hanami/cli/command.rb +5 -11
  7. data/lib/hanami/cli/commands/app/command.rb +5 -5
  8. data/lib/hanami/cli/commands/app/console.rb +1 -0
  9. data/lib/hanami/cli/commands/app/db/command.rb +0 -1
  10. data/lib/hanami/cli/commands/app/db/drop.rb +2 -2
  11. data/lib/hanami/cli/commands/app/db/rollback.rb +204 -0
  12. data/lib/hanami/cli/commands/app/db/utils/mysql.rb +3 -2
  13. data/lib/hanami/cli/commands/app/db/utils/postgres.rb +3 -1
  14. data/lib/hanami/cli/commands/app/generate/action.rb +32 -40
  15. data/lib/hanami/cli/commands/app/generate/command.rb +54 -17
  16. data/lib/hanami/cli/commands/app/generate/component.rb +4 -19
  17. data/lib/hanami/cli/commands/app/generate/part.rb +4 -21
  18. data/lib/hanami/cli/commands/app/generate/slice.rb +2 -2
  19. data/lib/hanami/cli/commands/app/generate/view.rb +5 -24
  20. data/lib/hanami/cli/commands/app/install.rb +12 -0
  21. data/lib/hanami/cli/commands/app/server.rb +0 -1
  22. data/lib/hanami/cli/commands/app.rb +1 -0
  23. data/lib/hanami/cli/commands/gem/new.rb +37 -7
  24. data/lib/hanami/cli/errors.rb +26 -0
  25. data/lib/hanami/cli/files.rb +20 -6
  26. data/lib/hanami/cli/generators/app/action.rb +78 -101
  27. data/lib/hanami/cli/generators/app/component.rb +11 -33
  28. data/lib/hanami/cli/generators/app/migration.rb +1 -1
  29. data/lib/hanami/cli/generators/app/operation.rb +4 -5
  30. data/lib/hanami/cli/generators/app/part.rb +42 -65
  31. data/lib/hanami/cli/generators/app/relation.rb +4 -5
  32. data/lib/hanami/cli/generators/app/repo.rb +3 -5
  33. data/lib/hanami/cli/generators/app/ruby_class_file.rb +32 -0
  34. data/lib/hanami/cli/generators/app/ruby_file.rb +128 -0
  35. data/lib/hanami/cli/generators/app/ruby_module_file.rb +28 -0
  36. data/lib/hanami/cli/generators/app/slice.rb +130 -37
  37. data/lib/hanami/cli/generators/app/struct.rb +3 -4
  38. data/lib/hanami/cli/generators/app/view.rb +40 -45
  39. data/lib/hanami/cli/generators/context.rb +6 -0
  40. data/lib/hanami/cli/generators/gem/app/assets.js +14 -13
  41. data/lib/hanami/cli/generators/gem/app/dev +1 -1
  42. data/lib/hanami/cli/generators/gem/app/gemfile.erb +5 -0
  43. data/lib/hanami/cli/generators/gem/app/gitignore.erb +3 -1
  44. data/lib/hanami/cli/generators/gem/app/rakefile.erb +3 -0
  45. data/lib/hanami/cli/generators/gem/app/readme.erb +14 -0
  46. data/lib/hanami/cli/generators/gem/app.rb +40 -37
  47. data/lib/hanami/cli/ruby_file_generator.rb +17 -8
  48. data/lib/hanami/cli/server.rb +15 -1
  49. data/lib/hanami/cli/version.rb +1 -1
  50. data/lib/hanami/console/context.rb +5 -0
  51. metadata +35 -41
  52. data/lib/hanami/cli/generators/app/action/action.erb +0 -17
  53. data/lib/hanami/cli/generators/app/action/slice_action.erb +0 -17
  54. data/lib/hanami/cli/generators/app/action/slice_template.html.erb +0 -1
  55. data/lib/hanami/cli/generators/app/action/slice_view.erb +0 -10
  56. data/lib/hanami/cli/generators/app/action/template.erb +0 -0
  57. data/lib/hanami/cli/generators/app/action/template.html.erb +0 -1
  58. data/lib/hanami/cli/generators/app/action/view.erb +0 -10
  59. data/lib/hanami/cli/generators/app/action_context.rb +0 -90
  60. data/lib/hanami/cli/generators/app/component/component.erb +0 -8
  61. data/lib/hanami/cli/generators/app/component/slice_component.erb +0 -8
  62. data/lib/hanami/cli/generators/app/component_context.rb +0 -82
  63. data/lib/hanami/cli/generators/app/part/app_base_part.erb +0 -9
  64. data/lib/hanami/cli/generators/app/part/app_part.erb +0 -13
  65. data/lib/hanami/cli/generators/app/part/slice_base_part.erb +0 -9
  66. data/lib/hanami/cli/generators/app/part/slice_part.erb +0 -13
  67. data/lib/hanami/cli/generators/app/part_context.rb +0 -82
  68. data/lib/hanami/cli/generators/app/ruby_file_writer.rb +0 -151
  69. data/lib/hanami/cli/generators/app/slice/action.erb +0 -7
  70. data/lib/hanami/cli/generators/app/slice/app_css.erb +0 -5
  71. data/lib/hanami/cli/generators/app/slice/app_js.erb +0 -1
  72. data/lib/hanami/cli/generators/app/slice/app_layout.erb +0 -18
  73. data/lib/hanami/cli/generators/app/slice/helpers.erb +0 -10
  74. data/lib/hanami/cli/generators/app/slice/keep.erb +0 -0
  75. data/lib/hanami/cli/generators/app/slice/operation.erb +0 -7
  76. data/lib/hanami/cli/generators/app/slice/relation.erb +0 -8
  77. data/lib/hanami/cli/generators/app/slice/repo.erb +0 -8
  78. data/lib/hanami/cli/generators/app/slice/routes.erb +0 -3
  79. data/lib/hanami/cli/generators/app/slice/struct.erb +0 -8
  80. data/lib/hanami/cli/generators/app/slice/view.erb +0 -7
  81. data/lib/hanami/cli/generators/app/slice_context.rb +0 -72
  82. data/lib/hanami/cli/generators/app/view/app_template.html.erb +0 -1
  83. data/lib/hanami/cli/generators/app/view/app_view.erb +0 -10
  84. data/lib/hanami/cli/generators/app/view/slice_template.html.erb +0 -1
  85. data/lib/hanami/cli/generators/app/view/slice_view.erb +0 -10
  86. data/lib/hanami/cli/generators/app/view_context.rb +0 -88
@@ -21,36 +21,73 @@ module Hanami
21
21
 
22
22
  # @since 2.2.0
23
23
  # @api private
24
- def initialize(
25
- fs:,
26
- inflector:,
27
- **opts
28
- )
24
+ def initialize(fs:, out:, **)
29
25
  super
30
- @generator = generator_class.new(fs: fs, inflector: inflector, out: out)
26
+ @generator = generator_class.new(fs:, inflector:, out:)
31
27
  end
32
28
 
29
+ # @since 2.2.0
30
+ # @api private
33
31
  def generator_class
34
- # Must be implemented by subclasses, with class that takes:
35
- # fs:, inflector:, out:
32
+ # Must be implemented by subclasses, with initialize method that takes:
33
+ # fs:, out:
36
34
  end
37
35
 
38
36
  # @since 2.2.0
39
37
  # @api private
40
- def call(name:, slice: nil, **)
41
- if slice
42
- generator.call(
43
- key: name,
44
- namespace: slice,
45
- base_path: fs.join("slices", inflector.underscore(slice))
46
- )
47
- else
38
+ def call(name:, slice: nil, **opts)
39
+ slice ||= detect_slice_from_cwd
40
+
41
+ if slice.nil?
48
42
  generator.call(
49
43
  key: name,
50
44
  namespace: app.namespace,
51
- base_path: "app"
45
+ base_path: "app",
46
+ **opts,
52
47
  )
48
+ return
53
49
  end
50
+
51
+ slice_root = slice.respond_to?(:root) ? slice.root : detect_slice_root(slice)
52
+ raise MissingSliceError.new(slice) unless fs.exist?(slice_root)
53
+
54
+ generator.call(
55
+ key: name,
56
+ namespace: slice,
57
+ base_path: slice_root,
58
+ **opts,
59
+ )
60
+ end
61
+
62
+ private
63
+
64
+ def detect_slice_from_cwd
65
+ slices_by_root = app.slices.with_nested.each.to_h { |slice| [slice.root.to_s, slice] }
66
+ slices_by_root[fs.pwd]
67
+ end
68
+
69
+ # Returns the root for the given slice name.
70
+ #
71
+ # This currently works with top-level slices only, and it simply appends the slice's
72
+ # name onto the "slices/" dir, returning e.g. "slices/main" when given "main".
73
+ #
74
+ # TODO: Make this work with nested slices when given slash-delimited slice names like
75
+ # "parent/child", which should look for "slices/parent/slices/child".
76
+ #
77
+ # This method makes two checks for the slice root (building off both `app.root` as well
78
+ # as `fs`). This is entirely to account for how we test commands, with most tests using
79
+ # an in-memory `fs` adapter, any files created via which will be invisible to the `app`,
80
+ # which doesn't know about the `fs`.
81
+ #
82
+ # FIXME: It would be better to find a way for this to make one check only. An ideal
83
+ # approach would be to use the slice_name to find actual slice registered within
84
+ # `app.slices`. To do this, we'd probably need to stop testing with an in-memory `fs`
85
+ # here.
86
+ def detect_slice_root(slice_name)
87
+ slice_root_in_fs = fs.join("slices", inflector.underscore(slice_name))
88
+ return slice_root_in_fs if fs.exist?(slice_root_in_fs)
89
+
90
+ app.root.join("slices", inflector.underscore(slice_name))
54
91
  end
55
92
  end
56
93
  end
@@ -3,6 +3,7 @@
3
3
  require "dry/inflector"
4
4
  require "dry/files"
5
5
  require "shellwords"
6
+
6
7
  module Hanami
7
8
  module CLI
8
9
  module Commands
@@ -10,9 +11,8 @@ module Hanami
10
11
  module Generate
11
12
  # @api private
12
13
  # @since 2.2.0
13
- class Component < App::Command
14
+ class Component < Command
14
15
  argument :name, required: true, desc: "Component name"
15
- option :slice, required: false, desc: "Slice name"
16
16
 
17
17
  example [
18
18
  %(isbn_decoder (MyApp::IsbnDecoder)),
@@ -20,26 +20,11 @@ module Hanami
20
20
  %(isbn_decoder --slice=admin (Admin::IsbnDecoder)),
21
21
  %(Exporters::Complete::CSV (MyApp::Exporters::Complete::CSV)),
22
22
  ]
23
- attr_reader :generator
24
- private :generator
25
23
 
26
- # @api private
27
24
  # @since 2.2.0
28
- def initialize(
29
- fs:, inflector:,
30
- generator: Generators::App::Component.new(fs: fs, inflector: inflector),
31
- **opts
32
- )
33
- @generator = generator
34
- super(fs: fs, inflector: inflector, **opts)
35
- end
36
-
37
25
  # @api private
38
- # @since 2.2.0
39
- def call(name:, slice: nil, **)
40
- slice = inflector.underscore(Shellwords.shellescape(slice)) if slice
41
-
42
- generator.call(app.namespace, name, slice)
26
+ def generator_class
27
+ Generators::App::Component
43
28
  end
44
29
  end
45
30
  end
@@ -11,12 +11,12 @@ module Hanami
11
11
  module Generate
12
12
  # @since 2.1.0
13
13
  # @api private
14
- class Part < App::Command
14
+ class Part < Command
15
15
  DEFAULT_SKIP_TESTS = false
16
16
  private_constant :DEFAULT_SKIP_TESTS
17
17
 
18
18
  argument :name, required: true, desc: "Part name"
19
- option :slice, required: false, desc: "Slice name"
19
+
20
20
  option \
21
21
  :skip_tests,
22
22
  required: false,
@@ -28,26 +28,9 @@ module Hanami
28
28
  %(book (MyApp::Views::Parts::Book)),
29
29
  %(book --slice=admin (Admin::Views::Parts::Book)),
30
30
  ]
31
- attr_reader :generator
32
- private :generator
33
-
34
- # @since 2.0.0
35
- # @api private
36
- def initialize(
37
- fs:, inflector:,
38
- generator: Generators::App::Part.new(fs: fs, inflector: inflector),
39
- **opts
40
- )
41
- super(fs: fs, inflector: inflector, **opts)
42
- @generator = generator
43
- end
44
-
45
- # @since 2.0.0
46
- # @api private
47
- def call(name:, slice: nil, skip_tests: DEFAULT_SKIP_TESTS, **) # rubocop:disable Lint/UnusedMethodArgument
48
- slice = inflector.underscore(Shellwords.shellescape(slice)) if slice
49
31
 
50
- generator.call(app.namespace, name, slice)
32
+ def generator_class
33
+ Generators::App::Part
51
34
  end
52
35
  end
53
36
  end
@@ -49,11 +49,11 @@ module Hanami
49
49
  # @since 2.0.0
50
50
  # @api private
51
51
  def initialize(
52
- fs:, inflector:,
52
+ fs:,
53
53
  generator: Generators::App::Slice.new(fs: fs, inflector: inflector),
54
54
  **opts
55
55
  )
56
- super(fs: fs, inflector: inflector, **opts)
56
+ super(fs: fs, **opts)
57
57
  @generator = generator
58
58
  end
59
59
 
@@ -13,40 +13,21 @@ module Hanami
13
13
  module Generate
14
14
  # @since 2.0.0
15
15
  # @api private
16
- class View < App::Command
17
- # TODO: make this configurable
18
- DEFAULT_FORMAT = "html"
19
- private_constant :DEFAULT_FORMAT
20
-
16
+ class View < Command
17
+ # TODO: make format configurable
21
18
  # TODO: make engine configurable
22
19
 
23
20
  argument :name, required: true, desc: "View name"
24
- option :slice, required: false, desc: "Slice name"
25
21
 
26
22
  example [
27
23
  %(books.index (MyApp::Actions::Books::Index)),
28
24
  %(books.index --slice=admin (Admin::Actions::Books::Index)),
29
25
  ]
30
- attr_reader :generator
31
- private :generator
32
26
 
33
- # @since 2.0.0
27
+ # @since 2.2.0
34
28
  # @api private
35
- def initialize(
36
- fs:, inflector:,
37
- generator: Generators::App::View.new(fs: fs, inflector: inflector),
38
- **opts
39
- )
40
- super(fs: fs, inflector: inflector, **opts)
41
- @generator = generator
42
- end
43
-
44
- # @since 2.0.0
45
- # @api private
46
- def call(name:, format: DEFAULT_FORMAT, slice: nil, **)
47
- slice = inflector.underscore(Shellwords.shellescape(slice)) if slice
48
-
49
- generator.call(app.namespace, name, format, slice)
29
+ def generator_class
30
+ Generators::App::View
50
31
  end
51
32
  end
52
33
  end
@@ -33,9 +33,21 @@ module Hanami
33
33
  # @api private
34
34
  option :head, type: :flag, desc: "Install head deps", default: DEFAULT_HEAD
35
35
 
36
+ # @api private
37
+ private attr_reader :bundler
38
+
39
+ def initialize(
40
+ fs:,
41
+ bundler: CLI::Bundler.new(fs: fs),
42
+ **opts
43
+ )
44
+ @bundler = bundler
45
+ end
46
+
36
47
  # @since 2.0.0
37
48
  # @api private
38
49
  def call(head: DEFAULT_HEAD, **)
50
+ bundler.install!
39
51
  end
40
52
  end
41
53
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rack"
4
3
  require "hanami/port"
5
4
  require_relative "../app"
6
5
  require_relative "../../server"
@@ -32,6 +32,7 @@ module Hanami
32
32
  db.register "create", DB::Create
33
33
  db.register "drop", DB::Drop
34
34
  db.register "migrate", DB::Migrate
35
+ db.register "rollback", DB::Rollback
35
36
  db.register "structure dump", DB::Structure::Dump
36
37
  db.register "structure load", DB::Structure::Load
37
38
  db.register "seed", DB::Seed
@@ -30,6 +30,11 @@ module Hanami
30
30
  SKIP_DB_DEFAULT = false
31
31
  private_constant :SKIP_DB_DEFAULT
32
32
 
33
+ # @since 2.2.0
34
+ # @api private
35
+ SKIP_VIEW_DEFAULT = false
36
+ private_constant :SKIP_VIEW_DEFAULT
37
+
33
38
  # @since 2.2.0
34
39
  # @api private
35
40
  DATABASE_SQLITE = "sqlite"
@@ -46,6 +51,9 @@ module Hanami
46
51
  # @api private
47
52
  SUPPORTED_DATABASES = [DATABASE_SQLITE, DATABASE_POSTGRES, DATABASE_MYSQL].freeze
48
53
 
54
+ # @api private
55
+ FORBIDDEN_APP_NAMES = %w[app slice].freeze
56
+
49
57
  desc "Generate a new Hanami app"
50
58
 
51
59
  # @since 2.0.0
@@ -76,6 +84,12 @@ module Hanami
76
84
  default: SKIP_DB_DEFAULT,
77
85
  desc: "Skip including hanami-db"
78
86
 
87
+ # @since 2.2.0
88
+ # @api private
89
+ option :skip_view, type: :flag, required: false,
90
+ default: SKIP_VIEW_DEFAULT,
91
+ desc: "Skip including hanami-view"
92
+
79
93
  # @since 2.2.0
80
94
  # @api private
81
95
  option :database, type: :string, required: false,
@@ -87,8 +101,9 @@ module Hanami
87
101
  "bookshelf # Generate a new Hanami app in `bookshelf/' directory, using `Bookshelf' namespace",
88
102
  "bookshelf --head # Generate a new Hanami app, using Hanami HEAD version from GitHub `main' branches",
89
103
  "bookshelf --skip-install # Generate a new Hanami app, but it skips Hanami installation",
90
- "bookshelf --skip-assets # Generate a new Hanami app without hanmai-assets",
104
+ "bookshelf --skip-assets # Generate a new Hanami app without hanami-assets",
91
105
  "bookshelf --skip-db # Generate a new Hanami app without hanami-db",
106
+ "bookshelf --skip-view # Generate a new Hanami app without hanami-view",
92
107
  "bookshelf --database={sqlite|postgres|mysql} # Generate a new Hanami app with a specified database (default: sqlite)",
93
108
  ]
94
109
  # rubocop:enable Layout/LineLength
@@ -98,13 +113,13 @@ module Hanami
98
113
  # @since 2.0.0
99
114
  # @api private
100
115
  def initialize(
101
- fs:, inflector:,
116
+ fs:,
102
117
  bundler: CLI::Bundler.new(fs: fs),
103
118
  generator: Generators::Gem::App.new(fs: fs, inflector: inflector),
104
119
  system_call: SystemCall.new,
105
120
  **opts
106
121
  )
107
- super(fs: fs, inflector: inflector, **opts)
122
+ super(fs: fs, **opts)
108
123
  @bundler = bundler
109
124
  @generator = generator
110
125
  @system_call = system_call
@@ -120,13 +135,14 @@ module Hanami
120
135
  skip_install: SKIP_INSTALL_DEFAULT,
121
136
  skip_assets: SKIP_ASSETS_DEFAULT,
122
137
  skip_db: SKIP_DB_DEFAULT,
138
+ skip_view: SKIP_VIEW_DEFAULT,
123
139
  database: nil
124
140
  )
125
141
  # rubocop:enable Metrics/ParameterLists
126
142
  app = inflector.underscore(app)
127
143
 
128
144
  raise PathAlreadyExistsError.new(app) if fs.exist?(app)
129
- raise ConflictingOptionsError.new("--skip-db", "--database") if skip_db && database
145
+ raise ForbiddenAppNameError.new(app) if FORBIDDEN_APP_NAMES.include?(app)
130
146
 
131
147
  normalized_database ||= normalize_database(database)
132
148
 
@@ -138,17 +154,18 @@ module Hanami
138
154
  head: head,
139
155
  skip_assets: skip_assets,
140
156
  skip_db: skip_db,
157
+ skip_view: skip_view,
141
158
  database: normalized_database
142
159
  )
143
160
  generator.call(app, context: context) do
144
161
  if skip_install
145
162
  out.puts "Skipping installation, please enter `#{app}' directory and run `bundle exec hanami install'"
146
163
  else
147
- out.puts "Running Bundler install..."
164
+ out.puts "Running bundle install..."
148
165
  bundler.install!
149
166
 
150
167
  unless skip_assets
151
- out.puts "Running NPM install..."
168
+ out.puts "Running npm install..."
152
169
  system_call.call("npm", ["install"]).tap do |result|
153
170
  unless result.successful?
154
171
  puts "NPM ERROR:"
@@ -157,8 +174,11 @@ module Hanami
157
174
  end
158
175
  end
159
176
 
160
- out.puts "Running Hanami install..."
177
+ out.puts "Running hanami install..."
161
178
  run_install_command!(head: head)
179
+
180
+ out.puts "Initializing git repository..."
181
+ init_git_repository
162
182
  end
163
183
  end
164
184
  end
@@ -194,6 +214,16 @@ module Hanami
194
214
  end
195
215
  end
196
216
  end
217
+
218
+ # @api private
219
+ def init_git_repository
220
+ system_call.call("git", ["init"]).tap do |result|
221
+ unless result.successful?
222
+ out.puts "WARNING: Failed to initialize git repository"
223
+ out.puts(result.err.lines.map { |line| line.prepend(" ") })
224
+ end
225
+ end
226
+ end
197
227
  end
198
228
  end
199
229
  end
@@ -44,6 +44,24 @@ module Hanami
44
44
  end
45
45
  end
46
46
 
47
+ # @api public
48
+ class FileAlreadyExistsError < Error
49
+ ERROR_MESSAGE = <<~ERROR.chomp
50
+ The file `%{file_path}` could not be generated because it already exists.
51
+ ERROR
52
+
53
+ def initialize(file_path)
54
+ super(ERROR_MESSAGE % {file_path:})
55
+ end
56
+ end
57
+
58
+ # @api public
59
+ class ForbiddenAppNameError < Error
60
+ def initialize(name)
61
+ super("Cannot create new Hanami app with the name: `#{name}'")
62
+ end
63
+ end
64
+
47
65
  # @since 2.0.0
48
66
  # @api public
49
67
  class MissingSliceError < Error
@@ -100,6 +118,14 @@ module Hanami
100
118
  end
101
119
  end
102
120
 
121
+ # @since 2.2.0
122
+ # @api public
123
+ class DatabaseExistenceCheckError < Error
124
+ def initialize(original_message)
125
+ super("Could not check if the database exists. Error message:\n#{original_message}")
126
+ end
127
+ end
128
+
103
129
  # @since 2.2.0
104
130
  # @api public
105
131
  class ConflictingOptionsError < Error
@@ -14,6 +14,13 @@ module Hanami
14
14
  @out = out
15
15
  end
16
16
 
17
+ # @api private
18
+ def create(path, *content)
19
+ raise FileAlreadyExistsError.new(path) if exist?(path)
20
+
21
+ write(path, *content)
22
+ end
23
+
17
24
  # @since 2.0.0
18
25
  # @api private
19
26
  def write(path, *content)
@@ -33,10 +40,10 @@ module Hanami
33
40
  # @since 2.0.0
34
41
  # @api private
35
42
  def mkdir(path)
36
- unless exist?(path)
37
- super
38
- created(_path(path))
39
- end
43
+ return if exist?(path)
44
+
45
+ super
46
+ created(dir_path(path))
40
47
  end
41
48
 
42
49
  # @since 2.0.0
@@ -46,6 +53,13 @@ module Hanami
46
53
  super
47
54
  end
48
55
 
56
+ def touch(path)
57
+ return if exist?(path)
58
+
59
+ super
60
+ created(path)
61
+ end
62
+
49
63
  private
50
64
 
51
65
  attr_reader :out
@@ -77,10 +91,10 @@ module Hanami
77
91
  end
78
92
 
79
93
  def within_folder(path)
80
- out.puts "-> Within #{_path(path)}"
94
+ out.puts "-> Within #{dir_path(path)}"
81
95
  end
82
96
 
83
- def _path(path)
97
+ def dir_path(path)
84
98
  path + ::File::SEPARATOR
85
99
  end
86
100
  end