tina4ruby 3.13.35 → 3.13.37

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: 9cdb8976e4e87658d1655be468112a986b32817f18f5415d96c8744d3f2b179c
4
- data.tar.gz: d2f3c50d3b646e062d38c088090b0919c347ecec1b130edcd661c0921b743846
3
+ metadata.gz: b75f9ed6546e671904c26e7dc9bb1f29fc83a62a63a7488d05540e12c535f32f
4
+ data.tar.gz: f33418ea68192ed529eb2f0b7953e2762ca5eface4c8b4b368b8a93c57344981
5
5
  SHA512:
6
- metadata.gz: 37f1743a3ef8f314baf055c84bf7696174b5977af5b2d9e87b5ceb5e93ed2d6a4870d4dcf2ca7e75f3e4e4734368274e84c4160c94199c7da59d57f5f5432194
7
- data.tar.gz: addb86d53bfc8d864dd93bc3dc52015c21e133590109ae91d262f082ecf55368acada387097b2e9340fddc5ae1443ba0bb5dd085492d6e2b2e5f90a81eb582cb
6
+ metadata.gz: 413ecc40fcc877a0ea0cfbdca95ef5a3b5da3b3f24035787a9619ecc385e4735e5532eea10c34cbe316727f4544865adea62fcf0233804019815c52f699674fd
7
+ data.tar.gz: 84c7bd505379a49527cb75eead709eb8155225a90406ee5691afdfc53127129e013b5c3ede53c7cb35df44dcbdeb3467fdaa91adf6881f159ffe3912476ec65e
@@ -392,12 +392,30 @@ module Tina4
392
392
  reload_type = body["type"] || "reload"
