howl.bundle

after_each ->
  _G.bundles = {}
  bundle.dirs = {}

with_bundle_dir = (name, f) ->
  with_tmpdir (dir) ->
    b_dir = dir / name
    b_dir\mkdir!
    status, err = pcall f, b_dir
    error(err) unless status
    mod_name = name\lower!\gsub '[%s%p]+', '_'
    pcall(bundle.unload, mod_name) if _G.bundles[mod_name]

bundle_init = (info = {}, spec = {}) ->
  ret = ''
  ret ..= "#{spec.code}\n" if spec.code
  mod = author: 'bundle_spec', description: 'spec_bundle', license: 'MIT'
  mod[k] = v for k,v in pairs info
  ret ..= 'return { info = {'
  ret ..= table.concat [k .. '="' .. v .. '"' for k,v in pairs mod], ','
  ret ..= '}, '

  if spec.other_returns
    ret ..= table.concat [k .. '="' .. tostring(v) .. '"' for k,v in pairs spec.other_returns], ','

  if spec.unload
    ret ..= "unload = #{spec.unload} }"
  else
    ret ..= 'unload = function() end }'


  ret

.unloaded holds the adjusted names of any unloaded bundles

with_tmpdir (dir) ->
  bundle.dirs = {dir}
  for name in *{'foo-bar', 'frob_nic'}
    b_dir = dir / name
    b_dir\mkdir!
    b_dir\join('init.lua').contents = bundle_init :name

  assert.same { 'foo_bar', 'frob_nic' }, bundle.unloaded
  bundle.load_by_name 'foo_bar'
  assert.same { 'frob_nic' }, bundle.unloaded
  bundle.unload 'foo_bar'
  assert.same { 'foo_bar', 'frob_nic' }, bundle.unloaded

load_from_dir(dir)

raises an error if dir is not a directory

assert.raises 'directory', -> bundle.load_from_dir File '/not-a-directory'

raises an error if the bundle init file is missing or incomplete

with_tmpdir (dir) ->
  assert.raises 'find file', -> bundle.load_from_dir dir
  init = dir / 'init.lua'
  init\touch!
  assert.raises 'Incorrect bundle', -> bundle.load_from_dir dir
  init.contents = 'return {}'
  assert.raises 'info missing', -> bundle.load_from_dir dir
  init.contents = 'return { info = {} }'
  assert.raises 'missing info field', -> bundle.load_from_dir dir

assigns the returned bundle table to bundles using the dir basename

mod = author: 'bundle_spec', description: 'spec_bundle', license: 'MIT'
with_bundle_dir 'foo', (dir) ->
  dir\join('init.lua').contents = bundle_init mod
  bundle.load_from_dir dir
  assert.same _G.bundles.foo.info, mod
  assert.is_equal 'function', type _G.bundles.foo.unload

massages the assigned module name to fit with naming standards if necessary

with_bundle_dir 'Test-hello 2', (dir) ->
  dir\join('init.lua').contents = bundle_init!
  bundle.load_from_dir dir
  assert.not_nil _G.bundles.test_hello_2

does nothing if the bundle is already loaded

with_bundle_dir 'two_times', (dir) ->
  dir\join('init.lua').contents = bundle_init!
  bundle.load_from_dir dir
  bundle.load_from_dir dir

raises an error upon implicit global writes

with_tmpdir (dir) ->
  dir\join('init.lua').contents = [[
    file = bundle_file('bundle_aux.lua')
    return {
      info = {
        author = 'spec',
        description = 'desc',
        license = 'MIT',
      },
      file = file
    }
  ]]
  assert.raises 'implicit global', -> bundle.load_from_dir dir

(exposed bundle helpers)

bundle_file provides access to bundle files

with_bundle_dir 'test', (dir) ->
  dir\join('init.lua').contents = [[
    local file = bundle_file('bundle_aux.lua')
    return {
      info = {
        author = 'spec',
        description = 'desc',
        license = 'MIT',
      },
      unload = function() end,
      file = file
    }
  ]]
  bundle.load_from_dir dir
  assert.equal _G.bundles.test.file, dir / 'bundle_aux.lua'

provide_module(name, prefix = nil)

makes a sub directory available for loading globally with require

with_bundle_dir 'test', (dir) ->
  mod = dir\join('testmod')
  mod\mkdir_p!
  mod\join('init.moon').contents = '{root: true}'
  mod\join('other.moon').contents = '{other: true}'
  dir\join('init.lua').contents = bundle_init nil, {
    code: 'provide_module("testmod")'
  }
  bundle.load_from_dir dir
  assert.same {root: true}, require 'testmod'
  assert.same {other: true}, require 'testmod.other'

require_bundle

ensures the required bundle is loaded before the dependent one

