Class: DiscourseJsProcessor::Transpiler

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

Constant Summary collapse

TRANSPILER_PATH =
(
  if Rails.env.production?
    "tmp/theme-transpiler.js"
  else
    "tmp/theme-transpiler/#{Process.pid}.js"
  end
)

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(skip_module: false) ⇒ Transpiler

Returns a new instance of Transpiler.



189
190
191
# File 'lib/discourse_js_processor.rb', line 189

def initialize(skip_module: false)
  @skip_module = skip_module
end

Class Method Details

.build_theme_transpilerObject



118
119
120
121
122
123
124
125
# File 'lib/discourse_js_processor.rb', line 118

def self.build_theme_transpiler
  Discourse::Utils.execute_command(
    "node",
    "app/assets/javascripts/theme-transpiler/build.js",
    TRANSPILER_PATH,
  )
  TRANSPILER_PATH
end

.create_new_contextObject



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/discourse_js_processor.rb', line 127

def self.create_new_context
  # timeout any eval that takes longer than 15 seconds
  ctx = MiniRacer::Context.new(timeout: 15_000, ensure_gc_after_idle: 2000)

  # General shims
  ctx.attach("rails.logger.info", proc { |err| Rails.logger.info(err.to_s) })
  ctx.attach("rails.logger.warn", proc { |err| Rails.logger.warn(err.to_s) })
  ctx.attach("rails.logger.error", proc { |err| Rails.logger.error(err.to_s) })

  # Theme template AST transformation plugins
  if Rails.env.development? || Rails.env.test?
    @processor_mutex.synchronize { build_theme_transpiler }
  end

  ctx.eval(File.read(TRANSPILER_PATH), filename: "theme-transpiler.js")

  ctx
end

.mutexObject



114
115
116
# File 'lib/discourse_js_processor.rb', line 114

def self.mutex
  @mutex
end

.reset_contextObject



146
147
148
149
# File 'lib/discourse_js_processor.rb', line 146

def self.reset_context
  @ctx&.dispose
  @ctx = nil
end

.v8Object



151
152
153
154
155
156
157
158
159
160
161
# File 'lib/discourse_js_processor.rb', line 151

def self.v8
  return @ctx if @ctx

  # ensure we only init one of these
  @ctx_init.synchronize do
    return @ctx if @ctx
    @ctx = create_new_context
  end

  @ctx
end

.v8_call(*args, **kwargs) ⇒ Object

Call a method in the global scope of the v8 context. The ‘fetch_result_call` kwarg provides a workaround for the lack of mini_racer async result support. The first call can perform some async operation, and then `fetch_result_call` will be called to fetch the result.



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/discourse_js_processor.rb', line 167

def self.v8_call(*args, **kwargs)
  fetch_result_call = kwargs.delete(:fetch_result_call)
  mutex.synchronize do
    result = v8.call(*args, **kwargs)
    result = v8.call(fetch_result_call) if fetch_result_call
    result
  end
rescue MiniRacer::RuntimeError => e
  message = e.message
  begin
    # Workaround for https://github.com/rubyjs/mini_racer/issues/262
    possible_encoded_message = message.delete_prefix("Error: ")
    decoded = JSON.parse("{\"value\": #{possible_encoded_message}}")["value"]
    message = "Error: #{decoded}"
  rescue JSON::ParserError
    message = e.message
  end
  transpile_error = TranspileError.new(message)
  transpile_error.set_backtrace(e.backtrace)
  raise transpile_error
end

Instance Method Details

#compile_raw_template(source, theme_id: nil) ⇒ Object



225
226
227
# File 'lib/discourse_js_processor.rb', line 225

def compile_raw_template(source, theme_id: nil)
  self.class.v8_call("compileRawTemplate", source, theme_id)
end

#module_name(root_path, logical_path) ⇒ Object



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/discourse_js_processor.rb', line 208

def module_name(root_path, logical_path)
  path = nil

  root_base = File.basename(Rails.root)
  # If the resource is a plugin, use the plugin name as a prefix
  if root_path =~ %r{(.*/#{root_base}/plugins/[^/]+)/}
    plugin_path = "#{Regexp.last_match[1]}/plugin.rb"

    plugin = Discourse.plugins.find { |p| p.path == plugin_path }
    path =
      "discourse/plugins/#{plugin.name}/#{logical_path.sub(%r{javascripts/}, "")}" if plugin
  end

  # We need to strip the app subdirectory to replicate how ember-cli works.
  path || logical_path&.gsub("app/", "")&.gsub("addon/", "")&.gsub("admin/addon", "admin")
end

#perform(source, root_path = nil, logical_path = nil, theme_id: nil, extension: nil) ⇒ Object



193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/discourse_js_processor.rb', line 193

def perform(source, root_path = nil, logical_path = nil, theme_id: nil, extension: nil)
  self.class.v8_call(
    "transpile",
    source,
    {
      skipModule: @skip_module,
      moduleId: module_name(root_path, logical_path),
      filename: logical_path || "unknown",
      extension: extension,
      themeId: theme_id,
      commonPlugins: DISCOURSE_COMMON_BABEL_PLUGINS,
    },
  )
end

#terser(tree, opts) ⇒ Object



229
230
231
# File 'lib/discourse_js_processor.rb', line 229

def terser(tree, opts)
  self.class.v8_call("minify", tree, opts, fetch_result_call: "getMinifyResult")
end