├── .github └── workflows │ └── test.yml ├── LICENSE ├── Makefile ├── README.md ├── loadkit-dev-1.rockspec ├── loadkit.lua ├── loadkit.moon └── spec ├── loadkit_spec.moon └── tests ├── file.cats └── thing.leafo /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: "spec" 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | luaVersion: ["5.1", "5.2", "5.3", "5.4", "luajit", "luajit-openresty"] 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@master 16 | 17 | - uses: leafo/gh-actions-lua@master 18 | with: 19 | luaVersion: ${{ matrix.luaVersion }} 20 | 21 | - uses: leafo/gh-actions-luarocks@master 22 | 23 | - name: build 24 | run: | 25 | luarocks install moonscript 26 | luarocks install busted 27 | luarocks make 28 | 29 | - name: test 30 | run: | 31 | busted -o utfTerminal 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Leaf Corcoran 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | busted 4 | 5 | build:: 6 | moonc loadkit.moon 7 | 8 | local: build 9 | luarocks make --local loadkit-dev-1.rockspec 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # loadkit 2 | 3 | ![spec](https://github.com/leafo/loadkit/workflows/spec/badge.svg) 4 | 5 | Loadkit allows you to load arbitrary files within the Lua package path. 6 | 7 | ## Install 8 | 9 | ```bash 10 | $ luarocks install loadkit 11 | ``` 12 | 13 | ## Example 14 | 15 | [`etlua`](http://github.com/leafo/etlua) is a library that lets you create 16 | embedded Lua templates. The result of the template is a function that when 17 | called returns the compiled template. Normally you need to load the template 18 | and compile the it manually. Let's make it so `require` is aware of `.elua` 19 | files and returns the compiled template. 20 | 21 | 22 | Here's an example directory structure: 23 | 24 | ``` 25 | |-- example.lua 26 | `-- templates 27 | |-- hello.elua 28 | `-- my_template.elua 29 | ``` 30 | 31 | And then we can run: 32 | 33 | ```lua 34 | -- example.lua 35 | local etlua = require "etlua" 36 | local loadkit = require "loadkit" 37 | 38 | -- register a handler for .elua files 39 | loadkit.register("elua", function(file) 40 | return assert(etlua.compile(file:read("*a"))) 41 | end) 42 | 43 | template = require "templates.my_template" 44 | print(template()) 45 | ``` 46 | 47 | The functionality of `require` is unchanged, if the template's module name is 48 | required again it would return the cached value, avoiding any additional 49 | searching. 50 | 51 | ### What's the point? 52 | 53 | A project like [MoonScript](http://moonscript.org) uses a technique like this 54 | to let you load compiled MoonScript as you would load Lua making the 55 | integration seamless. 56 | 57 | Alternatively, if you've ever wanted to bundle different kinds of file assets 58 | inside of a Lua module but were unsure about how to resolve the correct path to 59 | open the file you can use this module: 60 | 61 | ```lua 62 | local js_loader = loadkit.make_loader("js") 63 | 64 | -- find the actual path of your resource 65 | local fname = js_loader("mymodule.some_script") 66 | ``` 67 | 68 | ## Reference 69 | 70 | The module can be loaded by doing: 71 | 72 | ```lua 73 | local loadkit = require "loadkit" 74 | ``` 75 | 76 | ### Functions 77 | 78 | #### `loadkit.register(ext, handler)` 79 | 80 | Registers a new loader for the extension `ext`. The handler is a function that 81 | is responsible for creating the module after a matching file has been found. 82 | 83 | The handler takes three arguments: `file`, `module_name`, `file_path`. 84 | 85 | The `file` is a freshly opened Lua file object ready for reading. Loadkit will 86 | automatically close the file after executing the handler if you don't close it. 87 | 88 | `module_name` is the name of the module that was passed to require. `file_path` 89 | is the path of the file that was opened when searching through the search path. 90 | 91 | The return value of the handler determines if the module is loaded. If `nil` is 92 | returned no module is loaded. Any other value returned is used as the value of 93 | the module. 94 | 95 | #### `success = loadkit.unregister(ext)` 96 | 97 | Removes a handler that has already been registered. Returns `true` if found 98 | handler to remove. 99 | 100 | #### `bool = loadkit.is_registered(ext)` 101 | 102 | Returns `true` if a loader has already been registered for the extension `ext`. 103 | 104 | #### `loader = loadkit.make_loader(ext, [handler, package_path])` 105 | 106 | Makes a loader without manipulating Lua's module loaders. The return value is a 107 | function that takes a module name and returns the path of the file that matches 108 | that module name if it could be found. 109 | 110 | Handler is an optional function that works the same as in `register` from 111 | above. If a handler is specified then its return value is returned by the 112 | loader. 113 | 114 | `package_path` defaults to the Lua install's `package.path` variable. 115 | 116 | # Changelog 117 | 118 | **1.1.0 -- Sun Dec 6 17:50:53 PST 2015** 119 | 120 | * `make_loader` can operate on a custom package path 121 | * Support lua 5.2 and above (fix unpack reference) 122 | 123 | ## License 124 | 125 | MIT, Copyright (C) 2014 by Leaf Corcoran 126 | 127 | 128 | -------------------------------------------------------------------------------- /loadkit-dev-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "loadkit" 2 | version = "dev-1" 3 | 4 | source = { 5 | url = "git://github.com/leafo/loadkit.git" 6 | } 7 | 8 | description = { 9 | summary = "Loadkit allows you to load arbitrary files within the Lua package path", 10 | detailed = [[ 11 | Loadkit lets you register new file extension handlers that can be opened 12 | with require, or you can just search for files of any extension using the 13 | current search path. 14 | ]], 15 | homepage = "https://github.com/leafo/loadkit", 16 | maintainer = "Leaf Corcoran ", 17 | license = "MIT" 18 | } 19 | 20 | dependencies = { 21 | "lua >= 5.1", 22 | } 23 | 24 | build = { 25 | type = "builtin", 26 | modules = { 27 | ["loadkit"] = "loadkit.lua", 28 | }, 29 | } 30 | 31 | -------------------------------------------------------------------------------- /loadkit.lua: -------------------------------------------------------------------------------- 1 | local VERSION = "1.1.0" 2 | local insert, remove 3 | do 4 | local _obj_0 = table 5 | insert, remove = _obj_0.insert, _obj_0.remove 6 | end 7 | local unpack = table.unpack or _G.unpack 8 | local dirsep, pathsep, wildcard = unpack((function() 9 | local _accum_0 = { } 10 | local _len_0 = 1 11 | for c in package.config:gmatch("[^\n]") do 12 | _accum_0[_len_0] = c 13 | _len_0 = _len_0 + 1 14 | end 15 | return _accum_0 16 | end)()) 17 | local loaders 18 | loaders = function() 19 | return package.loaders or package.searchers 20 | end 21 | local escape_pattern 22 | do 23 | local punct = "[%^$()%.%[%]*+%-?%%]" 24 | escape_pattern = function(str) 25 | return (str:gsub(punct, function(p) 26 | return "%" .. p 27 | end)) 28 | end 29 | end 30 | wildcard = escape_pattern(wildcard) 31 | local modsep = escape_pattern(".") 32 | local make_loader 33 | make_loader = function(ext, handler, load_path) 34 | if load_path == nil then 35 | load_path = package.path 36 | end 37 | handler = handler or function(_, _, ...) 38 | return ... 39 | end 40 | local search_paths 41 | do 42 | local _accum_0 = { } 43 | local _len_0 = 1 44 | for path in load_path:gmatch("[^" .. tostring(pathsep) .. "]+") do 45 | local _continue_0 = false 46 | repeat 47 | do 48 | local p = path:match("^(.-)%.lua$") 49 | if p then 50 | _accum_0[_len_0] = p .. "." .. ext 51 | else 52 | _continue_0 = true 53 | break 54 | end 55 | end 56 | _len_0 = _len_0 + 1 57 | _continue_0 = true 58 | until true 59 | if not _continue_0 then 60 | break 61 | end 62 | end 63 | search_paths = _accum_0 64 | end 65 | return function(name) 66 | local name_path = name:gsub(modsep, dirsep) 67 | local file, file_path 68 | for _index_0 = 1, #search_paths do 69 | local search_path = search_paths[_index_0] 70 | file_path = search_path:gsub(wildcard, name_path) 71 | file = io.open(file_path) 72 | if file then 73 | break 74 | end 75 | end 76 | if file then 77 | local loaded = { 78 | handler(file, name, file_path) 79 | } 80 | if not (io.type(file) == "closed file") then 81 | file:close() 82 | end 83 | return unpack(loaded) 84 | end 85 | end 86 | end 87 | local registered_handlers = { } 88 | local register 89 | register = function(ext, handler, pos) 90 | if pos == nil then 91 | pos = 2 92 | end 93 | assert(ext, "missing extension") 94 | assert(handler, "missing handler") 95 | local real_ext = ext:match("^[^:]*") 96 | local loader_fn = make_loader(real_ext, handler) 97 | local wrapped_loader 98 | wrapped_loader = function(name) 99 | local res, err = loader_fn(name) 100 | if res ~= nil then 101 | return function() 102 | return res 103 | end 104 | else 105 | return err or "could not load `" .. tostring(real_ext) .. "` file" 106 | end 107 | end 108 | insert(loaders(), pos, wrapped_loader) 109 | registered_handlers[ext] = wrapped_loader 110 | return true 111 | end 112 | local unregister 113 | unregister = function(ext) 114 | local loader_fn = registered_handlers[ext] 115 | if not (loader_fn) then 116 | return nil, "can't find existing loader `" .. tostring(ext) .. "`" 117 | end 118 | for i, l in pairs(loaders()) do 119 | if l == loader_fn then 120 | remove(loaders(), i) 121 | return true 122 | end 123 | end 124 | return nil, "loader `" .. tostring(ext) .. "` is no longer in searchers" 125 | end 126 | local is_registered 127 | is_registered = function(ext) 128 | return not not registered_handlers[ext] 129 | end 130 | return { 131 | VERSION = VERSION, 132 | register = register, 133 | unregister = unregister, 134 | is_registered = is_registered, 135 | make_loader = make_loader, 136 | _registered_handlers = registered_handlers 137 | } 138 | -------------------------------------------------------------------------------- /loadkit.moon: -------------------------------------------------------------------------------- 1 | VERSION = "1.1.0" 2 | 3 | import insert, remove from table 4 | unpack = table.unpack or _G.unpack 5 | 6 | dirsep, pathsep, wildcard = unpack [ c for c in package.config\gmatch "[^\n]" ] 7 | loaders = -> package.loaders or package.searchers 8 | 9 | escape_pattern = do 10 | punct = "[%^$()%.%[%]*+%-?%%]" 11 | (str) -> (str\gsub punct, (p) -> "%"..p) 12 | 13 | wildcard = escape_pattern wildcard 14 | modsep = escape_pattern "." -- module name hierarchy separator 15 | 16 | make_loader = (ext, handler, load_path=package.path) -> 17 | handler or= (_, _, ...) -> ... 18 | 19 | search_paths = for path in load_path\gmatch "[^#{pathsep}]+" 20 | if p = path\match "^(.-)%.lua$" 21 | p .. "." .. ext 22 | else 23 | continue 24 | 25 | (name) -> 26 | name_path = name\gsub modsep, dirsep 27 | 28 | local file, file_path 29 | for search_path in *search_paths 30 | file_path = search_path\gsub wildcard, name_path 31 | file = io.open file_path 32 | break if file 33 | 34 | if file 35 | loaded = { handler file, name, file_path } 36 | 37 | -- close the file if it hasn't been closed 38 | unless io.type(file) == "closed file" 39 | file\close! 40 | 41 | unpack loaded 42 | 43 | registered_handlers = {} 44 | 45 | register = (ext, handler, pos=2) -> 46 | assert ext, "missing extension" 47 | assert handler, "missing handler" 48 | 49 | real_ext = ext\match "^[^:]*" 50 | 51 | loader_fn = make_loader real_ext, handler 52 | 53 | wrapped_loader = (name) -> 54 | res, err = loader_fn name 55 | 56 | if res != nil 57 | -> res 58 | else 59 | err or "could not load `#{real_ext}` file" 60 | 61 | insert loaders!, pos, wrapped_loader 62 | registered_handlers[ext] = wrapped_loader 63 | 64 | true 65 | 66 | unregister = (ext) -> 67 | loader_fn = registered_handlers[ext] 68 | 69 | unless loader_fn 70 | return nil, "can't find existing loader `#{ext}`" 71 | 72 | for i, l in pairs loaders! 73 | if l == loader_fn 74 | remove loaders!, i 75 | return true 76 | 77 | nil, "loader `#{ext}` is no longer in searchers" 78 | 79 | is_registered = (ext) -> 80 | not not registered_handlers[ext] 81 | 82 | { 83 | :VERSION 84 | :register, :unregister, :is_registered, :make_loader 85 | _registered_handlers: registered_handlers 86 | } 87 | -------------------------------------------------------------------------------- /spec/loadkit_spec.moon: -------------------------------------------------------------------------------- 1 | 2 | loadkit = require "loadkit" 3 | 4 | describe "loadkit", -> 5 | local old_loaders, old_loaded 6 | 7 | before_each -> 8 | old_loaders = { k,v for k,v in pairs package.loaders or package.searchers} 9 | old_loaded = { k,v for k,v in pairs package.loaded } 10 | 11 | for k in pairs loadkit._registered_handlers 12 | loadkit._registered_handlers[k] = nil 13 | 14 | after_each -> 15 | if package.loaders 16 | package.loaders = old_loaders 17 | else 18 | package.searchers = old_loaders 19 | 20 | for k in pairs package.loaded 21 | package.loaded[k] = nil 22 | 23 | for k,v in pairs old_loaded 24 | package.loaded[k] = v 25 | 26 | describe "register", -> 27 | it "should register basic handler", -> 28 | assert loadkit.register "leafo", -> "cool" 29 | assert.same "cool", require "spec.tests.thing" 30 | 31 | it "should register a tagged extension", -> 32 | assert loadkit.register "leafo:hello", -> "cool" 33 | assert.same "cool", require "spec.tests.thing" 34 | 35 | it "should load file as string", -> 36 | assert loadkit.register "leafo", (file) -> 37 | file\read "*a" 38 | 39 | assert.same "Hello world!\n", require "spec.tests.thing" 40 | 41 | it "should close the file", -> 42 | local f 43 | assert loadkit.register "leafo", (file) -> 44 | f = file 45 | file\read "*a" 46 | 47 | require "spec.tests.thing" 48 | assert.same "closed file", io.type(f) 49 | 50 | it "should not close file twice", -> 51 | assert loadkit.register "leafo", (file) -> 52 | out = file\read "*a" 53 | file\close! 54 | out 55 | 56 | require "spec.tests.thing" 57 | 58 | it "should pass expected arguments", -> 59 | assert loadkit.register "leafo", (file, mod_name, file_path) -> 60 | assert.same "file", io.type(file) 61 | assert.same "spec.tests.thing", mod_name 62 | assert.same "./spec/tests/thing.leafo", file_path 63 | "okay" 64 | 65 | require "spec.tests.thing" 66 | 67 | it "should allow loader function to return nil", -> 68 | fn = spy.new -> 69 | 70 | assert loadkit.register "leafo", -> 71 | fn! 72 | nil 73 | 74 | assert.has_error -> 75 | require "spec.tests.thing" 76 | 77 | assert.has_error -> 78 | require "spec.tests.thing" 79 | 80 | assert.spy(fn).was_called 2 81 | 82 | it "should register multiple loaders", -> 83 | assert loadkit.register "leafo", -> "leafo" 84 | assert loadkit.register "cats", -> "cats" 85 | 86 | assert.same "cats", require "spec.tests.file" 87 | assert.same "leafo", require "spec.tests.thing" 88 | 89 | -- tests before_each/after_each is cleaning correctly 90 | it "should not find file when nothing is registered", -> 91 | assert.has_error -> 92 | require "spec.tests.thing" 93 | 94 | describe "unregister", -> 95 | it "should respond to unregistered loader", -> 96 | status, err = loadkit.unregister "hello_world" 97 | assert.same nil, status 98 | assert.same "can't find existing loader `hello_world`", err 99 | 100 | it "should unregister basic handler", -> 101 | assert loadkit.register "leafo", -> "cool" 102 | assert loadkit.unregister "leafo" 103 | 104 | assert.has_error -> 105 | require "spec.tests.thing" 106 | 107 | it "should unregister tagged extension", -> 108 | assert loadkit.register "leafo", -> "cool" 109 | assert loadkit.register "leafo:tagged", -> "cool2" 110 | 111 | assert.same "cool2", require "spec.tests.thing" 112 | package.loaded["spec.tests.thing"] = nil 113 | 114 | assert loadkit.unregister "leafo:tagged" 115 | assert.same "cool", require "spec.tests.thing" 116 | 117 | describe "is_registered", -> 118 | it "should return true for registered ext", -> 119 | assert loadkit.register "leafo", -> "cool" 120 | assert.truthy loadkit.is_registered "leafo" 121 | 122 | it "should return false for registered ext", -> 123 | assert.falsy loadkit.is_registered "leafo" 124 | 125 | describe "make_loader", -> 126 | it "should find file", -> 127 | loader = loadkit.make_loader "cats" 128 | assert.same {}, {loader "spec.tests.thing"} 129 | assert.same { "./spec/tests/file.cats" }, { loader "spec.tests.file" } 130 | 131 | -------------------------------------------------------------------------------- /spec/tests/file.cats: -------------------------------------------------------------------------------- 1 | I love cats! 2 | -------------------------------------------------------------------------------- /spec/tests/thing.leafo: -------------------------------------------------------------------------------- 1 | Hello world! 2 | --------------------------------------------------------------------------------