hanami-cli 2.1.0.beta2 → 2.1.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/Gemfile +2 -0
  4. data/lib/hanami/cli/commands/app/assets/command.rb +2 -29
  5. data/lib/hanami/cli/commands/app/assets/compile.rb +1 -1
  6. data/lib/hanami/cli/commands/app/assets/watch.rb +1 -5
  7. data/lib/hanami/cli/commands/app/dev.rb +4 -16
  8. data/lib/hanami/cli/commands/app/generate/action.rb +25 -4
  9. data/lib/hanami/cli/commands/app/generate/part.rb +58 -0
  10. data/lib/hanami/cli/commands/app/install.rb +0 -28
  11. data/lib/hanami/cli/commands/app.rb +1 -0
  12. data/lib/hanami/cli/commands/gem/new.rb +28 -3
  13. data/lib/hanami/cli/generators/app/action.rb +46 -6
  14. data/lib/hanami/cli/generators/app/part/app_base_part.erb +9 -0
  15. data/lib/hanami/cli/generators/app/part/app_part.erb +13 -0
  16. data/lib/hanami/cli/generators/app/part/slice_base_part.erb +9 -0
  17. data/lib/hanami/cli/generators/app/part/slice_part.erb +13 -0
  18. data/lib/hanami/cli/generators/app/part.rb +101 -0
  19. data/lib/hanami/cli/generators/app/part_context.rb +98 -0
  20. data/lib/hanami/cli/generators/app/slice/app_layout.erb +1 -1
  21. data/lib/hanami/cli/generators/app/slice_context.rb +2 -2
  22. data/lib/hanami/cli/generators/context.rb +20 -2
  23. data/lib/hanami/cli/generators/gem/app/404.html +76 -5
  24. data/lib/hanami/cli/generators/gem/app/500.html +76 -5
  25. data/lib/hanami/cli/generators/gem/app/app_layout.erb +3 -3
  26. data/lib/hanami/cli/generators/gem/app/assets.js +14 -0
  27. data/lib/hanami/cli/generators/gem/app/dev +8 -0
  28. data/lib/hanami/cli/generators/gem/app/package.json.erb +11 -0
  29. data/lib/hanami/cli/generators/gem/app/puma.erb +37 -7
  30. data/lib/hanami/cli/generators/gem/app/routes.erb +1 -1
  31. data/lib/hanami/cli/generators/gem/app.rb +12 -3
  32. data/lib/hanami/cli/generators/version.rb +14 -2
  33. data/lib/hanami/cli/version.rb +1 -1
  34. metadata +13 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 85a528645e9205c15ccdf4b81cc32a3fb0789a8a7f07c56554c1b8b0e66b9e91
4
- data.tar.gz: 5937fd333d11a7f5131d0298dc609c8a92e233249bc0b2e9e8f28705977f20b4
3
+ metadata.gz: aa0dbea6bc97ed3a7d08b703e7fd9091def9a8326196d36c071fb8e3e9be654b
4
+ data.tar.gz: dc27646ce5d395932bd2fbddef1678cb5445959494270b8d5f149462a8648e90
5
5
  SHA512:
6
- metadata.gz: df19a905d03fec4665ce80dd89730dee6337312e32e0e923e9424a387d66b8f9605e8392a5a73bd4627251b96c57b4a95f4df32062d8a53d42041d69841c10d6
7
- data.tar.gz: e4931266c0517de1bb33bc0f862c73da58366d6d5707c0eacf48fd38b08b8d7ae767e8fd7c81f1f048b96ffda5977674e556e281c51ab1f2a4eb30c6ceaa7ade
6
+ metadata.gz: ac1ab34a920c7fea44106a75c8c7e8d1bb0e475ff1e48ce2ef9b87912e9da6c83aa847ddf56661db13a01551fd58e098245b6d8ac277ca7143c69681d97d5ece
7
+ data.tar.gz: 706d1bb79523ba8a7efd9281267de12a0f275be0b60374c619b132371e0339335e6a4112b7a53ed5951e57293799aea1b4a89ac5bf6f9ee718daec4e1c44ee45
data/CHANGELOG.md CHANGED
@@ -2,6 +2,43 @@
2
2
 
3
3
  Hanami Command Line Interface
4
4
 
5
+ ## v2.1.0.rc2 - 2023-11-08
6
+
7
+ ### Added
8
+
9
+ - [Tim Riley] Add `--skip-tests` for `hanami generate` commands. This CLI option will skip tests generation.
10
+
11
+ ### Changed
12
+
13
+ - [Tim Riley] Set `"type": "module"` in package.json, enabling ES modules by default
14
+ - [Tim Riley] Rename `config/assets.mjs` to `config/assets.js` (use a plain `.js` file extension)
15
+
16
+ ### Fixed
17
+
18
+ - [Tim Riley] Use correct helper names in generated app layout
19
+ - [Luca Guidi] Ensure to generate apps with correct pre-release version of `hanami-assets` NPM package
20
+ - [Sean Collins] Print to stderr NPM installation errors when running `hanami install`
21
+ - [Sean Collins] Ensure to install missing gems after `hanami install` is ran
22
+
23
+ ## v2.1.0.rc1 - 2023-11-01
24
+
25
+ ### Added
26
+
27
+ - [Tim Riley] `hanami new` to generate `bin/dev` as configuration for `hanami dev`
28
+ - [Luca Guidi] Introducing `hanami generate part` to generate view parts
29
+
30
+ ### Fixed
31
+
32
+ - [Luca Guidi] `hanami new` generates a fully documented Puma configuration in `config/puma.rb`
33
+
34
+ ### Changed
35
+
36
+ - [Tim Riley] `hanami new` generates a `config/assets.mjs` as Assets configuration
37
+ - [Tim Riley] `hanami new` generates a leaner `package.json`
38
+ - [Tim Riley] `hanami new` doesn't generate a default root route anymore
39
+ - [Aaron Moodie & Tim Riley] `hanami new` to generate a redesigned 404 and 500 error pages
40
+ - [Luca Guidi] When generating a RESTful action, skip `create`, if `new` is present, and `update`, if `edit` is present
41
+
5
42
  ## v2.1.0.beta2 - 2023-10-04
6
43
 
7
44
  ### Added
data/Gemfile CHANGED
@@ -15,6 +15,8 @@ gem "hanami-controller", github: "hanami/controller", branch: "main"
15
15
  gem "hanami-router", github: "hanami/router", branch: "main"
16
16
  gem "hanami-utils", github: "hanami/utils", branch: "main"
17
17
 
18
+ gem "dry-files", github: "dry-rb/dry-files", branch: "main"
19
+
18
20
  gem "rack"
19
21
 
20
22
  group :test do
@@ -23,7 +23,7 @@ module Hanami
23
23
  def call(**)
24
24
  cmd, *args = cmd_with_args
25
25
 
26
- system_call.call(cmd, *args, env: env)
26
+ system_call.call(cmd, *args)
27
27
  end
28
28
 
29
29
  private
@@ -39,34 +39,7 @@ module Hanami
39
39
  # @since 2.1.0
40
40
  # @api private
41
41
  def cmd_with_args
42
- [
43
- config.package_manager_executable,
44
- config.package_manager_command,
45
- config.executable
46
- ]
47
- end
48
-
49
- # @since 2.1.0
50
- # @api private
51
- def env
52
- ENV.to_h.merge(
53
- "ESBUILD_ENTRY_POINTS" => entry_points,
54
- "ESBUILD_OUTDIR" => destination
55
- )
56
- end
57
-
58
- # @since 2.1.0
59
- # @api private
60
- def entry_points
61
- config.entry_points.map do |entry_point|
62
- escape(entry_point)
63
- end.join(" ")
64
- end
65
-
66
- # @since 2.1.0
67
- # @api private
68
- def destination
69
- escape(config.destination)
42
+ [config.package_manager_run_command, "assets"]
70
43
  end
71
44
 
72
45
  # @since 2.1.0
@@ -19,7 +19,7 @@ module Hanami
19
19
 
20
20
  if config.subresource_integrity.any?
21
21
  result << "--"
22
- result << "--sri=#{config.subresource_integrity.join(',')}"
22
+ result << "--sri=#{escape(config.subresource_integrity.join(','))}"
23
23
  end
24
24
 
25
25
  result
@@ -22,11 +22,7 @@ module Hanami
22
22
  # @since 2.1.0
23
23
  # @api private
24
24
  def cmd_with_args
25
- super +
26
- [
27
- "--",
28
- "--watch"
29
- ]
25
+ super + ["--", "--watch"]
30
26
  end
31
27
  end
32
28
  end
@@ -13,16 +13,6 @@ module Hanami
13
13
  # @api private
14
14
  desc "Start the application in development mode"
15
15
 
16
- # @since 2.1.0
17
- # @api private
18
- option :procfile, type: :string, desc: "Path to Procfile", aliases: ["-f"]
19
-
20
- # @since 2.1.0
21
- # @api private
22
- example [
23
- "-f /path/to/Procfile",
24
- ]
25
-
26
16
  # @since 2.1.0
27
17
  # @api private
28
18
  def initialize(interactive_system_call: InteractiveSystemCall.new, **)
@@ -32,8 +22,8 @@ module Hanami
32
22
 
33
23
  # @since 2.1.0
34
24
  # @api private
35
- def call(procfile: nil, **)
36
- bin, args = executable(procfile: procfile)
25
+ def call(**)
26
+ bin, args = executable
37
27
  interactive_system_call.call(bin, *args)
38
28
  end
39
29
 
@@ -45,10 +35,8 @@ module Hanami
45
35
 
46
36
  # @since 2.1.0
47
37
  # @api private
48
- def executable(procfile: nil)
49
- # TODO: support other implementations of Foreman
50
- # See: https://github.com/ddollar/foreman#ports
51
- ["foreman", ["start", "-f", procfile || "Procfile.dev"]]
38
+ def executable
39
+ [::File.join("bin", "dev")]
52
40
  end
53
41
  end
54
42
  end
@@ -22,12 +22,25 @@ module Hanami
22
22
  DEFAULT_SKIP_VIEW = false
23
23
  private_constant :DEFAULT_SKIP_VIEW
24
24
 
25
+ DEFAULT_SKIP_TESTS = false
26
+ private_constant :DEFAULT_SKIP_TESTS
27
+
25
28
  argument :name, required: true, desc: "Action name"
26
29
  option :url, required: false, type: :string, desc: "Action URL"
27
30
  option :http, required: false, type: :string, desc: "Action HTTP method"
28
31
  # option :format, required: false, type: :string, default: DEFAULT_FORMAT, desc: "Template format"
29
- option :skip_view, required: false, type: :boolean, default: DEFAULT_SKIP_VIEW,
30
- desc: "Skip view and template generation"
32
+ option \
33
+ :skip_view,
34
+ required: false,
35
+ type: :boolean,
36
+ default: DEFAULT_SKIP_VIEW,
37
+ desc: "Skip view and template generation"
38
+ option \
39
+ :skip_tests,
40
+ required: false,
41
+ type: :boolean,
42
+ default: DEFAULT_SKIP_TESTS,
43
+ desc: "Skip test generation"
31
44
  option :slice, required: false, desc: "Slice name"
32
45
 
33
46
  # rubocop:disable Layout/LineLength
@@ -60,8 +73,16 @@ module Hanami
60
73
 
61
74
  # @since 2.0.0
62
75
  # @api private
