hanami-cli 2.1.0.beta2 → 2.1.0.rc2

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 (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