About
This is experimental CommonJS modules implementation in Ruby. The main difference is that in this implementation everything is local, there isn't any messing with the global namespace. It has a lot of advantages include hot code reloading.
Example
File lib/task.rb
class Task
attr_reader :name
def initialize(name)
@name
end
end
# Export single value.
export { Task }
File lib/runner.rb
Task = import('task')
# Export a variable.
export VERSION: '0.0.1'
# Export a function.
def exports.main(args)
task = Task.new(args.shift)
puts "~ #{task.name}"
end
File bin/main.rb
#!/usr/bin/env ruby -Ilib
require 'import'
runner = import('runner')
# => #<Imports::Export:0x00007f8dae26cd00
# @_DATA_ = {
# :VERSION => "0.0.1",
# :main => #<Method #main>},
# @_FILE_ = "lib/runner.rb">
# Run the code.
runner.main(ARGV)
The syntax is very flexible. Check the examples for more.
API
Kernel#import
Kernel#import
is a substitute for:
-
Kernel#require
when used with a path relative to$LOAD_PATH
or -
Kernel#require_relative
when used with a path starting with./
or../
.
Imports::Context#exports
This object is available as a top-level method, since everything is evaluated against an instance of Import::Context
You can assign anything to exports
. Currently the only limitation is that the value cannot be nil
.
exports.VERSION = '0.0.1'
# import('example.rb')
# => #<Imports::Export:0x00007f8dae26cd00
# @_DATA_ = {
# :VERSION => "0.0.1",
# @_FILE_ = "example.rb">
If you export key default
, then only specified value will be exported, rather than an instance of Imports::Exports
holding multiple values.
exports.default = "Only this will be exported."
# import('example.rb')
# => "Only this will be exported."
You can also define singleton methods on the exports
object:
def exports.main(*args)
# TODO: Implement me.
end
# => #<Imports::Export:0x00007f8dae26cd00
# @_DATA_ = {
# :main => #<Method #main>},
# @_FILE_ = "example.rb">
This is the only thing that the export
method doesn't support.
Also, here we are in an Imports::Exports
instance rather than in Imports::Context
.
Because of that we use __ACCESSOR__
s on Imports::Exports
rather than accessor
s.
Imports::Context#export
This is a convenience method for assigning things to exports
Exporting default value
# Using a block.
export { DefaultValue }
# Using hash.
export default: DefaultValue
Exporting multiple values
# Using hash.
export one: ClassOne, two: ClassTwo
# Using names from #name as the key.
# Every exported object has to have the #name method defined.
# That means you have to do it manually for anonymous classes.
class ClassOne; end
class ClassTwo; end
ClassThree = Class.new do
def self.name
'ClassThree'
end
end
export ClassOne, ClassTwo, ClassThree
Discussion
The thing about private APIs in Ruby
Ruby developers seldom distinguish between public and private APIs in their projects. Everything's goes into the global namespace, hencer everything is kinda public.
With commonjs_modules, you can choose what you export and what not.
TODO: Example.
Usage of modules
This makes use of Ruby modules for namespacing obsolete. Obviously, they still have their use as mixins.
This is a great news. With one global namespace, it's necessary to go full on with the namespacing craziness having all these LibName::SubModule::Module::ClassName
and dealing either with horrible nesting or with potential for missing module on which we want to definie a class.
Without a global namespace, everything is essentially flat. If we import a Task
, there's no chance of colision, because we import everything manually and it's crystal clear where every single thing is coming from.
Why bother if no one else is using it?
Even though all the gems out there are using the global namespace, it doesn't matter, it still a great way to organise your code. It plays well with the traditional approach.
Usage of refinements
No more monkey-patching
OriginalLib = import('original_lib')
class OriginalLibNew < OriginalLib
def method_i_want_to_override
# ..
end
end
Static code analysers
The big downside is you can way goodbye YARD, RDoc and many other static code analysers.
I assume Rubocop, CodeClimate and similar tools will be thrown off as well.
Standard Ruby compatibility
class Hour; end
export { Hour } if defined?(export)
TODO
- It DOES make sense to have default and others, see interfacer!
- This sets name: a = Testx = Class.new
- Create missing tests, fix existing ones.
- exports.default = Class.new {}. What .name to set? The file I guess.
- Tag and release version 0.1.
- This:
exports.myFnName do
end
# name here?
exports.default do
end
# -> Same as def exports.myFnName, but different namespace.
- Tweak rSpec to evaluate test files against
Imports::Context
or something alike, otherwise we have the annoying constant was already defined messages all over.