Skip to content

botanicus/commonjs_modules

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

59 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

About

Gem version Build status Coverage status CodeClimate status

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 accessors.

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.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages