Class: Mbeditor::JsProgramService

Inherits:
Object
  • Object
show all
Defined in:
app/services/mbeditor/js_program_service.rb

Overview

Enumerates the workspace's own JavaScript source and hands it to the editor so Monaco's TypeScript worker can build a real program from it.

This is the file-based replacement for declaring every discovered name as ambient any (JsGlobalsService). Under Sprockets a JS file with no import/export is a script, so its top-level declarations land in the global scope — which is exactly TypeScript's own model for script files. Giving the worker the sources instead of a name list yields real inferred types, member completions, and argument-count checking, and it still reports "Cannot find name" for genuinely unknown identifiers.

What it deliberately does NOT solve: UMD-wrapped libraries. React, lodash and axios all assign their global inside a closure (factory(global.React = {})), which TypeScript cannot follow statically — loading their source produces no global at all. Those stay on hand-written declarations (the React mini-UMD stub) or on JsGlobalsService's ambient names, which is why that service is still here.

No truncation: a workspace that exceeds any limit reports what it skipped and why, rather than silently returning a partial program.

Constant Summary collapse

SOURCE_EXT =
/\.(js|jsx|ts|tsx)\z/i
MINIFIED_NAME =

Minified bundles cost parse time and yield nothing useful — their globals are one-letter names inside a closure. Matched by convention, then by shape for bundles whose filename doesn't say so.

/[.\-]min\.(js|jsx|ts|tsx)\z/i
MAX_LINE_LENGTH =
2_000
MAX_FILE_BYTES =

A single source file this large is a bundle or generated output, not something a person edits.

1024 * 1024
CACHE_TTL =

seconds

10

Class Method Summary collapse

Class Method Details

.call(workspace_root) ⇒ Object



43
44
45
46
47
48
49
50
51
52
53
54
# File 'app/services/mbeditor/js_program_service.rb', line 43

def call(workspace_root)
  root = File.expand_path(workspace_root.to_s)
  now  = monotonic
  MUTEX.synchronize do
    entry = (@cache ||= {})[root]
    return entry[:data] if entry && (now - entry[:ts]) < CACHE_TTL
  end

  data = compute(root)
  MUTEX.synchronize { (@cache ||= {})[root] = { ts: monotonic, data: data } }
  data
end

.file(workspace_root, relative_path) ⇒ Object

Content for a single workspace-relative path, for incremental refresh after a file changes. Returns nil when the path isn't program material.



58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'app/services/mbeditor/js_program_service.rb', line 58

def file(workspace_root, relative_path)
  root = File.expand_path(workspace_root.to_s)
  rel  = relative_path.to_s.delete_prefix("/")
  return nil unless rel.match?(SOURCE_EXT)
  return nil if rel.match?(MINIFIED_NAME)
  return nil if matcher(root).excluded?(rel)

  abs = File.expand_path(File.join(root, rel))
  return nil unless abs == File.join(root, rel) # no traversal out of the workspace
  return nil unless File.file?(abs) && !File.symlink?(abs)

  content = read_source(abs)
  content && { path: rel, content: content }
end

.invalidate(workspace_root) ⇒ Object



73
74
75
# File 'app/services/mbeditor/js_program_service.rb', line 73

def invalidate(workspace_root)
  MUTEX.synchronize { (@cache ||= {}).delete(File.expand_path(workspace_root.to_s)) }
end