mbeditor 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b7a1448f753b338a539d853fc6bebeba0960d5e2652273f69a725b25773b505
4
- data.tar.gz: c20c3da8f5f8a27f3b3d3cd2a8d1038ac5451d2b1508c82ffe1e0eb272673070
3
+ metadata.gz: 720ab9c5f06c65a444d4e7a4d9204926a91e31941ed5d21df9a8cb234c322124
4
+ data.tar.gz: 3c31e3387d38ba23e997dd71051328678bd225ea6ec2078c7d8cae72b44b09c6
5
5
  SHA512:
6
- metadata.gz: 7ff05b972124d55ba7074c9d2afe2c7fc82648b2b88ff884126832d57e9cf232712dd814768728ad1799eae448663845555b9ccf4cd696a06d4351406aeeed59
7
- data.tar.gz: c61f528b91b20203b6667c53abb58e1e02dc7230d91b251e039bc1bf5f99cd660836921437c1c23e9295162dd8aded1fec11acc3972455f6ebfb767c5f2df76f
6
+ metadata.gz: aa5177a8fb2e8511a4452dc33d3965c5838001991883360e2767ad598edefcb0b3f4feeeb54cd9692b0d7e7527cdf87463faeef2713edcb48cdd672aadfc961b
7
+ data.tar.gz: 1634701e852db9e3273f0217e5268c6298f3443fdfc5b598d53cfff6a462771a754c7f4ed187cbc247429e0d19ba27913d188715e3846856c034f3382facab5b
data/CHANGELOG.md CHANGED
@@ -5,6 +5,37 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.7.3] - 2026-06-03
9
+
10
+ ### Fixed
11
+ - **Custom path reverse lookup** — moved the custom-path check in `extract_resource_names` before the `case` statement so paths under `app/assets/`, `app/javascript/`, etc. are handled. Stripped `_controller`/`_model`/`_helper`/`_service` suffixes from custom-path filenames in both backend and frontend for consistent label/grouping.
12
+ - **Schema modal `self.table_name` support** — reads `self.table_name` from the model file before falling back to `ActiveSupport::Inflector.tableize`, enabling custom table names.
13
+ - **Structure.sql broader schema-prefix regex** — handles quoted schemas, non-public schemas, and no prefix.
14
+ - **PostgreSQL type coverage** — expanded `sql_type_to_rails` with full PostgreSQL type coverage (timestamptz, double precision, citext, hstore, geometric types, etc.).
15
+ - **Schema read error handling** — broadened rescue in `try_schema_rb`/`try_structure_sql` to catch encoding errors.
16
+
17
+ ---
18
+
19
+ ## [0.7.2] - 2026-06-03
20
+
21
+ ### Changed
22
+ - **Changelog entries backfilled** — added full release notes for 0.7.1 (persistent undo history, `db/structure.sql` schema support, Rails panel fixes) so the What's New panel shows accurate content on upgrade.
23
+ - **Release workflow documented** — `CLAUDE.md` now describes the release process so future releases can be triggered with a single instruction.
24
+
25
+ ---
26
+
27
+ ## [0.7.1] - 2026-06-03
28
+
29
+ ### Added
30
+ - **Persistent undo history** — edit operations are captured in the browser and flushed to the server on save and tab close, then replayed the next time the file is opened. Undo/redo now reaches back further than the current session. Stale history is pruned automatically when old branches are cleaned up.
31
+ - **`db/structure.sql` schema support** — the model schema modal now reads `db/structure.sql` when `db/schema.rb` is absent (SQL-format migrations).
32
+
33
+ ### Fixed
34
+ - **Rails panel long filenames** — long filenames in the Rails panel are now truncated correctly and custom-path entries populate reliably.
35
+ - **Schema lookup error logging** — failures to parse the schema file now emit a diagnostic log entry to aid debugging.
36
+
37
+ ---
38
+
8
39
  ## [0.7.0] - 2026-05-21
9
40
 
10
41
  ### Added
data/README.md CHANGED
@@ -71,26 +71,61 @@ Mbeditor.configure do |config|
71
71
  # Optional Ruby/Rails side panel tuning
72
72
  # config.ruby_def_include_dirs = %w[app/models app/controllers app/helpers app/concerns]
73
73
  # config.related_files_custom_paths = %w[app/assets/javascripts/app app/policies]
74
+
75
+ # Resilient routing (see the "Resilient Routing" section below)
76
+ # config.mount_path = "/mbeditor" # explicit prefix override; auto-detected when nil
77
+ # config.resilient_routing = false # escape hatch; true keeps the editor up when host routes break
74
78
  end
