mirror of
https://github.com/discourse/discourse.git
synced 2026-08-04 10:39:43 +08:00
We've been using basic type-checking via JSDoc for some time. This commit allows us to author proper `.ts`/`.gts` files, and use the full typescript syntax. Initially, only d-button and a single chat file are migrated, as proof of functionality. In future, we may migrate more files, and consider making our tsconfig more strict.
253 lines
7.9 KiB
Ruby
Vendored
253 lines
7.9 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
module Plugin
|
|
class JsManager
|
|
@cache = {}
|
|
|
|
def self.optional_plugin_stub
|
|
"data:text/javascript,/* autogenerated missing optional plugin stub */const m=new Proxy({},{get:()=>null});export default new Proxy({},{get:()=>m});"
|
|
end
|
|
|
|
def self.required_plugin_stub(plugin_name)
|
|
message =
|
|
"Plugin '#{plugin_name}' is imported by another bundle, but it is not installed on this site. " \
|
|
"If this dependency is optional, import it with `with { discourseImport: \"optional\" }`."
|
|
|
|
"data:text/javascript, /* autogenerated missing required plugin stub */ export default null; throw new Error(#{message.to_json});"
|
|
end
|
|
|
|
def self.js_asset_exists?(plugin_directory_name)
|
|
maybe_cache("js_asset_exists_#{plugin_directory_name}") do
|
|
has_source_files_in_dir(plugin_directory_name, "assets/javascripts")
|
|
end
|
|
end
|
|
|
|
def self.admin_js_asset_exists?(plugin_directory_name)
|
|
maybe_cache("admin_js_asset_exists_#{plugin_directory_name}") do
|
|
has_source_files_in_dir(plugin_directory_name, "admin/assets/javascripts")
|
|
end
|
|
end
|
|
|
|
def self.test_js_asset_exists?(plugin_directory_name)
|
|
maybe_cache("test_js_asset_exists_#{plugin_directory_name}") do
|
|
has_source_files_in_dir(plugin_directory_name, "test/javascripts")
|
|
end
|
|
end
|
|
|
|
def self.read_manifest(plugin_directory_name)
|
|
maybe_cache("manifest_#{plugin_directory_name}") do
|
|
manifest_path =
|
|
"#{Rails.root.join("app/assets/generated/#{plugin_directory_name}/manifest.json")}"
|
|
JSON.parse(File.read(manifest_path))
|
|
rescue Errno::ENOENT
|
|
{}
|
|
end
|
|
end
|
|
|
|
def self.digested_logical_path_for(plugin_directory_name, entrypoint_name)
|
|
manifest_entry = read_manifest(plugin_directory_name)[entrypoint_name]
|
|
"js/plugins/#{manifest_entry["fileName"].delete_suffix(".js")}" if manifest_entry
|
|
end
|
|
|
|
def self.import_paths_for(plugin_directory_name, entrypoint_name)
|
|
read_manifest(plugin_directory_name)[entrypoint_name]["imports"].map do
|
|
"js/plugins/#{it.delete_suffix(".js")}"
|
|
end
|
|
end
|
|
|
|
def self.external_plugin_imports(plugin_directory_name, entrypoint_name)
|
|
read_manifest(plugin_directory_name)[entrypoint_name]["externalPluginImports"]
|
|
end
|
|
|
|
def compile!
|
|
log "Compiling #{Discourse.plugins.count} plugins..."
|
|
start = Time.now
|
|
|
|
if !GlobalSetting.mini_racer_single_threaded && AssetProcessor.booted?
|
|
raise "Cannot fork Plugin::JsManager for parallel compilation because AssetProcessor is already booted."
|
|
end
|
|
|
|
parallel_count = [Etc.nprocessors, 4].min
|
|
AssetProcessor.timeout = 120_000
|
|
|
|
Parallel.each(Discourse.plugins, in_processes: parallel_count) do |plugin|
|
|
compile_js_bundle(plugin)
|
|
end
|
|
|
|
log "Finished initial compilation of plugins in #{(Time.now - start).round(2)}s"
|
|
end
|
|
|
|
def compile_js_bundle(plugin)
|
|
base_output_dir = "#{Rails.root.join("app/assets/generated/#{plugin.directory_name}")}"
|
|
js_dir = "#{base_output_dir}/js/plugins"
|
|
map_dir = "#{base_output_dir}/map/plugins"
|
|
|
|
entrypoints = { "main" => "assets/javascripts", "admin" => "admin/assets/javascripts" }
|
|
entrypoints["test"] = "test/javascripts" if Rails.env.local?
|
|
|
|
tree = {}
|
|
entrypoints_config = {}
|
|
|
|
entrypoints.each do |name, js_path|
|
|
js_base = "#{plugin.directory}/#{js_path}"
|
|
|
|
files = Dir.glob("**/*", base: js_base)
|
|
|
|
next if files.empty?
|
|
|
|
entrypoints_config[name] = { modules: [] }
|
|
|
|
files.sort.each do |file|
|
|
full_path = File.join(js_base, file)
|
|
if File.file?(full_path)
|
|
normalized_file_path = file.sub(/\.js\.es6$/, ".js")
|
|
content = File.read(full_path)
|
|
content = AssetProcessor.append_es6_deprecation(content, file) if file.end_with?(
|
|
".js.es6",
|
|
)
|
|
tree[normalized_file_path] = content
|
|
if name == "test" && file.match(%r{/(acceptance|integration|unit)/})
|
|
if file.match?(/-test\.g?[jt]s$/)
|
|
entrypoints_config[name][:modules] << normalized_file_path
|
|
end
|
|
else
|
|
entrypoints_config[name][:modules] << normalized_file_path
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
hex_digest =
|
|
Digest::SHA1.hexdigest(
|
|
[
|
|
*tree.keys,
|
|
*tree.values,
|
|
AssetProcessor::BASE_COMPILER_VERSION,
|
|
AssetProcessor.ember_version,
|
|
minify?.to_s,
|
|
plugin.name,
|
|
].join,
|
|
)
|
|
base36_digest = hex_digest.to_i(16).to_s(36).first(8)
|
|
|
|
filename_prefix = "#{plugin.directory_name}_"
|
|
filename_suffix = "-#{base36_digest}.digested"
|
|
|
|
existing_files = Dir.glob("#{js_dir}/*").map { |path| File.basename(path) }
|
|
|
|
files_exist =
|
|
entrypoints_config.keys.all? do |name|
|
|
existing_files.any? do |file|
|
|
file.start_with?("#{filename_prefix}#{name}.") &&
|
|
file.end_with?("#{filename_suffix}.js")
|
|
end
|
|
end
|
|
|
|
if !cache? || !files_exist
|
|
compiler =
|
|
Plugin::JsCompiler.new(
|
|
plugin.name,
|
|
minify: minify?,
|
|
tree: tree,
|
|
entrypoints: entrypoints_config,
|
|
filename_prefix:,
|
|
filename_suffix:,
|
|
)
|
|
result = compiler.compile!
|
|
|
|
FileUtils.mkdir_p(js_dir)
|
|
FileUtils.mkdir_p(map_dir)
|
|
|
|
manifest = {}
|
|
result.each do |file_name, info|
|
|
code = info["code"]
|
|
code += "\n//# sourceMappingURL=../../map/plugins/#{file_name}.map\n" if info["map"]
|
|
File.write("#{js_dir}/#{file_name}", code)
|
|
|
|
File.write("#{map_dir}/#{file_name}.map", info["map"]) if info["map"]
|
|
|
|
if info["isEntry"]
|
|
manifest[info["name"]] = {
|
|
fileName: file_name,
|
|
imports: info["imports"],
|
|
externalPluginImports: info["externalPluginImports"],
|
|
}
|
|
end
|
|
end
|
|
|
|
File.write("#{base_output_dir}/manifest.json", JSON.pretty_generate(manifest))
|
|
end
|
|
|
|
# Delete any old versions
|
|
Dir
|
|
.glob("#{base_output_dir}/*/*/*")
|
|
.reject { |path| path.include?(filename_suffix) || path.include?("_extra") }
|
|
.each { |path| FileUtils.rm_rf(path) }
|
|
end
|
|
|
|
def watch
|
|
listener =
|
|
Listen.to(
|
|
*Discourse.plugins.map(&:directory),
|
|
{ ignore: [%r{/node_modules/}], only: /\.(gjs|js|hbs|ts|gts)\z/ },
|
|
) do |modified, added, removed|
|
|
changed_files = modified + added + removed
|
|
changed_plugins = Set.new
|
|
|
|
log "Changed files:"
|
|
changed_files.each do |file|
|
|
relative_path = Pathname.new(file).relative_path_from(Rails.root)
|
|
log "- #{relative_path}"
|
|
|
|
plugin = Discourse.plugins.find { |p| file.start_with?(p.resolved_dir) }
|
|
changed_plugins << plugin if plugin
|
|
end
|
|
|
|
log "Recompiling..."
|
|
start = Time.now
|
|
changed_plugins.each { |plugin| compile_js_bundle(plugin) }
|
|
log "Finished recompilation in #{(Time.now - start).round(2)}s"
|
|
|
|
MessageBus.publish("/file-change", ["refresh"])
|
|
rescue => e
|
|
log "Plugin JS watcher crashed \n#{e}"
|
|
end
|
|
|
|
begin
|
|
listener.start
|
|
compile!
|
|
yield
|
|
ensure
|
|
listener.stop
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def minify?
|
|
Rails.env.production?
|
|
end
|
|
|
|
def cache?
|
|
true
|
|
end
|
|
|
|
def log(message)
|
|
STDERR.puts message
|
|
end
|
|
|
|
private_class_method def self.maybe_cache(key, &blk)
|
|
if Rails.env.production?
|
|
@cache.fetch(key, &blk)
|
|
else
|
|
blk.call
|
|
end
|
|
end
|
|
|
|
private_class_method def self.has_source_files_in_dir(plugin_directory_name, dir)
|
|
Dir.glob("plugins/#{plugin_directory_name}/#{dir}/**/*.{js,hbs,gjs,es6,ts,gts}") do
|
|
break true
|
|
end || false
|
|
end
|
|
end
|
|
end
|