63
- def call(name:, url: nil, http: nil, format: DEFAULT_FORMAT, skip_view: DEFAULT_SKIP_VIEW, slice: nil,
64
- context: nil, **)
76
+ def call(
77
+ name:,
78
+ url: nil,
79
+ http: nil,
80
+ format: DEFAULT_FORMAT,
81
+ skip_view: DEFAULT_SKIP_VIEW,
82
+ skip_tests: DEFAULT_SKIP_TESTS, # rubocop:disable Lint/UnusedMethodArgument
83
+ slice: nil,
84
+ context: nil, **
85
+ )
65
86
  slice = inflector.underscore(Shellwords.shellescape(slice)) if slice
66
87
  name = naming.action_name(name)
67
88
  *controller, action = name.split(ACTION_SEPARATOR)
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/inflector"
4
+ require "dry/files"
5
+ require "shellwords"
6
+
7
+ module Hanami
8
+ module CLI
9
+ module Commands
10
+ module App
11
+ module Generate
12
+ # @since 2.1.0
13
+ # @api private
14
+ class Part < App::Command
15
+ DEFAULT_SKIP_TESTS = false
16
+ private_constant :DEFAULT_SKIP_TESTS
17
+
18
+ argument :name, required: true, desc: "Part name"
19
+ option :slice, required: false, desc: "Slice name"
20
+ option \
21
+ :skip_tests,
22
+ required: false,
23
+ type: :boolean,
24
+ default: DEFAULT_SKIP_TESTS,
25
+ desc: "Skip test generation"
26
+
27
+ example [
28
+ %(book (MyApp::Views::Parts::Book)),
29
+ %(book --slice=admin (Admin::Views::Parts::Book)),
30
+ ]
31
+ attr_reader :generator
32
+ private :generator
33
+
34
+ # @since 2.0.0
35
+ # @api private
36
+ def initialize(
37
+ fs: Hanami::CLI::Files.new,
38
+ inflector: Dry::Inflector.new,
39
+ generator: Generators::App::Part.new(fs: fs, inflector: inflector),
40
+ **
41
+ )
42
+ @generator = generator
43
+ super(fs: fs)
44
+ end
45
+
46
+ # @since 2.0.0
47
+ # @api private
48
+ def call(name:, slice: nil, skip_tests: DEFAULT_SKIP_TESTS, **) # rubocop:disable Lint/UnusedMethodArgument
49
+ slice = inflector.underscore(Shellwords.shellescape(slice)) if slice
50
+
51
+ generator.call(app.namespace, name, slice)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -33,37 +33,9 @@ module Hanami
33
33
  # @api private
34
34
  option :head, type: :boolean, desc: "Install head deps", default: DEFAULT_HEAD
35
35
 
36
- # @since 2.1.0
37
- # @api private
38
- def initialize(system_call: SystemCall.new, **)
39
- @system_call = system_call
40
- super()
41
- end
42
-
43
36
  # @since 2.0.0
44
37
  # @api private
45
38
  def call(head: DEFAULT_HEAD, **)
46
- install_hanami_assets!(head: head)
47
- end
48
-
49
- private
50
-
51
- # @since 2.1.0
52
- # @api private
53
- attr_reader :system_call
54
-
55
- # @since 2.1.0
56
- # @api private
57
- def install_hanami_assets!(head:)
58
- return unless Hanami.bundled?("hanami-assets")
59
-
60
- system_call.call("npm", ["init", "-y"])
61
-
62
- if head
63
- system_call.call("npm", %w[install https://github.com/hanami/assets-js])
64
- else
65
- system_call.call("npm", %w[install hanami-assets])
66
- end
67
39
  end
68
40
  end
69
41
  end
@@ -31,6 +31,7 @@ module Hanami
31
31
  prefix.register "slice", Generate::Slice
32
32
  prefix.register "action", Generate::Action
33
33
  prefix.register "view", Generate::View
34
+ prefix.register "part", Generate::Part
34
35
  end
35
36
  end
36
37
  end
@@ -58,6 +58,8 @@ module Hanami
58
58
  ]
59
59
  # rubocop:enable Layout/LineLength
60
60
 
61
+ # rubocop:disable Metrics/ParameterLists
62
+
61
63
  # @since 2.0.0
62
64
  # @api private
63
65
  def initialize(
@@ -65,13 +67,19 @@ module Hanami
65
67
  inflector: Dry::Inflector.new,
66
68
  bundler: CLI::Bundler.new(fs: fs),
67
69
  generator: Generators::Gem::App.new(fs: fs, inflector: inflector),
70
+ system_call: SystemCall.new,
68
71
  **other
69
72
  )
70
73
  @bundler = bundler
71
74
  @generator = generator
75
+ @system_call = system_call
72
76
  super(fs: fs, inflector: inflector, **other)
73
77
  end
74
78
 
79
+ # rubocop:enable Metrics/ParameterLists
80
+
81
+ # rubocop:disable Metrics/AbcSize
82
+
75
83
  # @since 2.0.0
76
84
  # @api private
77
85
  def call(app:, head: HEAD_DEFAULT, skip_install: SKIP_INSTALL_DEFAULT, skip_assets: SKIP_ASSETS_DEFAULT, **)
@@ -88,22 +96,39 @@ module Hanami
88
96
  else
89
97
  out.puts "Running Bundler install..."
90
98
  bundler.install!
99
+
100
+ unless skip_assets
101
+ out.puts "Running npm install..."
102
+ system_call.call("npm", ["install"]).tap do |result|
103
+ unless result.successful?
104
+ puts "NPM ERROR:"
105
+ puts(result.err.lines.map { |line| line.prepend(" ") })
106
+ end
107
+ end
108
+ end
109
+
91
110
  out.puts "Running Hanami install..."
92
- run_install_commmand!(head: head)
111
+ run_install_command!(head: head)
93
112
  end
94
113
  end
95
114
  end
96
115
  end
116
+ # rubocop:enable Metrics/AbcSize
97
117
 
98
118
  private
99
119
 
100
120
  attr_reader :bundler
101
121
  attr_reader :generator
122
+ attr_reader :system_call
102
123
 
103
- def run_install_commmand!(head:)
124
+ def run_install_command!(head:)
104
125
  head_flag = head ? " --head" : ""
105
126
  bundler.exec("hanami install#{head_flag}").tap do |result|
106
- raise HanamiInstallError.new(result.err) unless result.successful?
127
+ if result.successful?
128
+ bundler.exec("check").successful? || bundler.exec("install")
129
+ else
130
+ raise HanamiInstallError.new(result.err)
131
+ end
107
132
  end
108
133
  end
109
134
  end
@@ -56,6 +56,14 @@ module Hanami
56
56
  }.freeze
