├── LICENSE ├── README.md ├── Rakefile ├── example ├── app │ └── javascripts │ │ ├── application.js │ │ └── utils.js ├── config.ru └── public │ ├── index.html │ └── javascripts │ ├── plain.js │ └── yabble.js ├── lib └── rack │ ├── modulr.rb │ └── modulr │ ├── base.rb │ ├── config.rb │ ├── options.rb │ ├── request.rb │ ├── response.rb │ ├── source.rb │ └── version.rb └── rack-modulr.gemspec /LICENSE: -------------------------------------------------------------------------------- 1 | == License 2 | 3 | Based on work by Kelly Redding - http://github.com/kelredd/rack-less 4 | 5 | Copyright (c) 2010 Alex MacCaw (info@eribium.org) 6 | 7 | Permission is hereby granted, free of charge, to any person 8 | obtaining a copy of this software and associated documentation 9 | files (the "Software"), to deal in the Software without 10 | restriction, including without limitation the rights to use, 11 | copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the 13 | Software is furnished to do so, subject to the following 14 | conditions: 15 | 16 | The above copyright notice and this permission notice shall be 17 | included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | OTHER DEALINGS IN THE SOFTWARE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [rack-modulr](http://github.com/maccman/rack-modulr) lets you easily use [Common.JS](http://www.sitepen.com/blog/2010/07/16/asynchronous-commonjs-modules-for-the-browser-and-introducing-transporter/) modules in your Rack/Rails applications. 2 | 3 | ##Prerequisites 4 | 5 | Although not required, it's recommended you use [this custom version](https://github.com/maccman/modulr) of the Modulr gem. 6 | 7 | ##Usage 8 | 9 | For example with Rack: 10 | 11 | require "rack/modulr" 12 | 13 | use Rack::Modulr, :modulr => {:minify => true} 14 | run Proc.new { [200, {"Content-Type" => "text/html"}, []] } 15 | 16 | Or with Rails: 17 | 18 | // Gemfile 19 | 20 | gem "modulr", :git => "git://github.com/maccman/modulr.git" 21 | gem "rack-modulr", :git => "git://github.com/maccman/rack-modulr.git" 22 | 23 | // config/application.rb 24 | require "rack/modulr" 25 | config.middleware.use "Rack::Modulr" 26 | 27 | Then any modules in `app/javascripts` will be automatically parsed by [Modulr](https://github.com/maccman/modulr) 28 | 29 | // app/javascripts/utils.js 30 | exports.sum = function(val1, val2){ 31 | return(val1 + val2); 32 | }; 33 | 34 | // app/javascripts/application.js 35 | var utils = require("./utils"); 36 | console.log(utils.sum(1, 2)); 37 | 38 | When the browser requests a module, all its dependencies will be recursively resolved. 39 | 40 | $ curl "http://localhost:5001/javascripts/application.js" 41 | 42 | (function() { 43 | require.define({ 44 | 'utils': function(require, exports, module) { 45 | exports.sum = function(val1, val2){ 46 | return(val1 + val2); 47 | }; 48 | } 49 | }); 50 | 51 | require.ensure(['utils'], function(require) { 52 | var utils = require("./utils"); 53 | console.log(utils.sum(1, 2)); 54 | }); 55 | })(); 56 | 57 | Modulr injects a module loader library but if you want to use a different one, like [Yabble](https://github.com/jbrantly/yabble), you'll need to pass the `:custom_loader` option to `Rack::Modulr`: 58 | 59 | use Rack::Modulr, :modulr => {:custom_loader => true} 60 | 61 | Rack::Modulr caches the compiled modules in memory. Every request, the request module will be checked, to see if it's mtime has changed. If the module hasn't been changed, it'll be served from memory if possible. 62 | 63 | By defaulting, caching and minification are turned off. To turn them on in the production environment, for example, set the global settings on Rack::Modulr. 64 | 65 | // production.rb 66 | Rack::Modulr.configure do |config| 67 | config.cache = true 68 | config.minify = true 69 | end 70 | 71 | ---------------------------------------------------- 72 | 73 | Based on [Kelly Redding's](https://github.com/kelredd) great work on [rack-less](http://github.com/kelredd/rack-less). -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake/gempackagetask' 3 | 4 | require 'lib/rack/modulr/version' 5 | 6 | spec = Gem::Specification.new do |s| 7 | s.name = 'rack-modulr' 8 | s.version = RackModulr::Version.to_s 9 | s.summary = "CommonJS modules for Ruby web apps." 10 | s.author = 'Alex MacCaw' 11 | s.email = 'maccman@gmail.com' 12 | s.homepage = 'http://github.com/maccman/rack-modulr' 13 | s.files = %w(README.md Rakefile) + Dir.glob("{lib}/**/*") 14 | 15 | s.add_dependency("rack", [">= 0.4"]) 16 | s.add_dependency("modulr", [">= 0.7.1"]) 17 | end 18 | 19 | Rake::GemPackageTask.new(spec) do |pkg| 20 | pkg.gem_spec = spec 21 | end 22 | 23 | desc 'Generate the gemspec to serve this gem' 24 | task :gemspec do 25 | file = File.dirname(__FILE__) + "/#{spec.name}.gemspec" 26 | File.open(file, 'w') {|f| f << spec.to_ruby } 27 | puts "Created gemspec: #{file}" 28 | end 29 | 30 | task :default => :gem 31 | -------------------------------------------------------------------------------- /example/app/javascripts/application.js: -------------------------------------------------------------------------------- 1 | var utils = require("./utils"); 2 | console.log("Percentage", utils.per(50, 200)); -------------------------------------------------------------------------------- /example/app/javascripts/utils.js: -------------------------------------------------------------------------------- 1 | 2 | exports.per = function(value, total) { 3 | return( (value / total) * 100 ); 4 | }; -------------------------------------------------------------------------------- /example/config.ru: -------------------------------------------------------------------------------- 1 | $: << File.join(File.dirname(__FILE__), *%w[ .. lib ]) 2 | require "rack/modulr" 3 | 4 | use Rack::Modulr 5 | 6 | Rack::Modulr.configure do |config| 7 | config.custom_loader = true 8 | config.cache = false 9 | config.minify = false 10 | end 11 | 12 | run Rack::Directory.new("public") -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | rack-modulr 5 | 6 | 7 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /example/public/javascripts/plain.js: -------------------------------------------------------------------------------- 1 | // Plain -------------------------------------------------------------------------------- /example/public/javascripts/yabble.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2010 James Brantly 3 | * 4 | * Permission is hereby granted, free of charge, to any person 5 | * obtaining a copy of this software and associated documentation 6 | * files (the "Software"), to deal in the Software without 7 | * restriction, including without limitation the rights to use, 8 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the 10 | * Software is furnished to do so, subject to the following 11 | * conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be 14 | * included in all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | * OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | (function(globalEval) { 27 | 28 | var Yabble = function() { 29 | throw "Synchronous require() is not supported."; 30 | }; 31 | 32 | Yabble.unit = {}; 33 | 34 | var _moduleRoot = '', 35 | _modules, 36 | _callbacks, 37 | _fetchFunc, 38 | _timeoutLength = 20000, 39 | _mainProgram; 40 | 41 | 42 | var head = document.getElementsByTagName('head')[0]; 43 | 44 | // Shortcut to native hasOwnProperty 45 | var hasOwnProperty = Object.prototype.hasOwnProperty; 46 | 47 | // A for..in implementation which uses hasOwnProperty and fixes IE non-enumerable issues 48 | if ((function() {for (var prop in {hasOwnProperty: true}) { return prop; }})() == 'hasOwnProperty') { 49 | var forIn = function(obj, func, ctx) { 50 | for (var prop in obj) { 51 | if (hasOwnProperty.call(obj, prop)) { 52 | func.call(ctx, prop); 53 | } 54 | } 55 | }; 56 | } 57 | else { 58 | var ieBadProps = [ 59 | 'isPrototypeOf', 60 | 'hasOwnProperty', 61 | 'toLocaleString', 62 | 'toString', 63 | 'valueOf' 64 | ]; 65 | 66 | var forIn = function(obj, func, ctx) { 67 | for (var prop in obj) { 68 | if (hasOwnProperty.call(obj, prop)) { 69 | func.call(ctx, prop); 70 | } 71 | } 72 | 73 | for (var i = ieBadProps.length; i--;) { 74 | var prop = ieBadProps[i]; 75 | if (hasOwnProperty.call(obj, prop)) { 76 | func.call(ctx, prop); 77 | } 78 | } 79 | }; 80 | } 81 | 82 | // Array convenience functions 83 | var indexOf = function(arr, val) { 84 | for (var i = arr.length; i--;) { 85 | if (arr[i] == val) { return i; } 86 | } 87 | return -1; 88 | }; 89 | 90 | var removeWhere = function(arr, func) { 91 | var i = 0; 92 | while (i < arr.length) { 93 | if (func.call(null, arr[i], i) === true) { 94 | arr.splice(i, 1); 95 | } 96 | else { 97 | i++; 98 | } 99 | } 100 | }; 101 | 102 | var combinePaths = function(relPath, refPath) { 103 | var relPathParts = relPath.split('/'); 104 | refPath = refPath || ''; 105 | if (refPath.length && refPath.charAt(refPath.length-1) != '/') { 106 | refPath += '/'; 107 | } 108 | var refPathParts = refPath.split('/'); 109 | refPathParts.pop(); 110 | var part; 111 | while (part = relPathParts.shift()) { 112 | if (part == '.') { continue; } 113 | else if (part == '..' 114 | && refPathParts.length 115 | && refPathParts[refPathParts.length-1] != '..') { refPathParts.pop(); } 116 | else { refPathParts.push(part); } 117 | } 118 | return refPathParts.join('/'); 119 | }; 120 | 121 | // Takes a relative path to a module and resolves it according to the reference path 122 | var resolveModuleId = Yabble.unit.resolveModuleId = function(relModuleId, refPath) { 123 | if (relModuleId.charAt(0) != '.') { 124 | return relModuleId; 125 | } 126 | else { 127 | return combinePaths(relModuleId, refPath); 128 | } 129 | }; 130 | 131 | // Takes a module's ID and resolves a URI according to the module root path 132 | var resolveModuleUri = function(moduleId) { 133 | if (moduleId.charAt(0) != '.') { 134 | return _moduleRoot+moduleId+'.js'; 135 | } 136 | else { 137 | return this._resolveModuleId(moduleId, _moduleRoot)+'.js'; 138 | } 139 | }; 140 | 141 | // Returns a module object from the module ID 142 | var getModule = function(moduleId) { 143 | if (!hasOwnProperty.call(_modules, moduleId)) { 144 | return null; 145 | } 146 | return _modules[moduleId]; 147 | }; 148 | 149 | // Adds a callback which is executed when all deep dependencies are loaded 150 | var addCallback = function(deps, cb) { 151 | _callbacks.push([deps.slice(0), cb]); 152 | }; 153 | 154 | // Generic implementation of require.ensure() which takes a reference path to 155 | // use when resolving relative module IDs 156 | var ensureImpl = function(deps, cb, refPath) { 157 | var unreadyModules = []; 158 | 159 | for (var i = deps.length; i--;) { 160 | var moduleId = resolveModuleId(deps[i], refPath), 161 | module = getModule(moduleId); 162 | 163 | if (!areDeepDepsDefined(moduleId)) { 164 | unreadyModules.push(moduleId); 165 | } 166 | } 167 | 168 | if (unreadyModules.length) { 169 | addCallback(unreadyModules, function() { 170 | cb(createRequireFunc(refPath)); 171 | }); 172 | queueModules(unreadyModules); 173 | } 174 | else { 175 | setTimeout(function() { 176 | cb(createRequireFunc(refPath)); 177 | }, 0); 178 | } 179 | }; 180 | 181 | // Creates a require function that is passed into module factory functions 182 | // and require.ensure() callbacks. It is bound to a reference path for 183 | // relative require()s 184 | var createRequireFunc = function(refPath) { 185 | var require = function(relModuleId) { 186 | var moduleId = resolveModuleId(relModuleId, refPath), 187 | module = getModule(moduleId); 188 | 189 | if (!module) { 190 | throw "Module not loaded"; 191 | } 192 | else if (module.error) { 193 | throw "Error loading module"; 194 | } 195 | 196 | if (!module.exports) { 197 | module.exports = {}; 198 | var moduleDir = moduleId.substring(0, moduleId.lastIndexOf('/')+1), 199 | injects = module.injects, 200 | args = []; 201 | 202 | for (var i = 0, n = injects.length; i= 0; 442 | }); 443 | 444 | transport.modules.push({ 445 | id: arguments[0], 446 | factory: arguments[2], 447 | injects: arguments[1] 448 | }); 449 | } 450 | return transport; 451 | }; 452 | 453 | // Set the uri which forms the conceptual module namespace root 454 | Yabble.setModuleRoot = function(path) { 455 | if (!(/^http(s?):\/\//.test(path))) { 456 | var href = window.location.href; 457 | href = href.substr(0, href.lastIndexOf('/')+1); 458 | path = combinePaths(path, href); 459 | } 460 | 461 | if (path.length && path.charAt(path.length-1) != '/') { 462 | path += '/'; 463 | } 464 | 465 | _moduleRoot = path; 466 | }; 467 | 468 | // Set a timeout period for async module loading 469 | Yabble.setTimeoutLength = function(milliseconds) { 470 | _timeoutLength = milliseconds; 471 | }; 472 | 473 | // Use script tags with wrapped code instead of XHR+eval() 474 | Yabble.useScriptTags = function() { 475 | _fetchFunc = loadModuleByScript; 476 | }; 477 | 478 | // Define a module per various transport specifications 479 | Yabble.def = Yabble.define = function() { 480 | var transport = normalizeTransport.apply(null, arguments); 481 | 482 | var unreadyModules = [], 483 | definedModules = []; 484 | 485 | var deps = transport.deps; 486 | 487 | for (var i = transport.modules.length; i--;) { 488 | var moduleDef = transport.modules[i], 489 | moduleId = moduleDef.id, 490 | module = getModule(moduleId); 491 | 492 | if (!module) { 493 | module = _modules[moduleId] = {}; 494 | } 495 | module.module = { 496 | id: moduleId, 497 | uri: resolveModuleUri(moduleId) 498 | }; 499 | 500 | module.defined = true; 501 | module.deps = deps.slice(0); 502 | module.injects = moduleDef.injects; 503 | module.factory = moduleDef.factory; 504 | definedModules.push(module); 505 | } 506 | 507 | for (var i = deps.length; i--;) { 508 | var moduleId = deps[i], 509 | module = getModule(moduleId); 510 | 511 | if (!module || !areDeepDepsDefined(moduleId)) { 512 | unreadyModules.push(moduleId); 513 | } 514 | } 515 | 516 | if (unreadyModules.length) { 517 | setTimeout(function() { 518 | queueModules(unreadyModules); 519 | }, 0); 520 | } 521 | 522 | fireCallbacks(); 523 | }; 524 | 525 | Yabble.isKnown = function(moduleId) { 526 | return getModule(moduleId) != null; 527 | }; 528 | 529 | Yabble.isDefined = function(moduleId) { 530 | var module = getModule(moduleId); 531 | return !!(module && module.defined); 532 | }; 533 | 534 | // Do an async lazy-load of modules 535 | Yabble.ensure = function(deps, cb) { 536 | ensureImpl(deps, cb, ''); 537 | }; 538 | 539 | // Start an application via a main program module 540 | Yabble.run = function(program, cb) { 541 | program = _mainProgram = resolveModuleId(program, ''); 542 | Yabble.ensure([program], function(require) { 543 | require(program); 544 | if (cb != null) { cb(); } 545 | }); 546 | }; 547 | 548 | // Reset internal state. Used mostly for unit tests. 549 | Yabble.reset = function() { 550 | _mainProgram = null; 551 | _modules = {}; 552 | _callbacks = []; 553 | 554 | // Built-in system module 555 | Yabble.define({ 556 | 'system': function(require, exports, module) {} 557 | }); 558 | }; 559 | 560 | Yabble.reset(); 561 | 562 | // Export to the require global 563 | window.require = Yabble; 564 | })(function(code) { 565 | return (window.eval || eval)(code, null); 566 | }); -------------------------------------------------------------------------------- /lib/rack/modulr.rb: -------------------------------------------------------------------------------- 1 | require 'rack' 2 | require 'rack/modulr/base' 3 | require 'rack/modulr/config' 4 | require 'rack/modulr/request' 5 | require 'rack/modulr/response' 6 | require 'rack/modulr/source' 7 | 8 | # === Usage 9 | # 10 | # Create with default configs: 11 | # require 'rack/modulr' 12 | # Rack::Modulr.new(app) 13 | # 14 | # Within a rackup file (or with Rack::Builder): 15 | # require 'rack/modulr' 16 | # 17 | # use Rack::Modulr, 18 | # :source => 'app/modulr' 19 | # 20 | # run app 21 | 22 | module Rack::Modulr 23 | MIME_TYPE = "text/javascript" 24 | 25 | class << self 26 | # Configuration accessors for Rack::Modulr 27 | # (see config.rb for details) 28 | def configure 29 | yield config if block_given? 30 | end 31 | 32 | def config 33 | @config ||= Config.new 34 | end 35 | 36 | def config=(value) 37 | @config = value 38 | end 39 | end 40 | 41 | # Create a new Rack::Modulr middleware component 42 | # => the +options+ Hash can be used to specify default configuration values 43 | # => (see Rack::Modulr::Options for possible key/values) 44 | def self.new(app, options={}, &block) 45 | Base.new(app, options, &block) 46 | end 47 | end -------------------------------------------------------------------------------- /lib/rack/modulr/base.rb: -------------------------------------------------------------------------------- 1 | require 'rack/modulr/options' 2 | require 'rack/modulr/request' 3 | require 'rack/modulr/response' 4 | 5 | module Rack::Modulr 6 | class Base 7 | include Rack::Modulr::Options 8 | YEAR_IN_SECONDS = 31540000 9 | 10 | def initialize(app, options = {}) 11 | @app = app 12 | initialize_options options 13 | yield self if block_given? 14 | validate_options 15 | end 16 | 17 | # If CommonJS modules are being requested, this is an endpoint: 18 | # => generate the compiled js 19 | # => respond appropriately 20 | # Otherwise, call on up to the app as normal 21 | def call(env) 22 | @default_options.each { |k,v| env[k] ||= v } 23 | @env = env.dup.freeze 24 | 25 | if (@request = Request.new(@env)).for_modulr? 26 | response = Response.new(@env, source_for(@request)) 27 | cache(response) { response.to_rack } 28 | else 29 | @app.call(env) 30 | end 31 | end 32 | 33 | protected 34 | 35 | def cache? 36 | Rack::Modulr.config.cache? 37 | end 38 | 39 | def cache(response) 40 | return yield unless cache? 41 | 42 | env = response.env 43 | headers = response.headers 44 | 45 | headers["Cache-Control"] = "public, must-revalidate" 46 | if env["QUERY_STRING"] == response.md5 47 | headers["Cache-Control"] << ", max-age=#{YEAR_IN_SECONDS}" 48 | end 49 | 50 | headers["ETag"] = %("#{response.md5}") 51 | headers["Last-Modified"] = response.last_modified.httpdate 52 | 53 | if etag = env["HTTP_IF_NONE_MATCH"] 54 | return [304, headers.to_hash, []] if etag == headers["ETag"] 55 | end 56 | 57 | if time = env["HTTP_IF_MODIFIED_SINCE"] 58 | return [304, headers.to_hash, []] if time == headers["Last-Modified"] 59 | end 60 | 61 | yield 62 | end 63 | 64 | def source_for(request) 65 | @source ||= request.source 66 | 67 | previous_last_modified, @last_modified = @last_modified, @source.mtime 68 | unchanged = previous_last_modified == @last_modified 69 | 70 | (unchanged && cache?) ? @source : (@source = request.source) 71 | end 72 | 73 | def validate_options 74 | # ensure a root path is specified and does exists 75 | unless options.has_key?(option_name(:root)) and !options(:root).nil? 76 | raise(ArgumentError, "no :root option set") 77 | end 78 | set :root, File.expand_path(options(:root)) 79 | 80 | # ensure a source path is specified and does exists 81 | unless options.has_key?(option_name(:source)) and !options(:source).nil? 82 | raise(ArgumentError, "no :source option set") 83 | end 84 | end 85 | end 86 | end -------------------------------------------------------------------------------- /lib/rack/modulr/config.rb: -------------------------------------------------------------------------------- 1 | module Rack::Modulr 2 | class Config 3 | 4 | ATTRIBUTES = [:cache, :minify, :modulr] 5 | attr_accessor *ATTRIBUTES 6 | 7 | DEFAULTS = { 8 | :cache => false, 9 | :minify => false, 10 | :modulr => {} 11 | } 12 | 13 | def initialize(settings = {}) 14 | ATTRIBUTES.each do |a| 15 | send("#{a}=", settings[a] || DEFAULTS[a]) 16 | end 17 | end 18 | 19 | def modulr 20 | @modulr ||= {} 21 | end 22 | 23 | def minify=(val) 24 | self.modulr[:minify] = val 25 | end 26 | 27 | def custom_loader=(val) 28 | self.modulr[:custom_loader] = val 29 | end 30 | 31 | def cache? 32 | !!self.cache 33 | end 34 | end 35 | end -------------------------------------------------------------------------------- /lib/rack/modulr/options.rb: -------------------------------------------------------------------------------- 1 | module Rack::Modulr 2 | module Options 3 | 4 | # Handles options for Rack::Modulr 5 | # Available options: 6 | # => root 7 | # the app root. the reference point for the 8 | # source and public options 9 | # => source 10 | # the path (relative to the root) where 11 | # CommonJS files are located 12 | # => public 13 | # the path (relative to the root) where 14 | # static files are served 15 | # => hosted_at 16 | # the public HTTP root path for javascripts 17 | 18 | # Note: the following code is heavily influenced by: 19 | # => http://github.com/rtomayko/rack-cache/blob/master/lib/rack/cache/options.rb 20 | # => thanks to rtomayko, I thought his approach was really smart. 21 | 22 | RACK_ENV_NS = "rack-modulr" 23 | 24 | module ClassMethods 25 | 26 | def defaults 27 | { 28 | option_name(:root) => ".", 29 | option_name(:source) => 'app/javascripts', 30 | option_name(:public) => 'public', 31 | option_name(:hosted_at) => '/javascripts', 32 | option_name(:modulr) => {} 33 | } 34 | end 35 | 36 | # Rack::Modulr uses the Rack Environment to store option values. All options 37 | # are stored in the Rack Environment as ".