with_tmpdir (dir) ->
  bundle.dirs = {dir}
  first_dir = dir\join('first')
  first_dir\mkdir!
  second_dir = dir\join('second')
  second_dir\mkdir!
  third_dir = dir\join('third')
  third_dir\mkdir!

  first_dir\join('init.lua').contents = bundle_init!
  third_dir\join('init.lua').contents = bundle_init!

  second_dir\join('init.lua').contents = bundle_init nil, {
    code: 'require_bundle("third")\nrequire_bundle("first")'
  }

  bundle.load_from_dir second_dir
  assert.is_not_nil _G.bundles.first
  assert.is_not_nil _G.bundles.third

detects cyclic dependencies

with_tmpdir (dir) ->
  bundle.dirs = {dir}
  first_dir = dir\join('first')
  first_dir\mkdir!
  second_dir = dir\join('second')
  second_dir\mkdir!

  first_dir\join('init.lua').contents = bundle_init nil, {
    code: 'require_bundle("second")'
  }
  second_dir\join('init.lua').contents = bundle_init nil, {
    code: 'require_bundle("first")'
  }

  assert.raises 'Cyclic dependency', ->
    bundle.load_from_dir second_dir

load_all()

loads all found bundles in all directories in bundle.dirs

with_tmpdir (dir) ->
  bundle.dirs = {dir}
  for name in *{'foo', 'bar'}
    b_dir = dir / name
    b_dir\mkdir!
    b_dir\join('init.lua').contents = bundle_init :name

  bundle.load_all!
  assert.not_nil _G.bundles.foo
  assert.not_nil _G.bundles.bar

skips any hidden entries

with_tmpdir (dir) ->
  bundle.dirs = {dir}
  b_dir = dir / '.hidden'
  b_dir\mkdir!
  b_dir\join('init.lua').contents = bundle_init name: 'hidden'

  bundle.load_all!
  assert.same [name for name, _ in pairs _G.bundles], {}

raises an error if bundle names conflict

with_tmpdir (dir) ->
  for name in *{'foo', 'bar'}
    b_dir = dir / name / 'my_bundle'
    b_dir\mkdir_p!
    b_dir\join('init.lua').contents = bundle_init :name
  bundle.dirs = {dir\join('foo'), dir\join('bar')}

  assert.raises 'conflict', -> bundle.load_all!

load_by_name(name)

loads the bundle with the specified name, if not already loaded

with_tmpdir (dir) ->
  bundle.dirs = {dir}
  b_dir = dir / 'named'
  b_dir\mkdir!
  b_dir\join('init.lua').contents = bundle_init name: 'named'

  bundle.load_by_name 'named'
  assert.not_nil _G.bundles.named

  bundle.load_by_name 'named'

raises an error if the bundle could not be found

assert.raises 'not found', -> bundle.load_by_name 'oh_bundle_where_art_thouh'

unload(name)

raises an error if no bundle with the given name exists

assert.raises 'not found', -> bundle.unload 'serenity'

(for an existing bundle)

mod = name: 'bunny'

calls the bundle unload function and removes the bundle from _G.bundles

with_bundle_dir 'bunny', (dir) ->
  dir\join('init.lua').contents = bundle_init mod, unload: 'function() _G.bunny_bundle_unload = true end'
  bundle.load_from_dir dir
  bundle.unload 'bunny'
  assert.is_true _G.bunny_bundle_unload
  assert.is_nil _G.bundles.bunny

returns early with an error if the unload function raises an error

with_bundle_dir 'bad_seed', (dir) ->
  dir\join('init.lua').contents = bundle_init mod, unload: 'function() error("barf!") end'
  bundle.load_from_dir dir
  assert.raises 'barf!', -> bundle.unload 'bad_seed'
  assert.is_not_nil _G.bundles.bad_seed

transforms the given name to match the module name

with_bundle_dir 'dash-love', (dir) ->
  dir\join('init.lua').contents = bundle_init name: 'dash-love'
  bundle.load_from_dir dir
  assert.no_error -> bundle.unload 'dash-love'

removes any references to provided modules

with_bundle_dir 'test', (dir) ->
  mod = dir\join('testmod')
  mod\mkdir_p!
  mod\join('init.moon').contents = '{root: true}'
  mod\join('other.moon').contents = '{other: true}'
  dir\join('init.lua').contents = bundle_init nil, {
    code: 'provide_module("testmod")'
  }
  bundle.load_from_dir dir
  assert.same {root: true}, require 'testmod'
  assert.same {other: true}, require 'testmod.other'
  bundle.unload 'test'
  assert.is_false pcall require, 'testmod'
  assert.is_false pcall require, 'testmod.other'

from_file(file)

returns the adjusted name of the containing bundle if any

with_tmpdir (dir) ->
  bundle.dirs = {dir}
  b_dir = dir / 'my-bundle'
  init = b_dir\join('init.lua')
  assert.equal 'my_bundle', bundle.from_file(init)
  assert.is_nil bundle.from_file(File('/bin/ls'))
  assert.is_nil bundle.from_file(dir\join('directly_under_root'))