393
393
  Tina4::Log.info("External reload trigger: #{reload_type}#{@reload_file.empty? ? '' : " (#{@reload_file})"}")
394
394
  # Re-discover so files dropped into src/routes/ register without
395
- # a server restart. Idempotent — already-loaded files are skipped.
395
+ # a server restart. Idempotent — already-loaded files are skipped,
396
+ # changed files are re-loaded (mtime-tracked).
396
397
  begin
397
398
  Tina4::Router.rescan_routes!
398
399
  rescue StandardError => e
399
400
  Tina4::Log.error("Re-discover on reload failed: #{e.message}")
400
401
  end
402
+ # WebSocket-primary reload: push an instant message to every browser
403
+ # connected on /__dev_reload. The toolbar client (and the dev-admin
404
+ # dashboard) act on this immediately — the mtime poll above is only a
405
+ # fallback for when the socket is down. CSS changes swap stylesheets;
406
+ # everything else triggers a full page reload. We normalise the wire
407
+ # `type` to "css"/"reload" (the clients only react to css/reload/change)
408
+ # but still echo the caller's original type in the HTTP response.
409
+ # Wrapped so a broadcast failure — or zero connected clients — never
410
+ # 500s the reload endpoint.
411
+ begin
412
+ ws_type = reload_type == "css" ? "css" : "reload"
413
+ Tina4::DevReload.broadcast(
414
+ JSON.generate({ type: ws_type, file: @reload_file, mtime: @reload_mtime })
415
+ )
416
+ rescue StandardError => e
417
+ Tina4::Log.error("Dev-reload WebSocket broadcast failed: #{e.message}")
418
+ end
401
419
  json_response({ ok: true, type: reload_type })
402
420
  when ["GET", "/__dev/api/status"]
403
421
  json_response(status_payload)
@@ -1258,24 +1276,146 @@ module Tina4
1258
1276
  resolved
1259
1277
  end
1260
1278
 
1279
+ # Noise dirs + hidden dot-entries are hidden from the file browser,
1280
+ # except the env files — verbatim parity with PHP/Python dev-admin.
1281
+ DEV_FILES_IGNORED = %w[__pycache__ node_modules vendor .git venv .venv dist target .tina4].freeze
1282
+
1283
+ def dev_files_hidden?(name)
1284
+ return true if DEV_FILES_IGNORED.include?(name)
1285
+ name.start_with?(".") && name != ".env" && name != ".env.example"
1286
+ end
1287
+
1288
+ # Same 4-status mapping Python/PHP use for a porcelain code.
1289
+ def dev_git_status_label(code)
1290
+ return "untracked" if code == "??"
1291
+ return "modified" if code.include?("M")
1292
+ return "added" if code.include?("A")
1293
+ return "deleted" if code.include?("D")
1294
+ "clean"
1295
+ end
1296
+
1297
+ # Branch + porcelain status map for the file browser (mirrors PHP/Python
1298
+ # dev-admin). Paths git reports are relative to the repo root (always
1299
+ # forward-slash); the project root may sit inside a larger repo, so the
1300
+ # toplevel is returned too for rebasing. Empty on any error / no git.
1301
+ def dev_git_info(root)
1302
+ require "shellwords" unless defined?(Shellwords)
1303
+ esc = Shellwords.escape(root)
1304
+ inside = `cd #{esc} && git rev-parse --is-inside-work-tree 2>/dev/null`.strip
1305
+ return { branch: "", git_root: nil, status: {} } unless inside == "true"
1306
+
1307
+ branch = `cd #{esc} && git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
1308
+ top = `cd #{esc} && git rev-parse --show-toplevel 2>/dev/null`.strip
1309
+ git_root = top.empty? ? nil : top.tr("\\", "/").sub(%r{/+\z}, "")
1310
+
1311
+ status = {}
1312
+ `cd #{esc} && git status --porcelain -uall 2>/dev/null`.each_line do |line|
1313
+ line = line.chomp
1314
+ next if line.length < 4
1315
+ code = line[0, 2].strip
1316
+ path = line[3..].to_s.strip
1317
+ if (idx = path.index(" -> ")) # rename/copy — keep destination
1318
+ path = path[(idx + 4)..]
1319
+ end
1320
+ next if path.nil? || path.empty?
1321
+ status[path] = code
1322
+ end
1323
+ { branch: branch, git_root: git_root, status: status }
1324
+ rescue StandardError
1325
+ { branch: "", git_root: nil, status: {} }
1326
+ end
1327
+
1261
1328
  def files_list(env)
1329
+ # Response shape matches tina4-python / tina4-php 1:1 so the dev-admin
1330
+ # SPA works against every framework: each entry carries `is_dir`,
1331
+ # `has_children`, `git_status` and `size`; the payload carries `branch`.
1262
1332
  rel = query_param(env, "path") || "."
1263
- begin
1264
- target = safe_project_path(rel)
1265
- return { error: "Not found" } unless File.exist?(target)
1266
- return { error: "Not a directory" } unless File.directory?(target)
1267
- entries = Dir.children(target).sort.map do |name|
1268
- full = File.join(target, name)
1269
- {
1270
- name: name,
1271
- type: File.directory?(full) ? "dir" : "file",
1272
- size: File.file?(full) ? File.size(full) : 0
1273
- }
1333
+ root = File.expand_path(Dir.pwd)
1334
+ git = dev_git_info(root)
1335
+
1336
+ # Missing/invalid paths return an empty-but-valid shape (not an error
1337
+ # body): the SPA restores expanded-folder state from localStorage and
1338
+ # non-existent folders would otherwise spam the console.
1339
+ target = begin
1340
+ safe_project_path(rel)
1341
+ rescue StandardError
1342
+ nil
1343
+ end
1344
+ unless target && File.directory?(target)
1345
+ return { path: rel, branch: git[:branch], entries: [], error: "not a directory" }
1346
+ end
1347
+
1348
+ root_fwd = root.tr("\\", "/")
1349
+ cwd_in_git = ""
1350
+ if git[:git_root] && git[:git_root] != root_fwd && root_fwd.start_with?(git[:git_root])
1351
+ cwd_in_git = root_fwd[git[:git_root].length..].to_s.sub(%r{\A/+}, "")
1352
+ cwd_in_git += "/" unless cwd_in_git.empty?
1353
+ end
1354
+
1355
+ entries = Dir.children(target).sort.filter_map do |name|
1356
+ next if dev_files_hidden?(name)
1357
+ full = File.join(target, name)
1358
+ entry_rel = full[root.length..].to_s.sub(%r{\A/+}, "").tr("\\", "/")
1359
+ is_dir = File.directory?(full)
1360
+
1361
+ # git status for this entry (same mapping PHP/Python use)
1362
+ git_path = cwd_in_git + entry_rel
1363
+ label = "clean"
1364
+ if (code = git[:status][git_path])
1365
+ label = dev_git_status_label(code)
1366
+ elsif is_dir
1367
+ prefix = "#{git_path}/" # propagate dirty status from any child
1368
+ hit = git[:status].find { |gf, _| gf.start_with?(prefix) }
1369
+ label = (hit && hit[1] == "??") ? "untracked" : "modified" if hit
1274
1370
  end
1275
- { path: rel, entries: entries, count: entries.size }
1276
- rescue => e
1277
- { error: e.message }
1371
+
1372
+ # has_children: does the dir contain anything visible?
1373
+ has_children = nil
1374
+ if is_dir
1375
+ has_children = begin
1376
+ Dir.children(full).any? { |c| !dev_files_hidden?(c) }
1377
+ rescue StandardError
1378
+ false
1379
+ end
1380
+ end
1381
+
1382
+ {
1383
+ name: name,
1384
+ path: entry_rel,
1385
+ is_dir: is_dir,
1386
+ has_children: has_children,
1387
+ git_status: label,
1388
+ size: is_dir ? nil : (File.size(full) rescue 0)
1389
+ }
1278
1390
  end
1391
+
1392
+ { path: rel, branch: git[:branch], entries: entries, count: entries.size }
1393
+ end
1394
+
1395
+ # Map a relative path to a CodeMirror language string. IDENTICAL in
1396
+ # coverage to the Python master (tina4_python/dev_admin lang_map). The
1397
+ # dev-admin SPA reads the "language" field to pick its grammar.
1398
+ def dev_admin_language(rel)
1399
+ base = File.basename(rel.to_s).downcase
1400
+ return "dockerfile" if %w[dockerfile dockerfile.dev dockerfile.prod].include?(base)
1401
+ return "env" if base == ".env" || base == ".env.example"
1402
+
1403
+ ext_map = {
1404
+ ".py" => "python", ".php" => "php", ".rb" => "ruby",
1405
+ ".ts" => "typescript", ".js" => "javascript", ".jsx" => "javascript",
1406
+ ".tsx" => "typescript", ".json" => "json", ".html" => "html",
1407
+ ".twig" => "html", ".css" => "css", ".scss" => "css",
1408
+ ".md" => "markdown", ".sql" => "sql", ".yaml" => "yaml",
1409
+ ".yml" => "yaml", ".toml" => "toml", ".xml" => "html",
1410
+ ".env" => "env",
1411
+ ".sh" => "shell", ".bash" => "shell",
1412
+ ".bat" => "shell", ".cmd" => "shell", ".ps1" => "shell",
1413
+ ".rs" => "rust", ".go" => "go", ".java" => "java",
1414
+ ".txt" => "text", ".csv" => "text", ".log" => "text",
1415
+ ".gemspec" => "ruby", ".rake" => "ruby",
1416
+ ".svg" => "svg"
1417
+ }
1418
+ ext_map.fetch(File.extname(base).downcase, "text")
1279
1419
  end
1280
1420
 
1281
1421
  def file_read_payload(rel)
@@ -1285,7 +1425,7 @@ module Tina4
1285
1425
  return { error: "Not found" } unless File.exist?(target)
1286
1426
  return { error: "Not a file" } unless File.file?(target)
1287
1427
  content = File.read(target, encoding: "utf-8", invalid: :replace, undef: :replace)
1288
- { path: rel, content: content, bytes: File.size(target) }
1428
+ { path: rel, content: content, bytes: File.size(target), language: dev_admin_language(rel) }
1289
1429
  rescue => e
1290
1430
  { error: e.message }
1291
1431
  end