Class: RSX::Loader

Inherits:
Object
  • Object
show all
Defined in:
lib/rsx/loader.rb

Overview

Finds, compiles, evaluates and reloads .rsx files.

Defined Under Namespace

Classes: Entry

Constant Summary collapse

EXTENSIONS =
[".rsx", ".html.rsx"].freeze

Instance Method Summary collapse

Constructor Details

#initialize(config) ⇒ Loader

Returns a new instance of Loader.



18
19
20
21
22
23
# File 'lib/rsx/loader.rb', line 18

def initialize(config)
  @config = config
  @entries = {}
  @stack = []
  @monitor = Monitor.new
end

Instance Method Details

#clearObject



110
111
112
113
114
115
# File 'lib/rsx/loader.rb', line 110

def clear
  @monitor.synchronize do
    @entries.each_value { |entry| unload(entry) }
    @entries.clear
  end
end

#compile_cacheObject



38
39
40
# File 'lib/rsx/loader.rb', line 38

def compile_cache
  @config.compile_cache
end

#current_entryObject

The file currently being evaluated, used to attribute components to it.



34
35
36
# File 'lib/rsx/loader.rb', line 34

def current_entry
  @stack.last
end

#default_export(path) ⇒ Object



166
167
168
169
# File 'lib/rsx/loader.rb', line 166

def default_export(path)
  entry = load(path)
  entry.default || entry.components.first
end

#entriesObject



25
26
27
# File 'lib/rsx/loader.rb', line 25

def entries
  @entries.values
end

#files(paths = @config.paths) ⇒ Object



88
89
90
91
92
93
94
# File 'lib/rsx/loader.rb', line 88

def files(paths = @config.paths)
  Array(paths).flat_map do |root|
    next [] unless File.directory?(root)

    Dir.glob(File.join(root, "**", "*.rsx")).sort
  end
end

#import(spec, as: nil, from: nil) ⇒ Object



148
149
150
151
152
# File 'lib/rsx/loader.rb', line 148

def import(spec, as: nil, from: nil)
  entry = load(resolve!(spec, from: from))
  Array(as).each { |name| alias_constant(name, entry) }
  entry.default || entry.components.first
end

#load(path, force: false) ⇒ Object

Compiles and evaluates a .rsx file exactly once per digest.

Raises:



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/rsx/loader.rb', line 43

def load(path, force: false)
  absolute = File.expand_path(path)
  raise FileNotFoundError, "no such RSX file: #{absolute}" unless File.file?(absolute)

  @monitor.synchronize do
    source = File.read(absolute)
    digest = compile_cache.digest(source)
    existing = @entries[absolute]
    return existing if existing && existing.digest == digest && !force

    unload(existing) if existing

    entry = Entry.new(path: absolute, digest: digest, mtime: mtime(absolute), components: [], default: nil)
    @entries[absolute] = entry
    @stack.push(entry)

    begin
      ruby = compile_cache.fetch(absolute, source) do
        Transformer.transform(source, path: absolute)
      end

      if Transformer.defines_components?(ruby)
        # Component files are evaluated at the top level so that `class`,
        # `def` and constants behave exactly as they do in a .rb file.
        eval(ruby, TOPLEVEL_BINDING, absolute, 1) # rubocop:disable Security/Eval
      else
        entry.template = Template.from_ruby(ruby, path: absolute, digest: digest)
      end
    rescue Exception # rubocop:disable Lint/RescueException
      @entries.delete(absolute)
      raise
    ensure
      @stack.pop
    end

    entry
  end
end

#load_all(paths = @config.paths) ⇒ Object

Loads every .rsx file under the configured paths. Called at boot in production so no request pays for compilation.



84
85
86
# File 'lib/rsx/loader.rb', line 84

def load_all(paths = @config.paths)
  files(paths).each { |file| load(file) }
end

#loaded?(path) ⇒ Boolean

Returns:

  • (Boolean)


29
30
31
# File 'lib/rsx/loader.rb', line 29

def loaded?(path)
  @entries.key?(File.expand_path(path))
end

#reload!Object

Reloads only the files whose contents changed. Used by the Rails reloader.



97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/rsx/loader.rb', line 97

def reload!
  @monitor.synchronize do
    @entries.values.each do |entry|
      if !File.file?(entry.path)
        unload(entry)
        @entries.delete(entry.path)
      elsif mtime(entry.path) != entry.mtime
        load(entry.path, force: true)
      end
    end
  end
end

#resolve(spec, from: nil) ⇒ Object

Resolves an import specifier or template path to a file on disk.



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/rsx/loader.rb', line 118

def resolve(spec, from: nil)
  spec = spec.to_s
  candidates = []

  if spec.start_with?("/")
    candidates << spec
  else
    candidates << File.expand_path(spec, File.dirname(from)) if from && !from.empty?
    Array(@config.paths).each { |root| candidates << File.join(root, spec) }
    candidates << File.expand_path(spec, Dir.pwd)
  end

  candidates.each do |candidate|
    return candidate if File.file?(candidate)

    EXTENSIONS.each do |extension|
      with_extension = "#{candidate}#{extension}"
      return with_extension if File.file?(with_extension)
    end
  end

  nil
end

#resolve!(spec, from: nil) ⇒ Object



142
143
144
145
146
# File 'lib/rsx/loader.rb', line 142

def resolve!(spec, from: nil)
  resolve(spec, from: from) ||
    raise(FileNotFoundError, "could not find `#{spec}`#{" imported from #{from}" if from}. " \
                             "Looked in: #{Array(@config.paths).join(", ")}")
end

#track(component) ⇒ Object

Records a component defined by the file currently being loaded.



155
156
157
158
159
160
161
162
163
164
# File 'lib/rsx/loader.rb', line 155

def track(component)
  entry = current_entry
  return component unless entry

  entry.components << component
  entry.default ||= component
  component.rsx_source_path = entry.path
  component.rsx_source_digest = entry.digest
  component
end