mbeditor 0.12.8 → 0.13.0
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 +4 -4
- data/CHANGELOG.md +207 -0
- data/app/assets/javascripts/mbeditor/application.js +7 -1
- data/app/assets/javascripts/mbeditor/collaboration_service.js +31 -0
- data/app/assets/javascripts/mbeditor/color_provider.js +5 -0
- data/app/assets/javascripts/mbeditor/components/CollapsibleSection.js +3 -1
- data/app/assets/javascripts/mbeditor/components/EditorPanel.js +135 -40
- data/app/assets/javascripts/mbeditor/components/FileTree.js +6 -1
- data/app/assets/javascripts/mbeditor/components/GitPanel.js +10 -1
- data/app/assets/javascripts/mbeditor/components/ImportConflictModal.js +15 -8
- data/app/assets/javascripts/mbeditor/components/ImportDialog.js +216 -0
- data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +629 -137
- data/app/assets/javascripts/mbeditor/components/ModelGraph.js +25 -6
- data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +218 -13
- data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +39 -33
- data/app/assets/javascripts/mbeditor/components/TabBar.js +11 -2
- data/app/assets/javascripts/mbeditor/components/TestRunPanel.js +312 -0
- data/app/assets/javascripts/mbeditor/editor_plugins.js +444 -116
- data/app/assets/javascripts/mbeditor/file_import.js +78 -0
- data/app/assets/javascripts/mbeditor/file_service.js +122 -18
- data/app/assets/javascripts/mbeditor/git_service.js +130 -8
- data/app/assets/javascripts/mbeditor/history_service.js +33 -38
- data/app/assets/javascripts/mbeditor/log_service.js +4 -0
- data/app/assets/javascripts/mbeditor/search_service.js +30 -2
- data/app/assets/javascripts/mbeditor/tab_manager.js +59 -6
- data/app/assets/javascripts/mbeditor/websocket_service.js +88 -4
- data/app/assets/stylesheets/mbeditor/editor.css +302 -55
- data/app/channels/mbeditor/channel_authentication.rb +7 -0
- data/app/channels/mbeditor/editor_channel.rb +6 -3
- data/app/controllers/mbeditor/application_controller.rb +5 -0
- data/app/controllers/mbeditor/editors_controller.rb +173 -29
- data/app/controllers/mbeditor/git_controller.rb +3 -3
- data/app/controllers/mbeditor/logs_controller.rb +3 -1
- data/app/services/mbeditor/collaboration_doc_store.rb +7 -0
- data/app/services/mbeditor/editor_state_service.rb +36 -26
- data/app/services/mbeditor/exclusion_matcher.rb +12 -10
- data/app/services/mbeditor/file_operation_service.rb +38 -3
- data/app/services/mbeditor/file_tree_service.rb +30 -2
- data/app/services/mbeditor/git_combined_diff_service.rb +13 -3
- data/app/services/mbeditor/git_commit_detail_service.rb +20 -16
- data/app/services/mbeditor/git_diff_service.rb +5 -1
- data/app/services/mbeditor/git_info_service.rb +20 -8
- data/app/services/mbeditor/git_line_diff_service.rb +6 -2
- data/app/services/mbeditor/git_service.rb +65 -15
- data/app/services/mbeditor/js_definition_service.rb +3 -1
- data/app/services/mbeditor/js_globals_service.rb +7 -4
- data/app/services/mbeditor/js_members_service.rb +3 -2
- data/app/services/mbeditor/js_program_service.rb +15 -5
- data/app/services/mbeditor/js_syntax_check_service.rb +15 -4
- data/app/services/mbeditor/process_runner.rb +42 -12
- data/app/services/mbeditor/ri_definition_service.rb +8 -1
- data/app/services/mbeditor/route_service.rb +45 -8
- data/app/services/mbeditor/rubocop_run_service.rb +86 -0
- data/app/services/mbeditor/ruby_definition_service.rb +23 -4
- data/app/services/mbeditor/schema_service.rb +65 -64
- data/app/services/mbeditor/search_replace_service.rb +75 -10
- data/app/services/mbeditor/test_runner_service.rb +98 -10
- data/lib/mbeditor/cable_log_filter.rb +8 -2
- data/lib/mbeditor/configuration.rb +12 -1
- data/lib/mbeditor/editor_bootstrap.rb +18 -12
- data/lib/mbeditor/engine.rb +22 -3
- data/lib/mbeditor/exception_log.rb +2 -2
- data/lib/mbeditor/pending_migrations.rb +33 -0
- data/lib/mbeditor/rack/handle_pending_migrations.rb +25 -15
- data/lib/mbeditor/rack/pending_migration_bypass.rb +104 -0
- data/lib/mbeditor/rack/silence_ping_request.rb +10 -2
- data/lib/mbeditor/route_map.rb +2 -0
- data/lib/mbeditor/ruby_lsp_client.rb +71 -12
- data/lib/mbeditor/version.rb +1 -1
- metadata +7 -2
|
@@ -10,6 +10,11 @@ module Mbeditor
|
|
|
10
10
|
# Follows the same process-group kill pattern used by the lint endpoint to
|
|
11
11
|
# enforce a configurable timeout.
|
|
12
12
|
module TestRunnerService
|
|
13
|
+
# Cap on the output shipped to the browser. A whole-suite run emits
|
|
14
|
+
# megabytes of it, and the tail is the part that matters (the failure list
|
|
15
|
+
# and the summary). Parsing still sees the full output.
|
|
16
|
+
MAX_RAW_BYTES = 256_000
|
|
17
|
+
|
|
13
18
|
module_function
|
|
14
19
|
|
|
15
20
|
# Run the test file at +test_path+ inside +repo_path+.
|
|
@@ -26,13 +31,13 @@ module Mbeditor
|
|
|
26
31
|
|
|
27
32
|
cmd = build_command(repo_path, test_path, framework, command, line: line)
|
|
28
33
|
raw = execute_with_timeout(repo_path, cmd, timeout)
|
|
29
|
-
tests, summary = parse_output(raw, framework)
|
|
34
|
+
tests, summary = parse_output(raw, framework, repo_path: repo_path)
|
|
30
35
|
{
|
|
31
36
|
ok: true,
|
|
32
37
|
framework: framework.to_s,
|
|
33
38
|
summary: summary,
|
|
34
39
|
tests: tests,
|
|
35
|
-
raw: raw
|
|
40
|
+
raw: truncate_raw(raw)
|
|
36
41
|
}
|
|
37
42
|
rescue ProcessRunner::TimeoutError
|
|
38
43
|
error_result("Test run timed out after #{timeout} seconds")
|
|
@@ -40,6 +45,59 @@ module Mbeditor
|
|
|
40
45
|
error_result(e.message)
|
|
41
46
|
end
|
|
42
47
|
|
|
48
|
+
# Run the whole suite in +repo_path+ — no file argument, so the framework's
|
|
49
|
+
# own default target applies (test/ for Rails, spec/ for RSpec).
|
|
50
|
+
#
|
|
51
|
+
# Same return shape as +run+, so the panel renders one result type. The
|
|
52
|
+
# framework is detected from the project rather than from a filename,
|
|
53
|
+
# since there isn't one.
|
|
54
|
+
def run_all(repo_path, framework: nil, command: nil, timeout: 1800)
|
|
55
|
+
framework = detect_suite_framework(repo_path) if framework.nil?
|
|
56
|
+
return error_result("Could not detect test framework") unless framework
|
|
57
|
+
|
|
58
|
+
cmd = build_suite_command(repo_path, framework, command)
|
|
59
|
+
raw = execute_with_timeout(repo_path, cmd, timeout)
|
|
60
|
+
tests, summary = parse_output(raw, framework, repo_path: repo_path)
|
|
61
|
+
{
|
|
62
|
+
ok: true,
|
|
63
|
+
framework: framework.to_s,
|
|
64
|
+
summary: summary,
|
|
65
|
+
tests: tests,
|
|
66
|
+
raw: truncate_raw(raw)
|
|
67
|
+
}
|
|
68
|
+
rescue ProcessRunner::TimeoutError
|
|
69
|
+
error_result("Test run timed out after #{timeout} seconds")
|
|
70
|
+
rescue StandardError => e
|
|
71
|
+
error_result(e.message)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# No test_path to go on, so this reads the project layout only. RSpec wins
|
|
75
|
+
# a tie: a project with both usually keeps `test/` for legacy fixtures.
|
|
76
|
+
def detect_suite_framework(repo_path)
|
|
77
|
+
return :rspec if File.exist?(File.join(repo_path, ".rspec"))
|
|
78
|
+
return :rspec if File.directory?(File.join(repo_path, "spec"))
|
|
79
|
+
return :minitest if File.directory?(File.join(repo_path, "test"))
|
|
80
|
+
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def build_suite_command(repo_path, framework, custom_command)
|
|
85
|
+
return Shellwords.split(custom_command) if custom_command.present?
|
|
86
|
+
|
|
87
|
+
case framework.to_sym
|
|
88
|
+
when :rspec
|
|
89
|
+
bin = File.join(repo_path, "bin", "rspec")
|
|
90
|
+
(File.exist?(bin) ? [bin] : ["bundle", "exec", "rspec"]) + ["--format", "json"]
|
|
91
|
+
else
|
|
92
|
+
bin = File.join(repo_path, "bin", "rails")
|
|
93
|
+
# `bin/rails test` with no path runs the default suite. Without it,
|
|
94
|
+
# `rake test` is the portable fallback for a non-Rails project.
|
|
95
|
+
return [bin, "test", "--verbose"] if File.exist?(bin)
|
|
96
|
+
|
|
97
|
+
["bundle", "exec", "rake", "test"]
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
43
101
|
# Given a source file path, resolve it to its matching test/spec file.
|
|
44
102
|
# If the file is already a test/spec file, return it as-is.
|
|
45
103
|
def resolve_test_file(repo_path, relative_path)
|
|
@@ -176,18 +234,44 @@ module Mbeditor
|
|
|
176
234
|
result[:stdout] + result[:stderr]
|
|
177
235
|
end
|
|
178
236
|
|
|
179
|
-
|
|
237
|
+
# Keeps the tail. byteslice can cut a multi-byte character in half, so the
|
|
238
|
+
# result is scrubbed before it goes anywhere near JSON.
|
|
239
|
+
def truncate_raw(raw)
|
|
240
|
+
return raw if raw.bytesize <= MAX_RAW_BYTES
|
|
241
|
+
|
|
242
|
+
raw.byteslice(-MAX_RAW_BYTES, MAX_RAW_BYTES).scrub
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def parse_output(raw, framework, repo_path: nil)
|
|
180
246
|
case framework.to_sym
|
|
181
247
|
when :rspec
|
|
182
|
-
parse_rspec_output(raw)
|
|
248
|
+
parse_rspec_output(raw, repo_path: repo_path)
|
|
183
249
|
when :minitest
|
|
184
|
-
parse_minitest_output(raw)
|
|
250
|
+
parse_minitest_output(raw, repo_path: repo_path)
|
|
185
251
|
else
|
|
186
252
|
[[], empty_summary]
|
|
187
253
|
end
|
|
188
254
|
end
|
|
189
255
|
|
|
190
|
-
|
|
256
|
+
# Failure blocks name an absolute path in some runners and a repo-relative
|
|
257
|
+
# one in others. The editor can only open the relative form, so both are
|
|
258
|
+
# normalized here; anything that escapes the workspace is dropped rather
|
|
259
|
+
# than handed to the client as an unopenable path.
|
|
260
|
+
def relativize(path, repo_path)
|
|
261
|
+
return nil if path.nil? || path.empty?
|
|
262
|
+
|
|
263
|
+
rel = path.to_s
|
|
264
|
+
rel = rel.delete_prefix("./")
|
|
265
|
+
if repo_path
|
|
266
|
+
root = File.join(repo_path.to_s.chomp("/"), "")
|
|
267
|
+
rel = rel.delete_prefix(root)
|
|
268
|
+
end
|
|
269
|
+
return nil if rel.start_with?("/", "../")
|
|
270
|
+
|
|
271
|
+
rel
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def parse_rspec_output(raw, repo_path: nil)
|
|
191
275
|
# RSpec with --format json embeds JSON in the output
|
|
192
276
|
json_match = raw.match(/(\{.*"summary_line".*\})/m)
|
|
193
277
|
if json_match
|
|
@@ -205,18 +289,19 @@ module Mbeditor
|
|
|
205
289
|
name: ex["full_description"] || ex["description"],
|
|
206
290
|
status: ex["status"] == "passed" ? "pass" : (ex["status"] == "pending" ? "skip" : "fail"),
|
|
207
291
|
line: ex.dig("line_number"),
|
|
292
|
+
file: relativize(ex["file_path"], repo_path),
|
|
208
293
|
message: ex.dig("exception", "message")
|
|
209
294
|
}
|
|
210
295
|
end
|
|
211
296
|
[tests, summary]
|
|
212
297
|
else
|
|
213
|
-
parse_minitest_output(raw) # fallback to text parsing
|
|
298
|
+
parse_minitest_output(raw, repo_path: repo_path) # fallback to text parsing
|
|
214
299
|
end
|
|
215
300
|
rescue JSON::ParserError
|
|
216
|
-
parse_minitest_output(raw)
|
|
301
|
+
parse_minitest_output(raw, repo_path: repo_path)
|
|
217
302
|
end
|
|
218
303
|
|
|
219
|
-
def parse_minitest_output(raw)
|
|
304
|
+
def parse_minitest_output(raw, repo_path: nil)
|
|
220
305
|
lines = raw.lines
|
|
221
306
|
|
|
222
307
|
# First pass: collect per-test results from verbose output.
|
|
@@ -232,7 +317,7 @@ module Mbeditor
|
|
|
232
317
|
when "E" then "error"
|
|
233
318
|
when "S" then "skip"
|
|
234
319
|
end
|
|
235
|
-
verbose_results[m[1]] = { name: m[1], status: status, line: nil, message: nil }
|
|
320
|
+
verbose_results[m[1]] = { name: m[1], status: status, line: nil, file: nil, message: nil }
|
|
236
321
|
end
|
|
237
322
|
|
|
238
323
|
# Second pass: parse failure/error blocks for messages and line numbers.
|
|
@@ -246,6 +331,7 @@ module Mbeditor
|
|
|
246
331
|
|
|
247
332
|
name = name_line.strip.split(" [").first.chomp(":")
|
|
248
333
|
line_num = name_line[/:(\d+)\]/, 1]&.to_i
|
|
334
|
+
file = relativize(name_line[/\[([^\]]+):\d+\]/, 1], repo_path)
|
|
249
335
|
msg_lines = []
|
|
250
336
|
(idx + 2...lines.length).each do |j|
|
|
251
337
|
break if lines[j].strip.empty? || lines[j].match?(/^\s+\d+\)\s+/)
|
|
@@ -256,11 +342,13 @@ module Mbeditor
|
|
|
256
342
|
name: name,
|
|
257
343
|
status: line.include?("Error") ? "error" : "fail",
|
|
258
344
|
line: line_num,
|
|
345
|
+
file: file,
|
|
259
346
|
message: msg_lines.join("\n")
|
|
260
347
|
}
|
|
261
348
|
|
|
262
349
|
if verbose_results.key?(name)
|
|
263
350
|
verbose_results[name][:line] = line_num
|
|
351
|
+
verbose_results[name][:file] = file
|
|
264
352
|
verbose_results[name][:message] = msg_lines.join("\n")
|
|
265
353
|
else
|
|
266
354
|
failure_entries << entry
|
|
@@ -62,8 +62,14 @@ module Mbeditor
|
|
|
62
62
|
|
|
63
63
|
%w[debug info warn error fatal unknown].each do |level|
|
|
64
64
|
define_method(level) do |message = nil, &block|
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
# Evaluate the block exactly once. It used to be called for the
|
|
66
|
+
# suppression check and then handed to super as well, so every message
|
|
67
|
+
# that got through was built twice.
|
|
68
|
+
if message.nil? && block
|
|
69
|
+
message = block.call
|
|
70
|
+
block = nil
|
|
71
|
+
end
|
|
72
|
+
return if suppress_message?(message.to_s)
|
|
67
73
|
|
|
68
74
|
super(message, &block)
|
|
69
75
|
end
|
|
@@ -5,6 +5,7 @@ module Mbeditor
|
|
|
5
5
|
attr_accessor :allowed_environments, :workspace_root, :excluded_paths, :rubocop_command, :rubocop_server,
|
|
6
6
|
:redmine_enabled, :redmine_url, :redmine_api_key, :redmine_ticket_source,
|
|
7
7
|
:test_framework, :test_command, :test_timeout,
|
|
8
|
+
:test_all_command, :test_all_timeout,
|
|
8
9
|
:authenticate_with, :cable_authenticate_with, :authentication_cache_ttl, :user_name_callback, :user_name_methods,
|
|
9
10
|
:lint_timeout, :base_branch_candidates, :git_timeout, :search_timeout,
|
|
10
11
|
:ruby_def_include_dirs, :related_files_custom_paths,
|
|
@@ -27,7 +28,17 @@ module Mbeditor
|
|
|
27
28
|
@redmine_ticket_source = :commit # :commit (scan commit messages) or :branch (leading digits of branch name)
|
|
28
29
|
@test_framework = nil # :minitest or :rspec — auto-detected when nil
|
|
29
30
|
@test_command = nil # e.g. "bundle exec ruby -Itest" or "bundle exec rspec"
|
|
30
|
-
|
|
31
|
+
# Seconds. 60 was not enough for a real app: `bin/rails test` spends most
|
|
32
|
+
# of it booting Rails before the first assertion runs, so a perfectly
|
|
33
|
+
# healthy single-file run reported "timed out".
|
|
34
|
+
@test_timeout = 180
|
|
35
|
+
# A whole-suite run is a different order of magnitude — minutes, not
|
|
36
|
+
# seconds — so it gets its own ceiling rather than forcing test_timeout
|
|
37
|
+
# up to a value that lets one hung example block for half an hour.
|
|
38
|
+
@test_all_timeout = 1800
|
|
39
|
+
# Sibling of test_command, for the same reason: a project that needs a
|
|
40
|
+
# custom runner for one file needs one for the suite too. nil auto-detects.
|
|
41
|
+
@test_all_command = nil
|
|
31
42
|
@lint_timeout = 15 # seconds for RuboCop/haml-lint subprocesses
|
|
32
43
|
@base_branch_candidates = %w[origin/develop origin/main origin/master develop main master]
|
|
33
44
|
@git_timeout = 10 # seconds; nil disables (no timeout on git subprocesses)
|
|
@@ -55,27 +55,33 @@ module Mbeditor
|
|
|
55
55
|
|
|
56
56
|
window.loadPrettierPlugins = function() {
|
|
57
57
|
if (window._prettierLoadPromise) return window._prettierLoadPromise;
|
|
58
|
-
|
|
58
|
+
var promise = new Promise(function(resolve, reject) {
|
|
59
59
|
var savedDefine = window.define;
|
|
60
60
|
window.define = undefined;
|
|
61
61
|
var pending = prettierScripts.length;
|
|
62
|
+
var failed = null;
|
|
63
|
+
// Restore window.define only once every script has settled: an
|
|
64
|
+
// early restore let a sibling still in flight register as an AMD
|
|
65
|
+
// module instead of setting its window global.
|
|
66
|
+
function settle() {
|
|
67
|
+
if (--pending > 0) return;
|
|
68
|
+
window.define = savedDefine;
|
|
69
|
+
if (failed) reject(new Error('Failed to load Prettier: ' + failed));
|
|
70
|
+
else resolve();
|
|
71
|
+
}
|
|
62
72
|
prettierScripts.forEach(function(src) {
|
|
63
73
|
var s = document.createElement('script');
|
|
64
74
|
s.src = src;
|
|
65
|
-
s.onload =
|
|
66
|
-
|
|
67
|
-
window.define = savedDefine;
|
|
68
|
-
resolve();
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
s.onerror = function() {
|
|
72
|
-
window.define = savedDefine;
|
|
73
|
-
reject(new Error('Failed to load Prettier: ' + src));
|
|
74
|
-
};
|
|
75
|
+
s.onload = settle;
|
|
76
|
+
s.onerror = function() { failed = failed || src; settle(); };
|
|
75
77
|
document.head.appendChild(s);
|
|
76
78
|
});
|
|
77
79
|
});
|
|
78
|
-
|
|
80
|
+
window._prettierLoadPromise = promise;
|
|
81
|
+
// Clear the cached promise so the next Format action retries the
|
|
82
|
+
// load; caching a rejection disabled Format for the whole session.
|
|
83
|
+
promise.catch(function() { window._prettierLoadPromise = null; });
|
|
84
|
+
return promise;
|
|
79
85
|
};
|
|
80
86
|
|
|
81
87
|
// Lazy-load monaco-vim on first use. Drop-in for the old AMD
|
data/lib/mbeditor/engine.rb
CHANGED
|
@@ -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/pending_migration_bypass"
|
|
5
6
|
require "mbeditor/rack/resilient_router"
|
|
6
7
|
require "mbeditor/cable_log_filter"
|
|
7
8
|
|
|
@@ -71,7 +72,24 @@ module Mbeditor
|
|
|
71
72
|
app.config.active_record[:migration_error]
|
|
72
73
|
|
|
73
74
|
if defined?(ActiveRecord::Migration::CheckPending) && migration_error == :page_load
|
|
74
|
-
|
|
75
|
+
# Two layers, and both earn their place.
|
|
76
|
+
#
|
|
77
|
+
# The swap is the actual fix: CheckPending is replaced by a wrapper that
|
|
78
|
+
# runs the identical check for the host's own traffic, but records
|
|
79
|
+
# rather than raises for the editor's — so a pending migration no longer
|
|
80
|
+
# blocks the tree, opening a file, saving one, or loading the assets the
|
|
81
|
+
# page needs. Without it the editor was unusable exactly when it was
|
|
82
|
+
# needed most.
|
|
83
|
+
app.middleware.swap ActiveRecord::Migration::CheckPending,
|
|
84
|
+
Mbeditor::Rack::PendingMigrationBypass,
|
|
85
|
+
file_watcher: app.config.file_watcher
|
|
86
|
+
|
|
87
|
+
# HandlePendingMigrations stays above it as a backstop for a
|
|
88
|
+
# PendingMigrationError raised from anywhere else in the stack (an
|
|
89
|
+
# engine or initializer that checks on its own). It should now be
|
|
90
|
+
# unreachable for the ordinary case; it is cheap, and being locked out
|
|
91
|
+
# of the editor is the failure worth two guards.
|
|
92
|
+
app.middleware.insert_before Mbeditor::Rack::PendingMigrationBypass,
|
|
75
93
|
Mbeditor::Rack::HandlePendingMigrations
|
|
76
94
|
end
|
|
77
95
|
end
|
|
@@ -156,8 +174,9 @@ module Mbeditor
|
|
|
156
174
|
|
|
157
175
|
@exception_subscriber = ActiveSupport::Notifications.subscribe(
|
|
158
176
|
"process_action.action_controller"
|
|
159
|
-
) do
|
|
160
|
-
|
|
177
|
+
) do |_name, _started, _finished, _id, payload|
|
|
178
|
+
# 5-arity: the block runs for every host request, and building an Event
|
|
179
|
+
# just to read its payload allocated one per request for nothing.
|
|
161
180
|
exception = payload[:exception_object]
|
|
162
181
|
next unless exception
|
|
163
182
|
|
|
@@ -48,10 +48,10 @@ module Mbeditor
|
|
|
48
48
|
private
|
|
49
49
|
|
|
50
50
|
def build(exception, payload, workspace_root)
|
|
51
|
-
@seq = (@seq || 0) + 1
|
|
51
|
+
seq = MUTEX.synchronize { @seq = (@seq || 0) + 1 }
|
|
52
52
|
{
|
|
53
53
|
type: "exception",
|
|
54
|
-
id:
|
|
54
|
+
id: seq,
|
|
55
55
|
at: Time.now.utc.iso8601,
|
|
56
56
|
klass: exception.class.name,
|
|
57
57
|
message: exception.message.to_s[0, 2000],
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mbeditor
|
|
4
|
+
# The last pending-migration message seen, so the editor can warn about it
|
|
5
|
+
# without being blocked by it.
|
|
6
|
+
#
|
|
7
|
+
# Lives in lib/ (not autoloaded) for the same reason ExceptionLog does: a
|
|
8
|
+
# Zeitwerk reload must not wipe it, and a pending migration is exactly the
|
|
9
|
+
# state in which reloads are happening.
|
|
10
|
+
module PendingMigrations
|
|
11
|
+
MUTEX = Mutex.new
|
|
12
|
+
private_constant :MUTEX
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# message: the PendingMigrationError's message, or nil once it clears.
|
|
17
|
+
def note(message)
|
|
18
|
+
MUTEX.synchronize { @message = message && message.to_s.strip }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def clear
|
|
22
|
+
MUTEX.synchronize { @message = nil }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def message
|
|
26
|
+
MUTEX.synchronize { @message }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def pending?
|
|
30
|
+
!message.nil?
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -18,7 +18,7 @@ module Mbeditor
|
|
|
18
18
|
raise unless defined?(ActiveRecord::PendingMigrationError) && e.is_a?(ActiveRecord::PendingMigrationError)
|
|
19
19
|
|
|
20
20
|
path = "#{env["SCRIPT_NAME"]}#{env["PATH_INFO"]}"
|
|
21
|
-
raise unless path.start_with?(
|
|
21
|
+
raise unless path.start_with?(Mbeditor::MountPath.resolve)
|
|
22
22
|
|
|
23
23
|
if env["HTTP_X_MBEDITOR_CLIENT"] == "1"
|
|
24
24
|
# XHR from the editor frontend — structured JSON error.
|
|
@@ -30,13 +30,23 @@ module Mbeditor
|
|
|
30
30
|
# files. Assets are referenced by their unfingerprinted paths, which
|
|
31
31
|
# Sprockets resolves in development (the only env mbeditor runs in).
|
|
32
32
|
# The banner appears as soon as the first XHR fires.
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
#
|
|
34
|
+
# The base comes from MountPath, not SCRIPT_NAME: this middleware runs
|
|
35
|
+
# above the router, so SCRIPT_NAME is still "" and the engine-served
|
|
36
|
+
# URLs in the shell (/monaco-editor/...) were emitted without the
|
|
37
|
+
# mount prefix and 404'd.
|
|
38
|
+
[200, { "Content-Type" => "text/html; charset=utf-8" }, [editor_shell_html(mount_base)]]
|
|
35
39
|
end
|
|
36
40
|
end
|
|
37
41
|
|
|
38
42
|
private
|
|
39
43
|
|
|
44
|
+
def mount_base
|
|
45
|
+
Mbeditor::MountPath.resolve.to_s.sub(%r{/$}, "")
|
|
46
|
+
rescue StandardError
|
|
47
|
+
""
|
|
48
|
+
end
|
|
49
|
+
|
|
40
50
|
def editor_shell_html(base)
|
|
41
51
|
prettier_script_urls = %w[
|
|
42
52
|
prettier-standalone.js
|
|
@@ -45,14 +55,14 @@ module Mbeditor
|
|
|
45
55
|
prettier-plugin-html.js
|
|
46
56
|
prettier-plugin-postcss.js
|
|
47
57
|
prettier-plugin-markdown.js
|
|
48
|
-
].map { |f| "
|
|
58
|
+
].map { |f| "/assets/#{f}" }
|
|
49
59
|
|
|
50
60
|
# Bootstrap JS shared with the standard layout — see Mbeditor::EditorBootstrap
|
|
51
61
|
setup_js = Mbeditor::EditorBootstrap.setup_js(base)
|
|
52
62
|
boot_js = Mbeditor::EditorBootstrap.boot_js(
|
|
53
63
|
base: base,
|
|
54
64
|
prettier_script_urls: prettier_script_urls,
|
|
55
|
-
application_js_url: "
|
|
65
|
+
application_js_url: "/assets/mbeditor/application.js"
|
|
56
66
|
)
|
|
57
67
|
|
|
58
68
|
<<~HTML
|
|
@@ -62,16 +72,16 @@ module Mbeditor
|
|
|
62
72
|
<meta charset="UTF-8" />
|
|
63
73
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
64
74
|
<title>Mbeditor</title>
|
|
65
|
-
<link rel="stylesheet" href="
|
|
66
|
-
<link rel="stylesheet" href="
|
|
67
|
-
<script defer src="
|
|
68
|
-
<script defer src="
|
|
69
|
-
<script defer src="
|
|
70
|
-
<script defer src="
|
|
71
|
-
<script defer src="
|
|
72
|
-
<script defer src="
|
|
73
|
-
<script defer src="
|
|
74
|
-
<script defer src="
|
|
75
|
+
<link rel="stylesheet" href="/assets/fontawesome.min.css" />
|
|
76
|
+
<link rel="stylesheet" href="/assets/mbeditor/application.css" />
|
|
77
|
+
<script defer src="/assets/react.min.js"></script>
|
|
78
|
+
<script defer src="/assets/react-dom.min.js"></script>
|
|
79
|
+
<script defer src="/assets/axios.min.js"></script>
|
|
80
|
+
<script defer src="/assets/lodash.min.js"></script>
|
|
81
|
+
<script defer src="/assets/minisearch.min.js"></script>
|
|
82
|
+
<script defer src="/assets/marked.min.js"></script>
|
|
83
|
+
<script defer src="/assets/emmet.js"></script>
|
|
84
|
+
<script defer src="/assets/monaco-themes-bundle.js"></script>
|
|
75
85
|
<link rel="stylesheet" href="#{base}/monaco-editor/monaco.css" />
|
|
76
86
|
</head>
|
|
77
87
|
<body>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mbeditor/pending_migrations"
|
|
4
|
+
|
|
5
|
+
module Mbeditor
|
|
6
|
+
module Rack
|
|
7
|
+
# Wraps ActiveRecord::Migration::CheckPending so a pending migration cannot
|
|
8
|
+
# lock the developer out of the one tool they need to fix it.
|
|
9
|
+
#
|
|
10
|
+
# Rails inserts CheckPending after ActionDispatch::Callbacks whenever
|
|
11
|
+
# config.active_record.migration_error is :page_load — the default a real
|
|
12
|
+
# `rails new` app carries in development. From then until the migration is
|
|
13
|
+
# run, EVERY request raises PendingMigrationError, and that included all of
|
|
14
|
+
# mbeditor's: the file tree, opening a file, and saving one. The editor was
|
|
15
|
+
# unusable in precisely the situation it exists to get you out of, and the
|
|
16
|
+
# migration could not be edited to fix it.
|
|
17
|
+
#
|
|
18
|
+
# An earlier attempt rescued the error above CheckPending and answered
|
|
19
|
+
# mbeditor's own paths with a fallback shell and a 503 for every XHR. That
|
|
20
|
+
# left the shell without its stylesheets or JavaScript — those live under
|
|
21
|
+
# /assets, which is not an mbeditor path, so they still 500'd — and even
|
|
22
|
+
# once loaded every API call was a 503. Rescuing after the fact was the
|
|
23
|
+
# wrong shape; the check simply must not apply to this traffic.
|
|
24
|
+
#
|
|
25
|
+
# So the check still runs (it is file-watcher backed and cheap after the
|
|
26
|
+
# first call, and non-editor traffic must keep Rails' behaviour exactly),
|
|
27
|
+
# but for editor traffic the error is recorded and swallowed rather than
|
|
28
|
+
# raised, and the request proceeds. CheckPending raises before it calls
|
|
29
|
+
# anything downstream, so dispatching afterwards runs the request once, not
|
|
30
|
+
# twice.
|
|
31
|
+
#
|
|
32
|
+
# The recorded message is what surfaces the warning banner, so the developer
|
|
33
|
+
# is still told to migrate — they are just no longer prevented from working.
|
|
34
|
+
class PendingMigrationBypass
|
|
35
|
+
DEFAULT_ASSET_PREFIX = "/assets"
|
|
36
|
+
|
|
37
|
+
def initialize(app, file_watcher: nil)
|
|
38
|
+
@app = app
|
|
39
|
+
@checked =
|
|
40
|
+
if file_watcher
|
|
41
|
+
ActiveRecord::Migration::CheckPending.new(app, file_watcher: file_watcher)
|
|
42
|
+
else
|
|
43
|
+
ActiveRecord::Migration::CheckPending.new(app)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Announced on every bypassed response so the editor can warn about a
|
|
48
|
+
# migration created mid-session, not only one that existed at page load.
|
|
49
|
+
PENDING_HEADER = "X-Mbeditor-Pending-Migration"
|
|
50
|
+
|
|
51
|
+
def call(env)
|
|
52
|
+
return @checked.call(env) unless editor_traffic?(env)
|
|
53
|
+
|
|
54
|
+
begin
|
|
55
|
+
result = @checked.call(env)
|
|
56
|
+
# The check passed, so anything recorded earlier is stale.
|
|
57
|
+
Mbeditor::PendingMigrations.clear
|
|
58
|
+
result
|
|
59
|
+
rescue StandardError => e
|
|
60
|
+
raise unless pending_migration_error?(e)
|
|
61
|
+
|
|
62
|
+
Mbeditor::PendingMigrations.note(e.message)
|
|
63
|
+
status, headers, body = @app.call(env)
|
|
64
|
+
headers[PENDING_HEADER] = header_safe(e.message)
|
|
65
|
+
[status, headers, body]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
# A header cannot carry the newlines the error message has, and it does
|
|
72
|
+
# not need the whole thing — the banner supplies the instructions.
|
|
73
|
+
def header_safe(message)
|
|
74
|
+
message.to_s.gsub(/\s+/, " ").strip[0, 200]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def pending_migration_error?(error)
|
|
78
|
+
defined?(ActiveRecord::PendingMigrationError) &&
|
|
79
|
+
error.is_a?(ActiveRecord::PendingMigrationError)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Everything the editor needs in order to load and function: its own mount
|
|
83
|
+
# and the assets its page pulls in. Asset requests are included because
|
|
84
|
+
# the shell is inert without them — and because serving a stylesheet has
|
|
85
|
+
# never depended on the schema being current.
|
|
86
|
+
def editor_traffic?(env)
|
|
87
|
+
path = "#{env["SCRIPT_NAME"]}#{env["PATH_INFO"]}"
|
|
88
|
+
prefix = Mbeditor::MountPath.resolve
|
|
89
|
+
return true if path == prefix || path.start_with?("#{prefix}/")
|
|
90
|
+
|
|
91
|
+
path.start_with?("#{asset_prefix}/")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def asset_prefix
|
|
95
|
+
@asset_prefix ||= begin
|
|
96
|
+
configured = Rails.application.config.assets.prefix if Rails.application.config.respond_to?(:assets)
|
|
97
|
+
(configured.presence || DEFAULT_ASSET_PREFIX).to_s.chomp("/")
|
|
98
|
+
rescue StandardError
|
|
99
|
+
DEFAULT_ASSET_PREFIX
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -35,7 +35,15 @@ module Mbeditor
|
|
|
35
35
|
private
|
|
36
36
|
|
|
37
37
|
def mbeditor_request?(path)
|
|
38
|
-
path.start_with?("/
|
|
38
|
+
path.start_with?("#{mount_prefix}/")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The mount is configurable, so the prefix cannot be hardcoded — a host
|
|
42
|
+
# that mounts the engine elsewhere logged every editor request in full.
|
|
43
|
+
def mount_prefix
|
|
44
|
+
Mbeditor::MountPath.resolve.chomp("/")
|
|
45
|
+
rescue StandardError
|
|
46
|
+
Mbeditor::MountPath::DEFAULT
|
|
39
47
|
end
|
|
40
48
|
|
|
41
49
|
def cable_request?(path)
|
|
@@ -53,7 +61,7 @@ module Mbeditor
|
|
|
53
61
|
end
|
|
54
62
|
|
|
55
63
|
def root_request?(env, path)
|
|
56
|
-
env["REQUEST_METHOD"] == "GET" && path ==
|
|
64
|
+
env["REQUEST_METHOD"] == "GET" && path == mount_prefix
|
|
57
65
|
end
|
|
58
66
|
|
|
59
67
|
def normalized_request_path(env)
|
data/lib/mbeditor/route_map.rb
CHANGED
|
@@ -55,7 +55,9 @@ module Mbeditor
|
|
|
55
55
|
post 'lint', to: 'editors#lint'
|
|
56
56
|
post 'quick_fix', to: 'editors#quick_fix'
|
|
57
57
|
post 'format', to: 'editors#format_file'
|
|
58
|
+
post 'rubocop', to: 'editors#rubocop_run'
|
|
58
59
|
post 'test', to: 'editors#run_test'
|
|
60
|
+
post 'test_all', to: 'editors#run_all_tests'
|
|
59
61
|
get 'logs/tail', to: 'logs#tail'
|
|
60
62
|
|
|
61
63
|
# ── Git & Code Review ──────────────────────────────────────────────────────
|