diff --git a/app/assets/javascripts/discourse-js-processor.js b/app/assets/javascripts/discourse-js-processor.js index 5deb20f011..7050fe2ac7 100644 --- a/app/assets/javascripts/discourse-js-processor.js +++ b/app/assets/javascripts/discourse-js-processor.js @@ -4,6 +4,7 @@ const makeEmberTemplateCompilerPlugin = require("babel-plugin-ember-template-compilation").default; +const colocatedBabelPlugin = require("colocated-babel-plugin").default; const precompile = require("ember-template-compiler").precompile; const Handlebars = require("handlebars").default; @@ -50,6 +51,7 @@ function buildTemplateCompilerBabelPlugins({ themeId }) { } return [ + colocatedBabelPlugin, require("widget-hbs-compiler").WidgetHbsCompiler, [ makeEmberTemplateCompilerPlugin(() => compileFunction), diff --git a/app/models/theme.rb b/app/models/theme.rb index 7d41ce11ba..1f9f23e548 100644 --- a/app/models/theme.rb +++ b/app/models/theme.rb @@ -6,7 +6,7 @@ require 'json_schemer' class Theme < ActiveRecord::Base include GlobalPath - BASE_COMPILER_VERSION = 63 + BASE_COMPILER_VERSION = 64 attr_accessor :child_components diff --git a/lib/discourse_js_processor.rb b/lib/discourse_js_processor.rb index 4e999640c3..2dbf1f91c9 100644 --- a/lib/discourse_js_processor.rb +++ b/lib/discourse_js_processor.rb @@ -157,6 +157,7 @@ class DiscourseJsProcessor load_file_in_context(ctx, "node_modules/babel-plugin-ember-template-compilation/src/plugin.js", wrap_in_module: "babel-plugin-ember-template-compilation/index") load_file_in_context(ctx, "node_modules/babel-plugin-ember-template-compilation/src/expression-parser.js", wrap_in_module: "babel-plugin-ember-template-compilation/expression-parser") load_file_in_context(ctx, "node_modules/babel-import-util/src/index.js", wrap_in_module: "babel-import-util") + load_file_in_context(ctx, "node_modules/ember-cli-htmlbars/lib/colocated-babel-plugin.js", wrap_in_module: "colocated-babel-plugin") # Widget HBS compiler widget_hbs_compiler_source = File.read("#{Rails.root}/lib/javascripts/widget-hbs-compiler.js") diff --git a/lib/theme_javascript_compiler.rb b/lib/theme_javascript_compiler.rb index 1713009491..998963ef55 100644 --- a/lib/theme_javascript_compiler.rb +++ b/lib/theme_javascript_compiler.rb @@ -39,6 +39,46 @@ class ThemeJavascriptCompiler end end + # Handle colocated components + tree.dup.each_pair do |filename, content| + is_component_template = filename.end_with?(".hbs") && filename.start_with?("#{root_name}/components/") + next if !is_component_template + template_contents = content + + hbs_invocation_options = { + moduleName: filename, + parseOptions: { + srcName: filename + } + } + hbs_invocation = "hbs(#{template_contents.to_json}, #{hbs_invocation_options.to_json})" + + prefix = <<~JS + import { hbs } from 'ember-cli-htmlbars'; + const __COLOCATED_TEMPLATE__ = #{hbs_invocation}; + JS + + js_filename = filename.sub(/\.hbs\z/, ".js") + js_contents = tree[js_filename] # May be nil for template-only component + if js_contents && !js_contents.include?("export default") + message = "#{filename} does not contain a `default export`. Did you forget to export the component class?" + js_contents += "throw new Error(#{message.to_json});" + end + + if js_contents.nil? + # No backing class, use template-only + js_contents = <<~JS + import templateOnly from '@ember/component/template-only'; + export default templateOnly(); + JS + end + + js_contents = prefix + js_contents + + tree[js_filename] = js_contents + tree.delete(filename) + end + # Transpile and write to output tree.each_pair do |filename, content| module_name, extension = filename.split(".", 2) diff --git a/spec/lib/theme_javascript_compiler_spec.rb b/spec/lib/theme_javascript_compiler_spec.rb index 68b506135b..46fa810ee5 100644 --- a/spec/lib/theme_javascript_compiler_spec.rb +++ b/spec/lib/theme_javascript_compiler_spec.rb @@ -86,5 +86,44 @@ RSpec.describe ThemeJavascriptCompiler do expect(compiler.content).to include('define("discourse/theme-1/components/mycomponent"') expect(compiler.content).to include('define("discourse/theme-1/discourse/templates/components/mycomponent"') end + + it "handles colocated components" do + compiler.append_tree( + { + "discourse/components/mycomponent.js" => <<~JS, + import Component from "@glimmer/component"; + export default class MyComponent extends Component {} + JS + "discourse/components/mycomponent.hbs" => "{{my-component-template}}" + } + ) + expect(compiler.content).to include("__COLOCATED_TEMPLATE__ =") + expect(compiler.content).to include("setComponentTemplate") + end + + it "prints error when default export missing" do + compiler.append_tree( + { + "discourse/components/mycomponent.js" => <<~JS, + import Component from "@glimmer/component"; + class MyComponent extends Component {} + JS + "discourse/components/mycomponent.hbs" => "{{my-component-template}}" + } + ) + expect(compiler.content).to include("__COLOCATED_TEMPLATE__ =") + expect(compiler.content).to include("throw new Error") + end + + it "handles template-only components" do + compiler.append_tree( + { + "discourse/components/mycomponent.hbs" => "{{my-component-template}}" + } + ) + expect(compiler.content).to include("__COLOCATED_TEMPLATE__ =") + expect(compiler.content).to include("setComponentTemplate") + expect(compiler.content).to include("@ember/component/template-only") + end end end