75
79
  ```
76
80
 
77
- Available options:
78
-
79
- - `allowed_environments` controls which Rails environments can access the engine. Default: `[:development]`.
80
- - `authenticate_with` accepts a proc that runs as a `before_action` in all engine controllers. Use it to plug in the host app's authentication. The proc executes via `instance_exec` inside the engine controller, so it has access to `session`, `cookies`, `redirect_to`, and auth library class methods (e.g. Authlogic's `UserSession.find`), but not helper methods defined in the host app's `ApplicationController`. Default: `nil` (no authentication).
81
- - `authentication_cache_ttl` caches the authentication result in the session for the given number of seconds (default: `0`, no caching). Set to e.g. `300` to avoid calling `authenticate_with` on every request — useful when the lambda is expensive (e.g. calls Authlogic's `current_user`). Trade-off: if the user logs out of the host app, mbeditor remains accessible for up to TTL seconds.
82
- - `workspace_root` sets the root directory exposed by Mbeditor. Default: `Rails.root` from the host app.
83
- - `excluded_paths` hides files and directories from the tree and path-based operations. Entries without `/` match names anywhere in the workspace path; entries with `/` match relative paths and their descendants. Default: `%w[.git tmp log node_modules .bundle coverage vendor/bundle]`.
84
- - `rubocop_command` sets the command used for inline Ruby linting and formatting. Default: `"rubocop"`.
85
- - `test_framework` sets the test framework. `:minitest` or `:rspec`. Auto-detected from file suffix, `.rspec`, or `test`/`spec` directory when `nil`. Default: `nil`.
86
- - `test_command` overrides the full command used to run a test file. When `nil`, the engine picks `bin/rails test` (Minitest) or `bin/rspec` / `bundle exec rspec` (RSpec). Default: `nil`.
87
- - `test_timeout` sets the maximum seconds a test run may take before being killed. Default: `60`.
88
- - `redmine_enabled` enables issue lookup integration. Default: `false`.
89
- - `redmine_url` sets the Redmine base URL. Required when `redmine_enabled` is `true`.
90
- - `redmine_api_key` sets the Redmine API key. Required when `redmine_enabled` is `true`.
91
- - `redmine_ticket_source` controls how the current Redmine ticket is identified. `:commit` scans the 100 most recent branch commits for a `#123` reference in the commit message. `:branch` reads the leading digits from the branch name (e.g. `123-my-feature` → ticket 123). Default: `:commit`.
92
- - `ruby_def_include_dirs` sets which workspace-relative directories are searched when resolving Ruby go-to-definition jumps (e.g. clicking a class name). Add any extra directories that hold Ruby source files you want the engine to index. Default: `%w[app/models app/controllers app/helpers app/concerns]`.
93
- - `related_files_custom_paths` adds extra base directories to the Rails related-files side panel. For each entry the panel looks for a subdirectory named after the current resource (plural or singular) and lists its files. For example, `"app/assets/javascripts/app"` will surface `app/assets/javascripts/app/users/` when you are editing a `users` resource. Default: `[]`.
81
+ ### Core
82
+
83
+ | Option | Default | Description |
84
+ |--------|---------|-------------|
85
+ | `allowed_environments` | `[:development]` | Rails environments allowed to access the engine. |
86
+ | `workspace_root` | `Rails.root` | Root directory exposed by Mbeditor. |
87
+ | `excluded_paths` | `%w[.git tmp log node_modules .bundle coverage vendor/bundle]` | Files/directories hidden from the tree and path operations. Entries without `/` match a name anywhere in the path; entries with `/` match relative paths and their descendants. |
88
+ | `rubocop_command` | `"rubocop"` | Command used for inline Ruby linting and formatting. |
89
+
90
+ ### Authentication
91
+
92
+ | Option | Default | Description |
93
+ |--------|---------|-------------|
94
+ | `authenticate_with` | `nil` | Proc run as a `before_action` in all engine controllers. Executed via `instance_exec` inside the controller, so it has access to `session`, `cookies`, `redirect_to`, and auth-library class methods (e.g. Authlogic's `UserSession.find`) — but not helper methods from the host's `ApplicationController`. |
95
+ | `authentication_cache_ttl` | `0` | Seconds to cache the auth result in the session (`0` = no caching). Set e.g. `300` to avoid calling `authenticate_with` on every request when the proc is expensive. Trade-off: after host logout, mbeditor stays accessible for up to TTL seconds. |
96
+
97
+ ### Test runner
98
+
99
+ | Option | Default | Description |
100
+ |--------|---------|-------------|
101
+ | `test_framework` | `nil` | `:minitest` or `:rspec`. Auto-detected from file suffix, `.rspec`, or `test`/`spec` directory when `nil`. |
102
+ | `test_command` | `nil` | Full command used to run a test file. When `nil`, picks `bin/rails test` (Minitest) or `bin/rspec` / `bundle exec rspec` (RSpec). |
103
+ | `test_timeout` | `60` | Maximum seconds a test run may take before being killed. |
104
+
105
+ ### Redmine
106
+
107
+ | Option | Default | Description |
108
+ |--------|---------|-------------|
109
+ | `redmine_enabled` | `false` | Enables issue-lookup integration. |
110
+ | `redmine_url` | — | Redmine base URL. Required when `redmine_enabled` is `true`. |
111
+ | `redmine_api_key` | — | Redmine API key. Required when `redmine_enabled` is `true`. |
112
+ | `redmine_ticket_source` | `:commit` | How the current ticket is identified. `:commit` scans the 100 most recent branch commits for a `#123` reference; `:branch` reads leading digits from the branch name (e.g. `123-my-feature` → ticket 123). |
113
+
114
+ ### Ruby/Rails side panel
115
+
116
+ | Option | Default | Description |
117
+ |--------|---------|-------------|
118
+ | `ruby_def_include_dirs` | `%w[app/models app/controllers app/helpers app/concerns]` | Workspace-relative directories searched when resolving Ruby go-to-definition jumps. Add any extra dirs holding Ruby source you want indexed. |
119
+ | `related_files_custom_paths` | `[]` | Extra base directories for the Rails related-files side panel. For each entry the panel looks for a subdirectory named after the current resource (plural or singular). E.g. `"app/assets/javascripts/app"` surfaces `app/assets/javascripts/app/users/` when editing a `users` resource. |
120
+
121
+ ### Resilient routing
122
+
123
+ See [Resilient Routing](#resilient-routing) for details.
124
+
125
+ | Option | Default | Description |
126
+ |--------|---------|-------------|
127
+ | `mount_path` | `nil` | Explicit URL prefix to serve resilient routing from. When `nil`, auto-detected from your `mount Mbeditor::Engine, at: "..."` line on every healthy boot. Set only to override detection. |
128
+ | `resilient_routing` | `true` | Keeps mbeditor reachable when the host's `config/routes.rb` is broken, by serving its traffic from middleware that dispatches to a private route set. Set to `false` as an escape hatch: no middleware is inserted and the private set is never built. |
94
129
 
95
130
  ## Test Runner
96
131
 
@@ -111,6 +146,22 @@ The Test button appears in the editor toolbar for any `.rb` file when a `test/`
111
146
  - Minitest: `bin/rails test <file>` if `bin/rails` exists, otherwise `bundle exec ruby -Itest <file>`
112
147
  - RSpec: `bin/rspec <file>` if `bin/rspec` exists, otherwise `bundle exec rspec --format json <file>`
113
148
 
149
+ ## Resilient Routing
150
+
151
+ A broken `config/routes.rb` normally takes down the whole app — every request, including the one you'd use to fix it, raises while Rails tries to reload the route table. Mbeditor stays reachable anyway: it serves its own traffic from middleware that dispatches to a **private route set** built at boot and never registered with Rails' route reloader. When a bad route draw wipes the host route table, that private set survives the wipe, so `/mbeditor` keeps working while the rest of the app is down. This is on by default (`resilient_routing = true`); you don't need to configure anything.
152
+
153
+ **Custom mount paths work with zero configuration.** On every healthy boot, mbeditor detects the prefix from your `mount Mbeditor::Engine, at: "..."` line and remembers it. If the route table later breaks, the remembered prefix is used — so resilient routing serves from your custom mount, not just `/mbeditor`. Detection only ever updates from a *healthy* load, so a broken reload can never overwrite a good prefix. Set `config.mount_path` only if you want to override detection explicitly.
154
+
155
+ **What it cannot recover from: a host app that fails to *boot*.** Resilient routing relies on mbeditor's engine initializers running — that's when the middleware is inserted and the private route set is built. If the app can't boot at all, those initializers never run and there is nothing to fall back to. This includes:
156
+
157
+ - a syntax error or raised exception in `config/application.rb` or other engine/initializer code,
158
+ - a broken initializer in `config/initializers/`,
159
+ - a bad or incompatible gem that fails to load.
160
+
161
+ A *broken route* is recoverable in the browser; a *failed boot* is not. If the app won't boot, fix it from a terminal — mbeditor can't help until the app can start.
162
+
163
+ **Turning it off.** Set `config.resilient_routing = false` as an escape hatch (e.g. to debug a middleware-ordering conflict). With the flag off, no mbeditor middleware is inserted and the private route set is never built; mbeditor is then served only through the normal mount and shares the fate of a broken `config/routes.rb`.
164
+
114
165
  ## Keyboard Shortcuts
115
166
 
116
167
  | Shortcut | Action |
@@ -1631,13 +1631,12 @@ var MbeditorApp = function MbeditorApp() {
1631
1631
  if (p.startsWith(base + '/')) {
1632
1632
  var rest = p.slice(base.length + 1);
1633
1633
  var resource = rest.split('/')[0].replace(/\.[^.]+$/, '');
1634
- if (resource) {
1635
- var seg = resource.replace(/_/g, ' ').replace(/\b\w/g, function(c) { return c.toUpperCase(); });
1636
- return seg;
1637
- }
1634
+ // Strip Rails-style suffixes so custom-path files group with their controller/model
1635
+ resource = resource.replace(/_(controller|model|helper|service)$/, '');
1636
+ if (resource) { name = resource; break; }
1638
1637
  }
1639
1638
  }
1640
- return null;
1639
+ if (!name) return null;
1641
1640
  }
1642
1641
  var seg = (name || '').split('/').pop() || name || '';
1643
1642
  // Normalize plural→singular so views/users and models/user share one group
@@ -15,6 +15,7 @@ module Mbeditor
15
15
  before_action :verify_mbeditor_client, unless: -> { request.get? || request.head? }
16
16
 
17
17
  IMAGE_EXTENSIONS = %w[png jpg jpeg gif svg ico webp bmp avif].freeze
18
+ helper_method :mbeditor_base_path
18
19
 
19
20
  # GET /mbeditor — renders the IDE shell
20
21
  def index
@@ -545,8 +546,7 @@ module Mbeditor
545
546
 
546
547
  # GET /mbeditor/manifest.webmanifest — PWA manifest
547
548
  def pwa_manifest
548
- raw = root_path.chomp("/")
549
- base = raw.start_with?("/") || raw.empty? ? raw : "/#{raw}"
549
+ base = mbeditor_base_path
550
550
  manifest = {
551
551
  name: "Mbeditor — #{Rails.root.basename}",
552
552
  short_name: "Mbeditor",
@@ -787,6 +787,15 @@ module Mbeditor
787
787
 
788
788
  private
789
789
 
790
+ # Normalized base prefix mbeditor renders URLs against. Sourced from
791
+ # MountPath (not the engine `root_path` helper) so it still resolves when a
792
+ # broken host config/routes.rb has wiped Mbeditor::Engine.routes — the exact
793
+ # state resilient routing exists to survive.
794
+ def mbeditor_base_path
795
+ raw = Mbeditor::MountPath.resolve.chomp("/")
796
+ raw.start_with?("/") || raw.empty? ? raw : "/#{raw}"
797
+ end
798
+
790
799
  def history_file_path(branch, rel_path)
791
800
  branch_hash = Digest::SHA256.hexdigest(branch.to_s)[0, 16]
792
801
  file_hash = Digest::SHA256.hexdigest(rel_path.to_s)[0, 16]
@@ -124,6 +124,20 @@ module Mbeditor
124
124
  parts = relative_path.to_s.split("/")
125
125
  return nil unless parts.length >= 2
126
126
 
127
+ # Custom paths are checked first so they work regardless of top-level prefix.
128
+ # Paths under app/assets/, app/javascript/, etc. never reach the standard
129
+ # "app" branch, so the check must happen before the case statement.
130
+ Array(custom_paths).each do |base|
131
+ base = base.to_s.strip
132
+ next if base.empty?
133
+ next unless relative_path.start_with?("#{base}/")
134
+ rest = relative_path.delete_prefix("#{base}/")
135
+ resource = rest.split('/').first.to_s.sub(/\.[^.]+$/, '') # first segment, no extension
136
+ resource = resource.sub(/_(controller|model|helper|service)$/, '') # strip Rails suffixes
137
+ next if resource.empty?
138
+ return [pluralize(resource), singularize(resource)]
139
+ end
140
+
127
141
  case parts[0]
128
142
  when "app"
129
143
  case parts[1]
@@ -230,16 +244,6 @@ module Mbeditor
230
244
  end
231
245
 
232
246
  else
233
- # Custom path fallback — must be last
234
- Array(custom_paths).each do |base|
235
- base = base.to_s.strip
236
- next if base.empty?
237
- next unless relative_path.start_with?("#{base}/")
238
- rest = relative_path.delete_prefix("#{base}/")
239
- resource = rest.split('/').first.to_s.sub(/\.[^.]+$/, '') # first path segment, no extension
240
- next if resource.empty?
241
- return [pluralize(resource), singularize(resource)]
242
- end
243
247
  nil
244
248
  end
245
249
  end
@@ -40,8 +40,25 @@ module Mbeditor
40
40
  private
41
41
 
42
42
  # "User" → "users", "OrderItem" → "order_items", "Order Item" → "order_items"
43
+ # Also checks the model file for an explicit `self.table_name = "..."` declaration.
43
44
  def derive_table_name(model_name)
44
45
  normalized = model_name.delete(" ")
46
+
47
+ # Check model file for an explicit table_name override
48
+ singular = ActiveSupport::Inflector.underscore(normalized)
49
+ model_file = File.join(@workspace_root, "app", "models", "#{singular}.rb")
50
+ if File.exist?(model_file)
51
+ begin
52
+ source = File.read(model_file, encoding: "utf-8")
53
+ # Matches: self.table_name = "name" or = :name or = 'name'
54
+ if (m = source.match(/self\.table_name\s*=\s*[:"']([^"'\s]+)["']?/))
55
+ return m[1]
56
+ end
57
+ rescue StandardError
58
+ # fall through to default derivation
59
+ end
60
+ end
61
+
45
62
  ActiveSupport::Inflector.tableize(normalized)
46
63
  end
47
64
 
@@ -53,7 +70,7 @@ module Mbeditor
53
70
  begin
54
71
  content = File.read(schema_path, encoding: "utf-8")
55
72
  parse_schema_rb(content, table_name)
56
- rescue Errno::ENOENT, Errno::EACCES => e
73
+ rescue StandardError => e
57
74
  Rails.logger.debug("SchemaService: failed to read #{schema_path}: #{e.message}")
58
75
  nil
59
76
  end
@@ -67,7 +84,7 @@ module Mbeditor
67
84
  begin
68
85
  content = File.read(schema_path, encoding: "utf-8")
69
86
  parse_structure_sql(content, table_name)
70
- rescue Errno::ENOENT, Errno::EACCES => e
87
+ rescue StandardError => e
71
88
  Rails.logger.debug("SchemaService: failed to read #{schema_path}: #{e.message}")
72
89
  nil
73
90
  end
@@ -145,11 +162,13 @@ module Mbeditor
145
162
  # Handles: PostgreSQL, MySQL, SQLite with quoted/unquoted names
146
163
  # Ends with different delimiters: ); ENGINE...; or just );
147
164
  quoted_name = Regexp.escape(table_name)
165
+ # Schema prefix pattern: matches public., "public"., myschema., "myschema". or nothing.
166
+ schema_prefix = /(?:(?:"[^"]+"|`[^`]+`|\w+)\.)?/
148
167
  patterns = [
149
- # PostgreSQL with public schema: CREATE TABLE public."users" ( ... );
150
- /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:public\.)?["`]?#{quoted_name}["`]?\s*\(([\s\S]*?)\)\s*;/mi,
151
- # MySQL with ENGINE: CREATE TABLE `users` ( ... ) ENGINE=...;
152
- /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["`]?#{quoted_name}["`]?\s*\(([\s\S]*?)\)\s*(?:ENGINE|DEFAULT)/mi
168
+ # PostgreSQL: CREATE TABLE [schema.]table ( ... );
169
+ /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?#{schema_prefix}["`]?#{quoted_name}["`]?\s*\(([\s\S]*?)\)\s*;/mi,
170
+ # MySQL: CREATE TABLE [schema.]`table` ( ... ) ENGINE=...;
171
+ /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?#{schema_prefix}["`]?#{quoted_name}["`]?\s*\(([\s\S]*?)\)\s*(?:ENGINE|DEFAULT)/mi
153
172
  ]
154
173
 
155
174
  table_def = nil
@@ -240,6 +259,9 @@ module Mbeditor
240
259
  type_map = {
241
260
  'integer' => 'integer',
242
261
  'int' => 'integer',
262
+ 'int4' => 'integer',
263
+ 'int2' => 'integer',
264
+ 'int8' => 'bigint',
243
265
  'bigint' => 'bigint',
244
266
  'smallint' => 'integer',
245
267
  'bigserial' => 'bigint',
@@ -247,21 +269,50 @@ module Mbeditor
247
269
  'varchar' => 'string',
248
270
  'character varying' => 'string',
249
271
  'character' => 'string',
272
+ 'char' => 'string',
250
273
  'text' => 'text',
274
+ 'citext' => 'string',
251
275
  'boolean' => 'boolean',
252
276
  'bool' => 'boolean',
253
277
  'decimal' => 'decimal',
254
278
  'numeric' => 'decimal',
279
+ 'real' => 'float',
255
280
  'float' => 'float',
281
+ 'float4' => 'float',
282
+ 'float8' => 'float',
283
+ 'double precision' => 'float',
256
284
  'double' => 'float',
285
+ 'money' => 'decimal',
257
286
  'timestamp' => 'datetime',
287
+ 'timestamp without time zone' => 'datetime',
288
+ 'timestamp with time zone' => 'datetime',
289
+ 'timestamptz' => 'datetime',
258
290
  'datetime' => 'datetime',
259
291
  'date' => 'date',
260
292
  'time' => 'time',
293
+ 'time without time zone' => 'time',
294
+ 'time with time zone' => 'time',
295
+ 'interval' => 'string',
261
296
  'json' => 'json',
262
297
  'jsonb' => 'jsonb',
263
298
  'uuid' => 'uuid',
264
- 'bytea' => 'binary'
299
+ 'bytea' => 'binary',
300
+ 'bit' => 'string',
301
+ 'bit varying' => 'string',
302
+ 'inet' => 'string',
303
+ 'cidr' => 'string',
304
+ 'macaddr' => 'string',
305
+ 'xml' => 'string',
306
+ 'hstore' => 'hstore',
307
+ 'tsvector' => 'string',
308
+ 'ltree' => 'string',
309
+ 'point' => 'string',
310
+ 'line' => 'string',
311
+ 'lseg' => 'string',
312
+ 'box' => 'string',
313
+ 'path' => 'string',
314
+ 'polygon' => 'string',
315
+ 'circle' => 'string'
265
316
  }
266
317
 
267
318
  type_map[sql_type.downcase] || sql_type
@@ -7,7 +7,7 @@
7
7
  <meta name="theme-color" content="#1e1e2e" />
8
8
  <meta name="mobile-web-app-capable" content="yes" />
9
9
  <meta name="apple-mobile-web-app-capable" content="yes" />
10
- <% _raw_base = root_path.chomp('/'); pwa_base = (_raw_base.start_with?('/') || _raw_base.empty?) ? _raw_base : "/#{_raw_base}" %>
10
+ <% pwa_base = mbeditor_base_path %>
11
11
  <link rel="manifest" href="<%= "#{pwa_base}/manifest.webmanifest" %>" />
12
12
  <script>
13
13
  if ('serviceWorker' in navigator) {
data/config/routes.rb CHANGED
@@ -1,58 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Mbeditor::Engine.routes.draw do
4
- root to: 'editors#index'
5
-
6
- get 'ping', to: 'editors#ping'
7
- get 'workspace', to: 'editors#workspace'
8
- get 'files', to: 'editors#files'
9
- get 'file', to: 'editors#show'
10
- get 'raw', to: 'editors#raw'
11
- post 'file', to: 'editors#save'
12
- post 'create_file', to: 'editors#create_file'
13
- post 'create_dir', to: 'editors#create_dir'
14
- patch 'rename', to: 'editors#rename'
15
- delete 'delete', to: 'editors#destroy_path'
16
- get 'state', to: 'editors#state'
17
- post 'state', to: 'editors#save_state'
18
- get 'branch_state', to: 'editors#branch_state'
19
- post 'branch_state', to: 'editors#save_branch_state'
20
- post 'prune_branch_states', to: 'editors#prune_branch_states'
21
- get 'file_history', to: 'editors#file_history'
22
- post 'file_history', to: 'editors#save_file_history'
23
- get 'search', to: 'editors#search'
24
- post 'replace_in_files', to: 'editors#replace_in_files'
25
- get 'definition', to: 'editors#definition'
26
- get 'js_definition', to: 'editors#js_definition'
27
- get 'js_members', to: 'editors#js_members'
28
- get 'module_members', to: 'editors#module_members'
29
- get 'file_includes', to: 'editors#file_includes'
30
- get 'client_config', to: 'editors#client_config'
31
- get 'related_files', to: 'editors#related_files'
32
- get 'model_schema', to: 'editors#model_schema'
33
- get 'changelog', to: 'editors#changelog'
34
- get 'git_info', to: 'editors#git_info'
35
- get 'git_status', to: 'editors#git_status'
36
- get 'manifest.webmanifest', to: 'editors#pwa_manifest', format: false
37
- get 'sw.js', to: 'editors#pwa_sw', format: false
38
- get 'mbeditor-icon.svg', to: 'editors#pwa_icon', format: false
39
- get 'monaco_worker.js', to: 'editors#monaco_worker', format: false
40
- get 'ts_worker.js', to: 'editors#ts_worker', format: false
41
- get 'monaco-editor/*asset_path', to: 'editors#monaco_asset', format: false
42
- get 'min-maps/*asset_path', to: 'editors#monaco_asset', format: false
43
- post 'lint', to: 'editors#lint'
44
- post 'quick_fix', to: 'editors#quick_fix'
45
- post 'format', to: 'editors#format_file'
46
- post 'test', to: 'editors#run_test'
47
-
48
- # ── Git & Code Review ──────────────────────────────────────────────────────
49
- get 'git/diff', to: 'git#diff'
50
- get 'git/blame', to: 'git#blame'
51
- get 'git/file_history', to: 'git#file_history'
52
- get 'git/commit_graph', to: 'git#commit_graph'
53
- get 'git/commit_detail', to: 'git#commit_detail'
54
- get 'git/combined_diff', to: 'git#combined_diff'
55
-
56
- # Redmine integration (enabled via config.mbeditor.redmine_enabled)
57
- get 'redmine/issue/:id', to: 'git#redmine_issue', as: :redmine_issue
58
- end
3
+ # Routes are declared in Mbeditor::ROUTE_MAP (lib/mbeditor/route_map.rb), the
4
+ # single source of truth shared with the private, isolated route set. Drawing
5
+ # from it here keeps the engine route set and the private set from drifting.
6
+ Mbeditor::Engine.routes.draw(&Mbeditor::ROUTE_MAP)
@@ -7,7 +7,8 @@ module Mbeditor
7
7
  :test_framework, :test_command, :test_timeout,
8
8
  :authenticate_with, :authentication_cache_ttl,
9
9
  :lint_timeout, :base_branch_candidates, :git_timeout,
10
- :ruby_def_include_dirs, :related_files_custom_paths
10
+ :ruby_def_include_dirs, :related_files_custom_paths,
11
+ :mount_path, :resilient_routing
11
12
 
12
13
  def initialize
13
14
  @allowed_environments = [:development]
@@ -27,6 +28,8 @@ module Mbeditor
27
28
  @ruby_def_include_dirs = %w[app/models app/controllers app/helpers app/concerns]
28
29
  @related_files_custom_paths = []
29
30
  @authentication_cache_ttl = 0
31
+ @mount_path = nil # explicit URL prefix override; nil falls through to detection/"/mbeditor"
32
+ @resilient_routing = true # serve /mbeditor from middleware so the editor survives a broken host routes.rb; false is the escape hatch
30
33
  end
31
34
  end
32
35
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "mbeditor/rack/silence_ping_request"
4
4
  require "mbeditor/rack/handle_pending_migrations"
5
+ require "mbeditor/rack/resilient_router"
5
6
  require "mbeditor/cable_log_filter"
6
7
 
7
8
  module Mbeditor
@@ -12,6 +13,43 @@ module Mbeditor
12
13
  app.middleware.insert_before Rails::Rack::Logger, Mbeditor::Rack::SilencePingRequest
13
14
  end
14
15
 
16
+ initializer "mbeditor.resilient_routing" do |app|
17
+ # Serve mbeditor's own traffic from middleware that dispatches to a
18
+ # private, isolated route set, so the editor survives a broken host
19
+ # config/routes.rb. Disabling the flag is the escape hatch: no middleware
20
+ # is inserted and the private set is never built.
21
+ next unless Mbeditor.configuration.resilient_routing
22
+
23
+ # ActionDispatch::Reloader is only in the stack when reloading is enabled
24
+ # (the same condition Rails uses to add it). Sitting just above it means
25
+ # mbeditor requests are intercepted before any route reload can raise,
26
+ # while staying below DebugExceptions/ShowExceptions (mbeditor's own
27
+ # errors still get Rails error pages) and below Executor (dispatch runs
28
+ # inside app.executor's connection management).
29
+ if app.config.reloading_enabled?
30
+ app.middleware.insert_before ActionDispatch::Reloader, Mbeditor::Rack::ResilientRouter
31
+ end
32
+
33
+ # Build the private set at boot so it is ready before the first request
34
+ # and before the host route table can ever break.
35
+ Mbeditor::PrivateRoutes.route_set
36
+
37
+ # Auto-detect the mount prefix from each *healthy* route load and cache it,
38
+ # so resilient routing works at a custom mount with zero configuration.
39
+ # after_routes_loaded fires only after a successful load — a raising
40
+ # routes.rb skips it — so the cache is never populated from the wiped
41
+ # break-time table. MountPath.refresh! is a no-op when the engine isn't
42
+ # found, so a stray load can never clobber a prefix from an earlier
43
+ # healthy one.
44
+ #
45
+ # The hook's yielded receiver is NOT reliably Rails.application — the
46
+ # reloader's to_run path fires it with the reloader callback context as
47
+ # self — so read the live route set through Rails.application directly.
48
+ app.config.after_routes_loaded do
49
+ Mbeditor::MountPath.refresh!(Rails.application.routes)
50
+ end
51
+ end
52
+
15
53
  initializer "mbeditor.handle_pending_migrations" do |app|
16
54
  # Insert before CheckPending so our middleware wraps it and can rescue
17
55
  # the error it raises. Falls back silently if CheckPending is absent
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mbeditor
4
+ # Resolves the active URL prefix mbeditor serves from.
5
+ #
6
+ # At break-time the host route table is wiped, so it is the wrong place to
7
+ # ask "where am I mounted?". MountPath answers from a resolution chain that
8
+ # never touches the route table directly:
9
+ #
10
+ # 1. explicit Mbeditor.configuration.mount_path (override)
11
+ # 2. the cached detected value (populated from a healthy route load)
12
+ # 3. the "/mbeditor" default
13
+ #
14
+ # The cache is guarded by a mutex because it is read at break-time from
15
+ # whatever request thread happens to be serving mbeditor.
16
+ module MountPath
17
+ DEFAULT = '/mbeditor'
18
+
19
+ @mutex = Mutex.new
20
+ @cached = nil
21
+
22
+ class << self
23
+ def resolve
24
+ Mbeditor.configuration.mount_path || cached || DEFAULT
25
+ end
26
+
27
+ # Scans a route set for the route that mounts Mbeditor::Engine and returns
28
+ # its path-spec prefix (e.g. "/mbeditor"), or nil if the engine is not
29
+ # mounted in this set. A mounted engine's route.app is a Constraints
30
+ # wrapper whose #app is the engine; an unmounted route's app is something
31
+ # else, so an unwrap-once-then-compare is enough.
32
+ def detect(route_set)
33
+ route_set.routes.each do |route|
34
+ app = route.app
35
+ target = app.respond_to?(:app) ? app.app : app
36
+ return route.path.spec.to_s if target == Mbeditor::Engine
37
+ end
38
+ nil
39
+ end
40
+
41
+ # Re-detects from a route set and caches the result. Called from the
42
+ # engine's after_routes_loaded hook, which fires only on a *healthy* route
43
+ # load — never at break-time, when the table is wiped. A nil detection is
44
+ # left as a no-op so a stray refresh from an engine-less set can never
45
+ # clobber a good prefix cached from an earlier healthy load.
46
+ def refresh!(route_set)
47
+ detected = detect(route_set)
48
+ self.cached = detected if detected
49
+ end
50
+
51
+ # The cached detected prefix, or nil if nothing has been detected yet.
52
+ def cached
53
+ @mutex.synchronize { @cached }
54
+ end
55
+
56
+ # Stores the detected prefix. Passing nil clears the cache.
57
+ def cached=(value)
58
+ @mutex.synchronize { @cached = value }
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mbeditor
4
+ # The private, isolated route set mbeditor dispatches to when the host route
5
+ # table is broken.
6
+ #
7
+ # It is built from Mbeditor::ROUTE_MAP — the same source the engine's own
8
+ # route set draws from — so the two can never drift apart. Crucially, this set
9
+ # is created with ActionDispatch::Routing::RouteSet.new and is *never*
10
+ # registered with the host RoutesReloader. The reloader's clear! wipes every
11
+ # route set it knows about when a host route draw raises; because this set is
12
+ # unknown to it, it survives the wipe and keeps mbeditor reachable.
13
+ #
14
+ # ROUTE_MAP declares controllers without a namespace (e.g. "editors#index").
15
+ # The engine resolves those under "mbeditor/" via isolate_namespace; a plain
16
+ # RouteSet does not, so we reproduce the namespacing with scope module:.
17
+ module PrivateRoutes
18
+ @mutex = Mutex.new
19
+
20
+ class << self
21
+ def route_set
22
+ @mutex.synchronize { @route_set ||= build }
23
+ end
24
+
25
+ private
26
+
27
+ def build
28
+ set = ActionDispatch::Routing::RouteSet.new
29
+ set.draw do
30
+ scope module: 'mbeditor' do
31
+ instance_exec(&Mbeditor::ROUTE_MAP)
32
+ end
33
+ end
34
+ set
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mbeditor
4
+ module Rack
5
+ # Serves mbeditor's own traffic from a private, isolated route set so the
6
+ # editor stays reachable even when the host config/routes.rb is broken.
7
+ #
8
+ # Sibling of HandlePendingMigrations: both intercept mbeditor requests when
9
+ # the normal path is blocked. This one sits above ActionDispatch::Reloader,
10
+ # so a prefix-matching request is dispatched before any route reload can
11
+ # raise. Requests that don't match the mount prefix pass through untouched.
12
+ #
13
+ # The matched request is rewritten to mirror how Rails mounts an engine:
14
+ # the prefix is stripped from PATH_INFO and moved to SCRIPT_NAME, so the
15
+ # private set sees the un-prefixed path while the rendered base path stays
16
+ # correct. Dispatch still flows through the mbeditor controllers, so
17
+ # verify_mbeditor_client and resolve_path run unchanged.
18
+ #
19
+ # Because this middleware sits above the Reloader — and therefore above the
20
+ # host's Cookies/Session/Flash middleware — a bare dispatch would leave
21
+ # resilient-routed requests without a session, breaking session-based
22
+ # authenticate_with. To keep parity with the normal mount, the private set
23
+ # is wrapped in the host app's own cookies/session/flash middleware. The
24
+ # crypto config (key generator, secret_key_base, serializer) is already in
25
+ # env, merged by Rails::Application#call before the stack runs.
26
+ class ResilientRouter
27
+ def initialize(app)
28
+ @app = app
29
+ end
30
+
31
+ def call(env)
32
+ prefix = Mbeditor::MountPath.resolve
33
+ path = "#{env["SCRIPT_NAME"]}#{env["PATH_INFO"]}"
34
+
35
+ return @app.call(env) unless matches_prefix?(path, prefix)
36
+
37
+ remainder = path[prefix.length..] || ""
38
+ remainder = "/" if remainder.empty?
39
+ env["SCRIPT_NAME"] = prefix
40
+ env["PATH_INFO"] = remainder
41
+
42
+ dispatch.call(env)
43
+ end
44
+
45
+ private
46
+
47
+ def matches_prefix?(path, prefix)
48
+ path == prefix || path.start_with?("#{prefix}/")
49
+ end
50
+
51
+ # The private route set wrapped in the host's session middleware, so a
52
+ # resilient-routed request has the same session support as one that
53
+ # reaches mbeditor through the normal mount. Built lazily and memoized;
54
+ # the middleware instance lives for the process, so a benign double-build
55
+ # under a startup race is harmless.
56
+ def dispatch
57
+ @dispatch ||= build_dispatch
58
+ end
59
+
60
+ def build_dispatch
61
+ app = Mbeditor::PrivateRoutes.route_set
62
+ config = Rails.application.config
63
+ store = config.session_store
64
+ # config.session_store is the store class normally, but :disabled (a
65
+ # symbol) when sessions are off — in which case dispatch the bare set.
66
+ return app unless store.is_a?(Class)
67
+
68
+ app = ActionDispatch::Flash.new(app)
69
+ app = store.new(app, config.session_options)
70
+ ActionDispatch::Cookies.new(app)
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mbeditor
4
+ # Single source of truth for the engine's routes.
5
+ #
6
+ # Both the engine's own route set (config/routes.rb via
7
+ # Mbeditor::Engine.routes.draw) and the (later) private, isolated route set
8
+ # draw from this same proc, so the two can never drift apart.
9
+ ROUTE_MAP = proc do
10
+ root to: 'editors#index'
11
+
12
+ get 'ping', to: 'editors#ping'
13
+ get 'workspace', to: 'editors#workspace'
14
+ get 'files', to: 'editors#files'
15
+ get 'file', to: 'editors#show'
16
+ get 'raw', to: 'editors#raw'
17
+ post 'file', to: 'editors#save'
18
+ post 'create_file', to: 'editors#create_file'
19
+ post 'create_dir', to: 'editors#create_dir'
20
+ patch 'rename', to: 'editors#rename'
21
+ delete 'delete', to: 'editors#destroy_path'
22
+ get 'state', to: 'editors#state'
23
+ post 'state', to: 'editors#save_state'
24
+ get 'branch_state', to: 'editors#branch_state'
25
+ post 'branch_state', to: 'editors#save_branch_state'
26
+ post 'prune_branch_states', to: 'editors#prune_branch_states'
27
+ get 'file_history', to: 'editors#file_history'
28
+ post 'file_history', to: 'editors#save_file_history'
29
+ get 'search', to: 'editors#search'
30
+ post 'replace_in_files', to: 'editors#replace_in_files'
31
+ get 'definition', to: 'editors#definition'
32
+ get 'js_definition', to: 'editors#js_definition'
33
+ get 'js_members', to: 'editors#js_members'
34
+ get 'module_members', to: 'editors#module_members'
35
+ get 'file_includes', to: 'editors#file_includes'
36
+ get 'client_config', to: 'editors#client_config'
37
+ get 'related_files', to: 'editors#related_files'
38
+ get 'model_schema', to: 'editors#model_schema'
39
+ get 'changelog', to: 'editors#changelog'
40
+ get 'git_info', to: 'editors#git_info'
41
+ get 'git_status', to: 'editors#git_status'
42
+ get 'manifest.webmanifest', to: 'editors#pwa_manifest', format: false
43
+ get 'sw.js', to: 'editors#pwa_sw', format: false
44
+ get 'mbeditor-icon.svg', to: 'editors#pwa_icon', format: false
45
+ get 'monaco_worker.js', to: 'editors#monaco_worker', format: false
46
+ get 'ts_worker.js', to: 'editors#ts_worker', format: false
47
+ get 'monaco-editor/*asset_path', to: 'editors#monaco_asset', format: false
48
+ get 'min-maps/*asset_path', to: 'editors#monaco_asset', format: false
49
+ post 'lint', to: 'editors#lint'
50
+ post 'quick_fix', to: 'editors#quick_fix'
51
+ post 'format', to: 'editors#format_file'
52
+ post 'test', to: 'editors#run_test'
53
+
54
+ # ── Git & Code Review ──────────────────────────────────────────────────────
55
+ get 'git/diff', to: 'git#diff'
56
+ get 'git/blame', to: 'git#blame'
57
+ get 'git/file_history', to: 'git#file_history'
58
+ get 'git/commit_graph', to: 'git#commit_graph'
59
+ get 'git/commit_detail', to: 'git#commit_detail'
60
+ get 'git/combined_diff', to: 'git#combined_diff'
61
+
62
+ # Redmine integration (enabled via config.mbeditor.redmine_enabled)
63
+ get 'redmine/issue/:id', to: 'git#redmine_issue', as: :redmine_issue
64
+ end
65
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mbeditor
4
- VERSION = "0.7.1"
4
+ VERSION = "0.7.3"
5
5
  end
data/lib/mbeditor.rb CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  require "mbeditor/version"
4
4
  require "mbeditor/configuration"
5
+ require "mbeditor/mount_path"
6
+ require "mbeditor/route_map"
7
+ require "mbeditor/private_routes"
5
8
  require "mbeditor/engine"
6
9
 
7
10
  module Mbeditor
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mbeditor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.7.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oliver Noonan
@@ -121,8 +121,12 @@ files:
121
121
  - lib/mbeditor/cable_log_filter.rb
122
122
  - lib/mbeditor/configuration.rb
123
123
  - lib/mbeditor/engine.rb
124
+ - lib/mbeditor/mount_path.rb
125
+ - lib/mbeditor/private_routes.rb
124
126
  - lib/mbeditor/rack/handle_pending_migrations.rb
127
+ - lib/mbeditor/rack/resilient_router.rb
125
128
  - lib/mbeditor/rack/silence_ping_request.rb
129
+ - lib/mbeditor/route_map.rb
126
130
  - lib/mbeditor/version.rb
127
131
  - mbeditor.gemspec
128
132
  - public/mbeditor-icon.svg