{"id":13878393,"url":"https://github.com/shioyama/im","last_synced_at":"2025-04-07T05:16:41.018Z","repository":{"id":45212221,"uuid":"513405365","full_name":"shioyama/im","owner":"shioyama","description":"Isolated Module Autoloader for Ruby","archived":false,"fork":false,"pushed_at":"2023-06-03T07:01:32.000Z","size":817,"stargazers_count":117,"open_issues_count":1,"forks_count":1,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-03-31T04:06:53.463Z","etag":null,"topics":["im","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/shioyama.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"MIT-LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-07-13T06:24:58.000Z","updated_at":"2024-12-30T16:36:41.000Z","dependencies_parsed_at":"2024-10-27T00:57:45.636Z","dependency_job_id":null,"html_url":"https://github.com/shioyama/im","commit_stats":{"total_commits":601,"total_committers":29,"mean_commits":"20.724137931034484","dds":"0.16139767054908483","last_synced_commit":"8c8a6b16732b084fe9cdd32d35f98a7aeee956cc"},"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shioyama%2Fim","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shioyama%2Fim/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shioyama%2Fim/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shioyama%2Fim/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/shioyama","download_url":"https://codeload.github.com/shioyama/im/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247595335,"owners_count":20963943,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["im","ruby"],"created_at":"2024-08-06T08:01:48.239Z","updated_at":"2025-04-07T05:16:40.982Z","avatar_url":"https://github.com/shioyama.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# Im\n\n[![Gem Version](https://badge.fury.io/rb/im.svg)][gem]\n[![Build Status](https://github.com/shioyama/im/actions/workflows/ci.yml/badge.svg)][actions]\n\n[gem]: https://rubygems.org/gems/im\n[actions]: https://github.com/shioyama/im/actions\n\n\u003c!-- TOC --\u003e\n\n- [Introduction](#introduction)\n- [Synopsis](#synopsis)\n- [File structure](#file-structure)\n  - [File paths match constant paths under loader](#file-paths-match-constant-paths-under-loader)\n  - [Root directories](#root-directories)\n  - [Relative and absolute cpaths](#relative-and-absolute-cpaths)\n- [Usage](#usage)\n- [Motivation](#motivation)\n- [License](#license)\n\n\u003c!-- /TOC --\u003e\n\n\u003ca id=\"markdown-introduction\" name=\"introduction\"\u003e\u003c/a\u003e\n## Introduction\n\nIm is a thread-safe code loader for anonymous-rooted namespaces in Ruby. It\nallows you to share any nested, autoloaded set of code without polluting or in\nany way touching the global namespace.\n\nTo do this, Im leverages code autoloading, Zeitwerk conventions around file\nstructure and naming, and two features added in Ruby 3.2: `Kernel#load`\nwith a module argument[^1] and `Module#const_added`[^2]. Since these Ruby\nfeatures are essential to its design, Im is not usable with earlier versions\nof Ruby.\n\nIm started its life as a fork of Zeitwerk and has a very similar interface. Im\nand Zeitwerk can be used alongside each other provided there is no overlap\nbetween file paths managed by each gem.\n\nIm is in active development and should be considered experimental until the\neventual release of version 1.0. Versions 0.1.6 and earlier of the gem were\npart of a different experiment and are unrelated to the current gem.\n\n\u003ca id=\"markdown-synopsis\" name=\"synopsis\"\u003e\u003c/a\u003e\n## Synopsis\n\nIm's public interface is in most respects identical to that of Zeitwerk. The\ncentral difference is that whereas Zeitwerk loads constants into the global\nnamespace (rooted in `Object`), Im loads them into anonymous namespaces rooted\non the loader itself. `Im::Loader` is a subclass of `Module`, and thus each\nloader instance can define its own namespace. Since there can be arbitrarily\nmany loaders, there can also be arbitrarily many autoloaded namespaces.\n\nIm's gem interface looks like this:\n\n```ruby\n# lib/my_gem.rb (main file)\n\nrequire \"im\"\nloader = Im::Loader.for_gem\nloader.setup # ready!\n\nmodule loader::MyGem\n  # ...\nend\n\nloader.eager_load # optionally\n```\n\nThe generic interface is identical to Zeitwerk's:\n\n```ruby\nloader = Zeitwerk::Loader.new\nloader.push_dir(...)\nloader.setup # ready!\n```\n\nOther than gem names, the only difference here is in the definition of `MyGem`\nunder the loader namespace in the gem code. Unlike Zeitwerk, with Im the gem\nnamespace is not defined at toplevel:\n\n```ruby\nObject.const_defined?(:MyGem)\n#=\u003e false\n```\n\nIn order to prevent leakage, the gem's entrypoint, in this case\n`lib/my_gem.rb`, must not define anything at toplevel, hence the use of\n`module loader::MyGem`.\n\nOnce the entrypoint has been required, all constants defined within the gem's\nfile structure are autoloadable from the loader itself:\n\n```ruby\n# lib/my_gem/foo.rb\n\nmodule MyGem\n  class Foo\n    def hello_world\n      \"Hello World!\"\n    end\n  end\nend\n```\n\n```ruby\nfoo = loader::MyGem::Foo\n# loads `Foo` from lib/my_gem/foo.rb\n\nfoo.new.hello_world\n#=\u003e \"Hello World!\"\n```\n\nConstants under the loader can be given permanent names that are different from\nthe one defined in the gem itself:\n\n```ruby\nBar = loader::MyGem::Foo\nBar.new.hello_world\n#=\u003e \"Hello World!\"\n```\n\nLike Zeitwerk, Im keeps a registry of all loaders, so the loader objects won't\nbe garbage collected. For convenience, Im also provides a method, `Im#import`,\nto fetch a loader for a given file path:\n\n```ruby\nrequire \"im\"\nrequire \"my_gem\"\n\nextend Im\nmy_gem = import \"my_gem\"\n#=\u003e my_gem::MyGem is autoloadable\n```\n\nReloading works like Zeitwerk:\n\n```ruby\nloader = Im::Loader.new\nloader.push_dir(...)\nloader.enable_reloading # you need to opt-in before setup\nloader.setup\n...\nloader.reload\n```\n\nYou can assign a permanent name to an autoloaded constant, and it will be\nreloaded when the loader is reloaded:\n\n```ruby\nFoo = loader::Foo\nloader.reload # Object::Foo is replaced by an autoload\nFoo #=\u003e autoload is triggered, reloading loader::Foo\n```\n\nLike Zeitwerk, you can eager-load all the code at once:\n\n```ruby\nloader.eager_load\n```\n\nAlternatively, you can broadcast `eager_load` to all loader instances:\n\n```ruby\nIm::Loader.eager_load_all\n```\n\n\u003ca id=\"markdown-file-structure\" name=\"file-structure\"\u003e\u003c/a\u003e\n## File structure\n\n\u003ca id=\"markdown-the-idea-file-paths-match-constant-paths-under-loader\" name=\"the-idea-file-paths-match-constant-paths-under-loader\"\u003e\u003c/a\u003e\n### File paths match constant paths under loader\n\nFile structure is identical to Zeitwerk, again with the difference that\nconstants are loaded from the loader's namespace rather than the root one:\n\n```\nlib/my_gem.rb         -\u003e loader::MyGem\nlib/my_gem/foo.rb     -\u003e loader::MyGem::Foo\nlib/my_gem/bar_baz.rb -\u003e loader::MyGem::BarBaz\nlib/my_gem/woo/zoo.rb -\u003e loader::MyGem::Woo::Zoo\n```\n\nIm inherits support for collapsing directories and custom inflection, see\nZeitwerk's documentation for details on usage of these features.\n\n\u003ca id=\"markdown-root-directories\" name=\"root-directories\"\u003e\u003c/a\u003e\n### Root directories\n\nInternally, each loader in Im can have one or more _root directories_ from which\nit loads code onto itself. Root directories are added to the loader using\n`Im::Loader#push_dir`:\n\n```ruby\nloader.push_dir(\"#{__dir__}/models\")\nloader.push_dir(\"#{__dir__}/serializers\"))\n```\n\nNote that concept of a _root namespace_, which Zeitwerk uses to load code\nunder a given node of the global namespace, is absent in Im. Custom root\nnamespaces are likewise not supported. These features were removed as they add\ncomplexity for little gain given Im's flexibility to anchor a namespace\nanywhere in the global namespace.\n\n\u003ca id=\"markdown-relative-and-absolute-cpaths\" name=\"relative-and-absolute-cpaths\"\u003e\u003c/a\u003e\n### Relative and absolute cpaths\n\nIm uses two types of constant paths: relative and absolute, wherever possible\ndefaulting to relative ones. A _relative cpath_ is a constant name relative to\nthe loader in which it was originally defined, regardless of any other names it\nwas later assigned. Whereas Zeitwerk uses absolute cpaths, Im uses relative\ncpaths for all external loader APIs (see usage for examples).\n\nTo understand these concepts, it is important first to distinguish between two\ntypes of names in Ruby: _temporary names_ and _permanent names_.\n\nA _temporary name_ is a constant name on an anonymous-rooted namespace, for\nexample a loader:\n\n```ruby\nmy_gem = import \"my_gem\"\nmy_gem::Foo\nmy_gem::Foo.name\n#=\u003e \"#\u003cIm::Loader ...\u003e::Foo\"\n```\n\nHere, the string `\"#\u003cIm::Loader ...\u003e::Foo\"` is called a temporary name. We can\ngive this module a _permanent name_ by assigning it to a toplevel constant:\n\n```ruby\nBar = my_gem::Foo\nmy_gem::Foo.name\n#=\u003e \"Bar\"\n```\n\nNow its name is `\"Bar\"`, and it is near impossible to get back its original\ntemporary name.\n\nThis property of module naming in Ruby is problematic since cpaths are used as\nkeys in Im's internal registries to index constants and their autoloads, which\nis critical for successful autoloading.\n\nTo get around this issue, Im tracks all module names and uses relative naming\ninside loader code. Internally, Im has a method, `relative_cpath`, which can\ngenerate any module name under a module in the loader namespace:\n\n```ruby\nmy_gem.send(:relative_cpath, loader::Foo, :Baz)\n#=\u003e \"Foo::Baz\"\n```\n\nUsing relative cpaths frees Im from depending on `Module#name` for\nregistry keys like Zeitwerk does, which does not work with anonymous\nnamespaces. All public methods in Im that take a `cpath` take the _relative_\ncpath, i.e. the cpath relative to the loader as toplevel, regardless of any\ntoplevel-rooted constant a module may have been assigned to.\n\n\u003ca id=\"markdown-usage\" name=\"usage\"\u003e\u003c/a\u003e\n## Usage\n\n(TODO)\n\n\u003ca id=\"markdown-motivation\" name=\"motivation\"\u003e\u003c/a\u003e\n## Motivation\n\n(TODO)\n\n## Related\n\n- [Demo Rails app](https://github.com/shioyama/rails_on_im) using Im to isolate\n  the application under one namespace\n\n\u003ca id=\"markdown-license\" name=\"license\"\u003e\u003c/a\u003e\n## License\n\nReleased under the MIT License, Copyright (c) 2023 Chris Salzberg and 2019–\u003ci\u003eω\u003c/i\u003e Xavier Noria.\n\n[^1]: https://bugs.ruby-lang.org/issues/6210\n[^2]: https://bugs.ruby-lang.org/issues/17881\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshioyama%2Fim","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fshioyama%2Fim","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshioyama%2Fim/lists"}