57
57
  private_constant :ROUTE_RESTFUL_URL_SUFFIXES
58
58
 
59
+ # @api private
60
+ # @since 2.1.0
61
+ RESTFUL_COUNTERPART_VIEWS = {
62
+ "create" => "new",
63
+ "update" => "edit"
64
+ }.freeze
65
+ private_constant :RESTFUL_COUNTERPART_VIEWS
66
+
59
67
  PATH_SEPARATOR = "/"
60
68
  private_constant :PATH_SEPARATOR
61
69
 
@@ -77,7 +85,7 @@ module Hanami
77
85
  fs.mkdir(directory = fs.join(slice_directory, "actions", controller))
78
86
  fs.write(fs.join(directory, "#{action}.rb"), t("slice_action.erb", context))
79
87
 
80
- unless skip_view
88
+ if generate_view?(skip_view, action, directory)
81
89
  fs.mkdir(directory = fs.join(slice_directory, "views", controller))
82
90
  fs.write(fs.join(directory, "#{action}.rb"), t("slice_view.erb", context))
83
91
 
@@ -97,12 +105,15 @@ module Hanami
97
105
  fs.mkdir(directory = fs.join("app", "actions", controller))
98
106
  fs.write(fs.join(directory, "#{action}.rb"), t("action.erb", context))
99
107
 
100
- unless skip_view
101
- fs.mkdir(directory = fs.join("app", "views", controller))
102
- fs.write(fs.join(directory, "#{action}.rb"), t("view.erb", context))
108
+ view = action
109
+ view_directory = fs.join("app", "views", controller)
103
110
 
104
- fs.mkdir(directory = fs.join("app", "templates", controller))
105
- fs.write(fs.join(directory, "#{action}.#{format}.erb"),
111
+ if generate_view?(skip_view, view, view_directory)
112
+ fs.mkdir(view_directory)
113
+ fs.write(fs.join(view_directory, "#{view}.rb"), t("view.erb", context))
114
+
115
+ fs.mkdir(template_directory = fs.join("app", "templates", controller))
116
+ fs.write(fs.join(template_directory, "#{view}.#{format}.erb"),
106
117
  t(template_with_format_ext("template", format), context))
107
118
  end
108
119
  end
@@ -117,6 +128,35 @@ module Hanami
117
128
  http)} "#{route_url(controller, action, url)}", to: "#{controller.join('.')}.#{action}")
118
129
  end
119
130
 
131
+ # @api private
132
+ # @since 2.1.0
133
+ def generate_view?(skip_view, view, directory)
134
+ return false if skip_view
135
+ return generate_restful_view?(view, directory) if rest_view?(view)
136
+
137
+ true
138
+ end
139
+
140
+ # @api private
141
+ # @since 2.1.0
142
+ def generate_restful_view?(view, directory)
143
+ corresponding_action = corresponding_restful_view(view)
144
+
145
+ !fs.exist?(fs.join(directory, "#{corresponding_action}.rb"))
146
+ end
147
+
148
+ # @api private
149
+ # @since 2.1.0
150
+ def rest_view?(view)
151
+ RESTFUL_COUNTERPART_VIEWS.keys.include?(view)
152
+ end
153
+
154
+ # @api private
155
+ # @since 2.1.0
156
+ def corresponding_restful_view(view)
157
+ RESTFUL_COUNTERPART_VIEWS.fetch(view, nil)
158
+ end
159
+
120
160
  def template_with_format_ext(name, format)
121
161
  ext =
122
162
  case format.to_sym
@@ -0,0 +1,9 @@
1
+ # auto_register: false
2
+ # frozen_string_literal: true
3
+
4
+ module <%= camelized_app_name %>
5
+ module Views
6
+ class Part < Hanami::View::Part
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ # auto_register: false
2
+ # frozen_string_literal: true
3
+
4
+ module <%= camelized_app_name %>
5
+ module Views
6
+ module Parts
7
+ <%= module_namespace_declaration -%>
8
+ <%= module_namespace_offset %>class <%= camelized_name %> < <%= camelized_app_name %>::Views::Part
9
+ <%= module_namespace_offset %>end
10
+ <%= module_namespace_end -%>
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ # auto_register: false
2
+ # frozen_string_literal: true
3
+
4
+ module <%= camelized_slice_name %>
5
+ module Views
6
+ class Part < <%= camelized_app_name %>::Views::Part
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ # auto_register: false
2
+ # frozen_string_literal: true
3
+
4
+ module <%= camelized_slice_name %>
5
+ module Views
6
+ module Parts
7
+ <%= module_namespace_declaration -%>
8
+ <%= module_namespace_offset %>class <%= camelized_name %> < <%= camelized_slice_name %>::Views::Part
9
+ <%= module_namespace_offset %>end
10
+ <%= module_namespace_end -%>
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+ require "dry/files"
5
+ require_relative "../../errors"
6
+
7
+ module Hanami
8
+ module CLI
9
+ module Generators
10
+ module App
11
+ # @since 2.1.0
12
+ # @api private
13
+ class Part
14
+ # @since 2.1.0
15
+ # @api private
16
+ def initialize(fs:, inflector:)
17
+ @fs = fs
18
+ @inflector = inflector
19
+ end
20
+
21
+ # @since 2.1.0
22
+ # @api private
23
+ def call(app, key, slice)
24
+ context = PartContext.new(inflector, app, slice, key)
25
+
26
+ if slice
27
+ generate_for_slice(context, slice)
28
+ else
29
+ generate_for_app(context)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ # @since 2.1.0
36
+ # @api private
37
+ attr_reader :fs
38
+
39
+ # @since 2.1.0
40
+ # @api private
41
+ attr_reader :inflector
42
+
43
+ # @since 2.1.0
44
+ # @api private
45
+ def generate_for_slice(context, slice)
46
+ slice_directory = fs.join("slices", slice)
47
+ raise MissingSliceError.new(slice) unless fs.directory?(slice_directory)
48
+
49
+ generate_base_part_for_app(context)
50
+ generate_base_part_for_slice(context, slice)
51
+
52
+ fs.mkdir(directory = fs.join(slice_directory, "views", "parts", *context.underscored_namespace))
53
+ fs.write(fs.join(directory, "#{context.underscored_name}.rb"), t("slice_part.erb", context))
54
+ end
55
+
56
+ # @since 2.1.0
57
+ # @api private
58
+ def generate_for_app(context)
59
+ generate_base_part_for_app(context)
60
+
61
+ fs.mkdir(directory = fs.join("app", "views", "parts", *context.underscored_namespace))
62
+ fs.write(fs.join(directory, "#{context.underscored_name}.rb"), t("app_part.erb", context))
63
+ end
64
+
65
+ # @since 2.1.0
66
+ # @api private
67
+ def generate_base_part_for_app(context)
68
+ path = fs.join("app", "views", "part.rb")
69
+ return if fs.exist?(path)
70
+
71
+ fs.write(path, t("app_base_part.erb", context))
72
+ end
73
+
74
+ # @since 2.1.0
75
+ # @api private
76
+ def generate_base_part_for_slice(context, slice)
77
+ path = fs.join("slices", slice, "views", "part.rb")
78
+ return if fs.exist?(path)
79
+
80
+ fs.write(path, t("slice_base_part.erb", context))
81
+ end
82
+
83
+ # @since 2.1.0
84
+ # @api private
85
+ def template(path, context)
86
+ require "erb"
87
+
88
+ ERB.new(
89
+ File.read(__dir__ + "/part/#{path}"),
90
+ trim_mode: "-"
91
+ ).result(context.ctx)
92
+ end
93
+
94
+ # @since 2.1.0
95
+ # @api private
96
+ alias_method :t, :template
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "slice_context"
4
+ require "dry/files/path"
5
+
6
+ module Hanami
7
+ module CLI
8
+ module Generators
9
+ # @since 2.1.0
10
+ # @api private
11
+ module App
12
+ # @since 2.1.0
13
+ # @api private
14
+ class PartContext < SliceContext
15
+ # TODO: move these constants somewhere that will let us reuse them
16
+
17
+ # @since 2.1.0
18
+ # @api private
19
+ KEY_SEPARATOR = "."
20
+ private_constant :KEY_SEPARATOR
21
+
22
+ # @since 2.1.0
23
+ # @api private
24
+ INDENTATION = " "
25
+ private_constant :INDENTATION
26
+
27
+ # @since 2.1.0
28
+ # @api private
29
+ OFFSET = INDENTATION * 2
30
+ private_constant :OFFSET
31
+
32
+ # @since 2.1.0
33
+ # @api private
34
+ attr_reader :key
35
+
36
+ # @since 2.1.0
37
+ # @api private
38
+ def initialize(inflector, app, slice, key)
39
+ @key = key
40
+ super(inflector, app, slice, nil)
41
+ end
42
+
43
+ # @since 2.1.0
44
+ # @api private
45
+ def namespaces
46
+ @namespaces ||= key.split(KEY_SEPARATOR)[..-2]
47
+ end
48
+
49
+ # @since 2.1.0
50
+ # @api private
51
+ def name
52
+ @name ||= key.split(KEY_SEPARATOR)[-1]
53
+ end
54
+
55
+ # @since 2.1.0
56
+ # @api private
57
+ def camelized_name
58
+ inflector.camelize(name)
59
+ end
60
+
61
+ # @since 2.1.0
62
+ # @api private
63
+ def underscored_namespace
64
+ namespaces.map { inflector.underscore(_1) }
65
+ end
66
+
67
+ # @since 2.1.0
68
+ # @api private
69
+ def underscored_name
70
+ inflector.underscore(name)
71
+ end
72
+
73
+ # @since 2.1.0
74
+ # @api private
75
+ def module_namespace_declaration
76
+ namespaces.each_with_index.map { |token, i|
77
+ "#{OFFSET}#{INDENTATION * i}module #{inflector.camelize(token)}"
78
+ }.join($/)
79
+ end
80
+
81
+ # @since 2.1.0
82
+ # @api private
83
+ def module_namespace_end
84
+ namespaces.each_with_index.map { |_, i|
85
+ "#{OFFSET}#{INDENTATION * i}end"
86
+ }.reverse.join($/)
87
+ end
88
+
89
+ # @since 2.1.0
90
+ # @api private
91
+ def module_namespace_offset
92
+ "#{OFFSET}#{INDENTATION * namespaces.count}"
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title><%= humanized_app_name %> - <%= humanized_slice_name %></title>
7
7
  <%- if bundled_assets? -%>
8
- <%%= favicon %>
8
+ <%%= favicon_tag %>
9
9
  <%= stylesheet_erb_tag %>
10
10
  <%- end -%>
11
11
  </head>
@@ -38,13 +38,13 @@ module Hanami
38
38
  # @since 2.1.0
39
39
  # @api private
40
40
  def stylesheet_erb_tag
41
- %(<%= css "#{slice}/app" %>)
41
+ %(<%= stylesheet_tag "#{slice}/app" %>)
42
42
  end
43
43
 
44
44
  # @since 2.1.0
45
45
  # @api private
46
46
  def javascript_erb_tag
47
- %(<%= js "#{slice}/app" %>)
47
+ %(<%= javascript_tag "#{slice}/app" %>)
48
48
  end
49
49
 
50
50
  private
@@ -27,12 +27,12 @@ module Hanami
27
27
  def hanami_gem(name)
28
28
  gem_name = name == "hanami" ? "hanami" : "hanami-#{name}"
29
29
 
30
- %(gem "#{gem_name}", #{hanami_version(name)})
30
+ %(gem "#{gem_name}", #{hanami_gem_version(name)})
31
31
  end
32
32
 
33
33
  # @since 2.0.0
34
34
  # @api private
35
- def hanami_version(gem_name)
35
+ def hanami_gem_version(gem_name)
36
36
  if hanami_head?
37
37
  %(github: "hanami/#{gem_name}", branch: "main")
38
38
  else
@@ -40,6 +40,16 @@ module Hanami
40
40
  end
41
41
  end
42
42
 
43
+ # @since 2.1.0
44
+ # @api private
45
+ def hanami_assets_npm_package
46
+ if hanami_head?
47
+ %("hanami-assets": "hanami/assets-js#main")
48
+ else
49
+ %("hanami-assets": "#{Version.npm_package_requirement}")
50
+ end
51
+ end
52
+
43
53
  # @since 2.0.0
44
54
  # @api private
45
55
  def camelized_app_name
@@ -82,6 +92,14 @@ module Hanami
82
92
  Hanami.bundled?("hanami-assets")
83
93
  end
84
94
 
95
+ # @since 2.1.0
96
+ # @api private
97
+ #
98
+ # @see https://rubyreferences.github.io/rubychanges/3.1.html#values-in-hash-literals-and-keyword-arguments-can-be-omitted
99
+ def ruby_omit_hash_values?
100
+ RUBY_VERSION >= "3.1"
101
+ end
102
+
85
103
  private
86
104
 
87
105
  # @since 2.0.0
@@ -1,11 +1,82 @@
1
1
  <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>The page you were looking for doesn’t exist (404)</title>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>The page you were looking for doesn’t exist (404)</title>
7
+ <style>
8
+ :root {
9
+ --foreground-rgb: 0, 0, 0;
10
+ --background-rgb: 255, 255, 255;
11
+ --font-sans: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;
12
+ }
13
+
14
+ @media (prefers-color-scheme: dark) {
15
+ :root {
16
+ --foreground-rgb: 255, 255, 255;
17
+ --background-rgb: 0, 0, 0;
18
+ }
19
+ }
20
+
21
+ * {
22
+ box-sizing: border-box;
23
+ margin: 0;
24
+ padding: 0;
25
+ }
26
+
27
+ body,
28
+ html {
29
+ max-width: 100vw;
30
+ overflow-x: hidden;
31
+ font-size: 100%;
32
+ }
33
+
34
+ body {
35
+ color: rgb(var(--foreground-rgb));
36
+ background: rgb(var(--background-rgb));
37
+ font-family: var(--font-sans);
38
+ font-style: normal;
39
+ }
40
+
41
+ main {
42
+ display: flex;
43
+ flex-direction: column;
44
+ align-items: center;
45
+ justify-content: center;
46
+ height: 100vh;
47
+ padding: 0 4vw;
48
+ }
49
+
50
+ .message {
51
+ display: flex;
52
+ gap: 1rem;
53
+ flex-direction: column;
54
+ text-align: center;
55
+ }
56
+
57
+ .message h1 {
58
+ font-size: 2rem;
59
+ font-weight: 500;
60
+ }
61
+
62
+ p {
63
+ line-height: 1.6;
64
+ }
65
+
66
+ @media (prefers-color-scheme: dark) {
67
+ html {
68
+ color-scheme: dark;
69
+ }
70
+ }
71
+ </style>
5
72
  </head>
6
73
  <body>
7
74
  <!-- This file lives in public/404.html -->
8
- <h1>The page you were looking for doesn’t exist.</h1>
9
- <p>You may have mistyped the address or the page may have moved.</p>
75
+ <main>
76
+ <div class="message">
77
+ <h1>404</h1>
78
+ <p>The page you were looking for doesn’t exist.</p>
79
+ </div>
80
+ </main>
10
81
  </body>
11
82
  </html>
@@ -1,11 +1,82 @@
1
1
  <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>We’re sorry, but something went wrong (500)</title>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>We’re sorry, but something went wrong (500)</title>
7
+ <style>
8
+ :root {
9
+ --foreground-rgb: 0, 0, 0;
10
+ --background-rgb: 255, 255, 255;
11
+ --font-sans: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;
12
+ }
13
+
14
+ @media (prefers-color-scheme: dark) {
15
+ :root {
16
+ --foreground-rgb: 255, 255, 255;
17
+ --background-rgb: 0, 0, 0;
18
+ }
19
+ }
20
+
21
+ * {
22
+ box-sizing: border-box;
23
+ margin: 0;
24
+ padding: 0;
25
+ }
26
+
27
+ body,
28
+ html {
29
+ max-width: 100vw;
30
+ overflow-x: hidden;
31
+ font-size: 100%;
32
+ }
33
+
34
+ body {
35
+ color: rgb(var(--foreground-rgb));
36
+ background: rgb(var(--background-rgb));
37
+ font-family: var(--font-sans);
38
+ font-style: normal;
39
+ }
40
+
41
+ main {
42
+ display: flex;
43
+ flex-direction: column;
44
+ align-items: center;
45
+ justify-content: center;
46
+ height: 100vh;
47
+ padding: 0 4vw;
48
+ }
49
+
50
+ .message {
51
+ display: flex;
52
+ gap: 1rem;
53
+ flex-direction: column;
54
+ text-align: center;
55
+ }
56
+
57
+ .message h1 {
58
+ font-size: 2rem;
59
+ font-weight: 500;
60
+ }
61
+
62
+ p {
63
+ line-height: 1.6;
64
+ }
65
+
66
+ @media (prefers-color-scheme: dark) {
67
+ html {
68
+ color-scheme: dark;
69
+ }
70
+ }
71
+ </style>
5
72
  </head>
6
73
  <body>
7
74
  <!-- This file lives in public/500.html -->
8
- <h1>We’re sorry, but something went wrong.</h1>
9
- <p>If you are the application owner, check the logs for more information.</p>
75
+ <main>
76
+ <div class="message">
77
+ <h1>500</h1>
78
+ <p>We’re sorry, but something went wrong.</p>
79
+ </div>
80
+ </main>
10
81
  </body>
11
82
  </html>
@@ -5,14 +5,14 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title><%= humanized_app_name %></title>
7
7
  <%- if generate_assets? -%>
8
- <%%= favicon %>
9
- <%%= css "app" %>
8
+ <%%= favicon_tag %>
9
+ <%%= stylesheet_tag "app" %>
10
10
  <%- end -%>
11
11
  </head>
12
12
  <body>
13
13
  <%%= yield %>
14
14
  <%- if generate_assets? -%>
15
- <%%= js "app" %>
15
+ <%%= javascript_tag "app" %>
16
16
  <%- end -%>
17
17
  </body>
18
18
  </html>
@@ -0,0 +1,14 @@
1
+ import * as assets from "hanami-assets";
2
+
3
+ await assets.run();
4
+
5
+ // To provide additional esbuild (https://esbuild.github.io) options, use the following:
6
+ //
7
+ // await assets.run({
8
+ // esbuildOptionsFn: (args, esbuildOptions) => {
9
+ // // Add to esbuildOptions here. Use `args.watch` as a condition for different options for
10
+ // // compile vs watch.
11
+ //
12
+ // return esbuildOptions;
13
+ // }
14
+ // });
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env sh
2
+
3
+ if ! gem list foreman -i --silent; then
4
+ echo "Installing foreman..."
5
+ gem install foreman
6
+ fi
7
+
8
+ exec foreman start -f Procfile.dev "$@"
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "<%= underscored_app_name %>",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "assets": "node config/assets.js"
7
+ },
8
+ "dependencies": {
9
+ <%= hanami_assets_npm_package %>
10
+ }
11
+ }
@@ -1,17 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ #
4
+ # Environment and port
5
+ #
6
+ port ENV.fetch("<%= Hanami::Port::ENV_VAR %>", <%= Hanami::Port::DEFAULT %>)
7
+ environment ENV.fetch("HANAMI_ENV", "development")
8
+
9
+ #
10
+ # Threads within each Puma/Ruby process (aka worker)
11
+ #
12
+
13
+ # Configure the minimum and maximum number of threads to use to answer requests.
3
14
  max_threads_count = ENV.fetch("HANAMI_MAX_THREADS", 5)
