Class: DiscourseJsProcessor::Transpiler

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

Constant Summary collapse

TRANSPILER_PATH =
"tmp/theme-transpiler.js"

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(skip_module: false) ⇒ Transpiler

Returns a new instance of Transpiler.



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

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

Class Method Details

.build_production_theme_transpilerObject



77
78
79
80
# File 'lib/discourse_js_processor.rb', line 77

def self.build_production_theme_transpiler
  File.write(TRANSPILER_PATH, build_theme_transpiler)
  TRANSPILER_PATH
end

.build_theme_transpilerObject



67
68
69
70
71
72
73
74
75
# File 'lib/discourse_js_processor.rb', line 67

def self.build_theme_transpiler
  FileUtils.rm_rf("tmp/theme-transpiler") # cleanup old files - remove after Jan 2025
  Discourse::Utils.execute_command(
    "pnpm",
    "-C=app/assets/javascripts/theme-transpiler",
    "node",
    "build.js",
  )
end

.create_new_contextObject



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/discourse_js_processor.rb', line 82

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) })

  source =
    if Rails.env.production?
      File.read(TRANSPILER_PATH)
    else
      @processor_mutex.synchronize { build_theme_transpiler }
    end

  ctx.eval(source, filename: "theme-transpiler.js")

  ctx
end

.mutexObject



63
64
65
# File 'lib/discourse_js_processor.rb', line 63

def self.mutex
  @mutex
end

.reset_contextObject



103
104
105
106
# File 'lib/discourse_js_processor.rb', line 103

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

.v8Object



108
109
110
111
112
113
114
115
116
117
118
# File 'lib/discourse_js_processor.rb', line 108

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.



124
125
126
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 124

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



181
182
183
# File 'lib/discourse_js_processor.rb', line 181

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

#module_name(root_path, logical_path) ⇒ Object



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/discourse_js_processor.rb', line 164

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



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

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,
    },
  )
end

#terser(tree, opts) ⇒ Object



185
186
187
# File 'lib/discourse_js_processor.rb', line 185

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