js extension mechanism for plugins, refs CNVS-5434

given app/coffeescripts/foo.coffee in canvas-lms, if you want to monkey
patch it from your plugin, create app/coffeescripts/extensions/foo.coffee
(in your plugin) like so:

define ->
  (Foo) ->
    Foo::zomg = -> "i added this method"
    Foo

and that's it, no changes required in canvas-lms, no plugin bundles, etc.

note that Foo is not an explicit dependency, it magically figures it out.
also note that your module should return a function that returns Foo.
this function will magically wrap around Foo so you can do stuff to it
anytime somebody requires "foo" as per usual.

test plan:
1. use it as explained above
2. it should work

Change-Id: If3b21782c0e79bb0ce55b4f16804047a2c2e2143
Reviewed-on: https://gerrit.instructure.com/20004
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Ryan Florence <ryanf@instructure.com>
Product-Review: Jon Jensen <jon@instructure.com>
QA-Review: Jon Jensen <jon@instructure.com>
This commit is contained in:
Jon Jensen 2013-04-23 17:42:05 -06:00
parent c8cd11388a
commit e6b36d6817
5 changed files with 122 additions and 2 deletions

View File

@ -4,7 +4,9 @@ guard 'coffeescript', :input => 'app/coffeescripts', :output => 'public/javascr
guard 'coffeescript', :input => 'spec/coffeescripts', :output => 'spec/javascripts'
guard 'jst', :input => 'app/views/jst', :output => 'public/javascripts/jst'
guard :styleguide
guard :js_extensions
Dir[File.join(File.dirname(__FILE__),'vendor/plugins/*/Guardfile')].each do |g|
eval(File.read(g))
end

27
guard/js_extensions.rb Normal file
View File

@ -0,0 +1,27 @@
require 'guard'
require 'guard/guard'
require 'lib/canvas/require_js/plugin_extension'
module Guard
class JsExtensions < Guard
def initialize(watchers = [], options = {})
pattern = %r{vendor/plugins/[^/]+/app/coffeescripts/extensions/(.+\.coffee)$}
super [::Guard::Watcher.new(pattern)], options
end
def run_on_additions(paths)
UI.info "Generating plugin extensions for #{paths.join(", ")}"
paths.each do |path|
path = path.gsub(%r{.*?/extensions/}, '')
Canvas::RequireJs::PluginExtension.generate(path)
end
UI.info "Successfully generated plugin extensions for #{paths.join(", ")}"
end
def run_all
UI.info "Generating all plugin extensions"
Canvas::RequireJs::PluginExtension.generate_all
UI.info "Successfully generated all plugin extensions"
end
end
end

View File

@ -56,7 +56,7 @@ module Canvas
:jqueryui => 'vendor/jqueryui',
:use => 'vendor/use',
:uploadify => '../flash/uploadify/jquery.uploadify-3.1.min'
}.update(plugin_paths).to_json.gsub(/([,{])/, "\\1\n ")
}.update(plugin_paths).update(Canvas::RequireJs::PluginExtension.paths).to_json.gsub(/([,{])/, "\\1\n ")
end
def plugin_paths

View File

@ -0,0 +1,79 @@
module Canvas
module RequireJs
module PluginExtension
class << self
# magical plugin js extension stuff
#
# given app/coffeescripts/foo.coffee in canvas-lms, if you want to
# monkey patch it from your plugin, create
# app/coffeescripts/extensions/foo.coffee (in your plugin) like so:
#
# define ->
# (Foo) ->
# Foo::zomg = -> "i added this method"
# Foo
#
# and that's it, no changes required in canvas-lms, no plugin
# bundles, etc.
#
# note that Foo is not an explicit dependency, it magically figures
# it out. also note that your module should return a function that
# accepts and returns Foo. this function will magically wrap around
# Foo so you can do stuff to it anytime somebody requires "foo" as
# per usual.
GLOB = "vendor/plugins/*/app/coffeescripts/extensions"
REGEXP = Regexp.new GLOB.gsub('*', '([^/]*)')
# added to require config paths so that when you require an
# extended module, you instead get the glue module which gives you
# the original plus the extensions
def paths
map.keys.inject({}) { |hash, file|
hash["compiled/#{file}_without_extensions"] = "compiled/#{file}"
hash["compiled/#{file}"] = "compiled/#{file}_with_extensions"
hash
}
end
def map
@map ||= Dir["#{GLOB}/**/*.coffee"].inject({}) { |hash, ext|
_, plugin, file = ext.match(%r{#{REGEXP}/(.*)\.coffee}).to_a
hash[file] ||= []
hash[file] << plugin
hash
}
end
# given a file, which plugins extend it?
def infer_extension_plugins(file)
Dir[GLOB].map { |match|
match.sub(REGEXP, '\1')
}
end
# create the glue module that requires the original and its
# extensions. when your bundle (or whatever) requires the original
# path, you'll get this module instead
def generate(file, plugins = infer_extension_plugins(file))
plugin_paths = plugins.map { |p| "#{p}/compiled/extensions/#{file}" }
plugin_paths.unshift "compiled/#{file}_without_extensions"
plugin_args = plugins.each_index.map { |i| "p#{i}" }
plugin_calls = plugin_args.reverse.inject("orig"){ |s, a| "#{a}(#{s})" }
FileUtils.mkdir_p(File.dirname("public/javascripts/compiled/#{file}"))
File.open("public/javascripts/compiled/#{file}_with_extensions.js", "w") { |f|
f.write <<-JS.gsub(/^ /, '')
define(#{plugin_paths.inspect}, function(orig, #{plugin_args.join(', ')}) {
return #{plugin_calls};
});
JS
}
end
def generate_all
map.each { |file, plugin| generate file, plugin }
end
end
end
end
end

View File

@ -46,7 +46,13 @@ namespace :js do
end
end
desc "generates compiled coffeescript and handlebars templates"
desc "generates plugin extension modules"
task :generate_extensions do
require 'canvas/require_js/plugin_extension'
Canvas::RequireJs::PluginExtension.generate_all
end
desc "generates compiled coffeescript, handlebars templates and plugin extensions"
task :generate do
require 'config/initializers/plugin_symlinks'
require 'fileutils'
@ -63,6 +69,12 @@ namespace :js do
FileUtils.rm_rf(paths_to_remove)
threads = []
threads << Thread.new do
puts "--> Generating plugin extensions"
extensions_time = Benchmark.realtime { Rake::Task['js:generate_extensions'].invoke }
puts "--> Generating plugin extensions finished in #{extensions_time}"
end
threads << Thread.new do
puts "--> Pre-compiling handlebars templates"
handlebars_time = Benchmark.realtime { Rake::Task['jst:compile'].invoke }