4
15
  min_threads_count = ENV.fetch("HANAMI_MIN_THREADS") { max_threads_count }
16
+
5
17
  threads min_threads_count, max_threads_count
6
18
 
7
- port ENV.fetch("<%= Hanami::Port::ENV_VAR %>", <%= Hanami::Port::DEFAULT %>)
8
- environment ENV.fetch("HANAMI_ENV", "development")
9
- workers ENV.fetch("HANAMI_WEB_CONCURRENCY", 0)
19
+ #
20
+ # Workers (aka Puma/Ruby processes)
21
+ #
10
22
 
11
- if ENV.fetch("HANAMI_WEB_CONCURRENCY", 0) > 0
12
- on_worker_boot do
23
+ puma_concurrency = Integer(ENV.fetch("HANAMI_WEB_CONCURRENCY", 0))
24
+ puma_cluster_mode = puma_concurrency > 1
25
+
26
+ # How many worker (Puma/Ruby) processes to run.
27
+ # Typically this is set to the number of available cores.
28
+ workers puma_concurrency
29
+
30
+ #
31
+ # Cluster mode (aka multiple workers)
32
+ #
33
+
34
+ if puma_cluster_mode
35
+ # Preload the application before starting the workers. Only in cluster mode.
36
+ preload_app!
37
+
38
+ # Code to run immediately before master process forks workers (once on boot).
39
+ #
40
+ # These hooks can block if necessary to wait for background operations unknown
41
+ # to puma to finish before the process terminates. This can be used to close
42
+ # any connections to remote servers (database, redis, …) that were opened when
43
+ # preloading the code.
44
+ before_fork do
13
45
  Hanami.shutdown
14
46
  end
15
47
  end
16
-
17
- preload_app!
@@ -2,6 +2,6 @@
2
2
 
3
3
  module <%= camelized_app_name %>
4
4
  class Routes < Hanami::Routes
5
- root { "Hello from Hanami" }
5
+ # Add your routes here. See https://guides.hanamirb.org/routing/overview/ for details.
6
6
  end
7
7
  end
@@ -43,6 +43,9 @@ module Hanami
43
43
  fs.write("Procfile.dev", t("procfile.erb", context))
44
44
  fs.write("config.ru", t("config_ru.erb", context))
45
45
 
46
+ fs.write("bin/dev", file("dev"))
47
+ fs.chmod("bin/dev", 0o755)
48
+
46
49
  fs.write("config/app.rb", t("app.erb", context))
47
50
  fs.write("config/settings.rb", t("settings.erb", context))
48
51
  fs.write("config/routes.rb", t("routes.erb", context))
@@ -58,13 +61,15 @@ module Hanami
58
61
  fs.write("app/templates/layouts/app.html.erb", t("app_layout.erb", context))
59
62
 
60
63
  if context.generate_assets?
64
+ fs.write("package.json", t("package.json.erb", context))
65
+ fs.write("config/assets.js", file("assets.js"))
61
66
  fs.write("app/assets/js/app.js", t("app_js.erb", context))
62
67
  fs.write("app/assets/css/app.css", t("app_css.erb", context))
63
- fs.write("app/assets/images/favicon.ico", File.read(File.join(__dir__, "app", "favicon.ico")))
68
+ fs.write("app/assets/images/favicon.ico", file("favicon.ico"))
64
69
  end
65
70
 
66
- fs.write("public/404.html", File.read(File.join(__dir__, "app", "404.html")))
67
- fs.write("public/500.html", File.read(File.join(__dir__, "app", "500.html")))
71
+ fs.write("public/404.html", file("404.html"))
72
+ fs.write("public/500.html", file("500.html"))
68
73
  end
69
74
 
70
75
  def template(path, context)
@@ -77,6 +82,10 @@ module Hanami
77
82
  end
78
83
 
79
84
  alias_method :t, :template
85
+
86
+ def file(path)
87
+ File.read(File.join(__dir__, "app", path))
88
+ end
80
89
  end
81
90
  end
82
91
  end
@@ -9,7 +9,7 @@ module Hanami
9
9
  # @since 2.0.0
10
10
  # @api private
11
11
  def self.version
12
- return Hanami::VERSION if defined?(Hanami::VERSION)
12
+ return Hanami::VERSION if Hanami.const_defined?(:VERSION)
13
13
 
14
14
  Hanami::CLI::VERSION
15
15
  end
@@ -26,10 +26,22 @@ module Hanami
26
26
  "~> #{result}"
27
27
  end
28
28
 
29
+ def self.npm_package_requirement
30
+ result = version
31
+ # Change "2.1.0.beta2.1" to "2.1.0-beta.2" (the only format tolerable by `npm install`)
32
+ if prerelease?
33
+ result = result
34
+ .sub(/\.(alpha|beta|rc)/, '-\1')
35
+ .sub(/(alpha|beta|rc)(\d+)(?:\.\d+)?\Z/, '\1.\2')
36
+ end
37
+
38
+ "^#{result}"
39
+ end
40
+
29
41
  # @since 2.0.0
30
42
  # @api private
31
43
  def self.prerelease?
32
- version =~ /alpha|beta|rc/
44
+ version.match?(/alpha|beta|rc/)
33
45
  end
34
46
 
35
47
  # @example
@@ -6,6 +6,6 @@ module Hanami
6
6
  #
7
7
  # @api public
8
8
  # @since 2.0.0
9
- VERSION = "2.1.0.beta2"
9
+ VERSION = "2.1.0.rc2"
10
10
  end
11
11
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hanami-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0.beta2
4
+ version: 2.1.0.rc2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Luca Guidi
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-10-04 00:00:00.000000000 Z
11
+ date: 2023-11-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -216,6 +216,7 @@ files:
216
216
  - lib/hanami/cli/commands/app/dev.rb
217
217
  - lib/hanami/cli/commands/app/generate.rb
218
218
  - lib/hanami/cli/commands/app/generate/action.rb
219
+ - lib/hanami/cli/commands/app/generate/part.rb
219
220
  - lib/hanami/cli/commands/app/generate/slice.rb
220
221
  - lib/hanami/cli/commands/app/generate/view.rb
221
222
  - lib/hanami/cli/commands/app/install.rb
@@ -237,6 +238,12 @@ files:
237
238
  - lib/hanami/cli/generators/app/action/template.html.erb
238
239
  - lib/hanami/cli/generators/app/action/view.erb
239
240
  - lib/hanami/cli/generators/app/action_context.rb
241
+ - lib/hanami/cli/generators/app/part.rb
242
+ - lib/hanami/cli/generators/app/part/app_base_part.erb
243
+ - lib/hanami/cli/generators/app/part/app_part.erb
244
+ - lib/hanami/cli/generators/app/part/slice_base_part.erb
245
+ - lib/hanami/cli/generators/app/part/slice_part.erb
246
+ - lib/hanami/cli/generators/app/part_context.rb
240
247
  - lib/hanami/cli/generators/app/slice.rb
241
248
  - lib/hanami/cli/generators/app/slice/action.erb
242
249
  - lib/hanami/cli/generators/app/slice/app_css.erb
@@ -265,13 +272,16 @@ files:
265
272
  - lib/hanami/cli/generators/gem/app/app_css.erb
266
273
  - lib/hanami/cli/generators/gem/app/app_js.erb
267
274
  - lib/hanami/cli/generators/gem/app/app_layout.erb
275
+ - lib/hanami/cli/generators/gem/app/assets.js
268
276
  - lib/hanami/cli/generators/gem/app/config_ru.erb
277
+ - lib/hanami/cli/generators/gem/app/dev
269
278
  - lib/hanami/cli/generators/gem/app/env.erb
270
279
  - lib/hanami/cli/generators/gem/app/favicon.ico
271
280
  - lib/hanami/cli/generators/gem/app/gemfile.erb
272
281
  - lib/hanami/cli/generators/gem/app/gitignore.erb
273
282
  - lib/hanami/cli/generators/gem/app/helpers.erb
274
283
  - lib/hanami/cli/generators/gem/app/keep.erb
284
+ - lib/hanami/cli/generators/gem/app/package.json.erb
275
285
  - lib/hanami/cli/generators/gem/app/procfile.erb
276
286
  - lib/hanami/cli/generators/gem/app/puma.erb
277
287
  - lib/hanami/cli/generators/gem/app/rakefile.erb
@@ -320,7 +330,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
320
330
  - !ruby/object:Gem::Version
321
331
  version: 1.3.1
322
332
  requirements: []
323
- rubygems_version: 3.4.13
333
+ rubygems_version: 3.4.21
324
334
  signing_key:
325
335
  specification_version: 4
326
336
  summary: Hanami CLI