syntropy 0.12 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 18d6c37aa12dcf2315d95bc656d1cbf196ada1dd0e2309b0f1bf1ca8b65a1cbb
4
- data.tar.gz: 04f12a324d704814f45d9218e6e02d0d38469dbed753dc8202009568f447d400
3
+ metadata.gz: dad58d19dc755d1a8ea6adb32cd7146b2228b5ace655a01b2e7932fd8abb7d23
4
+ data.tar.gz: 228678e0a76e67b012e52c89f5ed90c3d08e884a6424c0c51fcf9f840b2e6876
5
5
  SHA512:
6
- metadata.gz: c62d9b26c9754c23bfc53dd25c1d698733e1d01b703d580db17cfeed1505c36f3f382fcd762ef0e91e641505cda26af03a328e549a840f9f7af4b2ab7c582bc0
7
- data.tar.gz: 0577b8d2fda40cb40c81419c31c7f515230f7948ab49158a7ca2d647fe71a323d51b176003d976b1e5428fe4ead6415064321340c0e20b5c84806d1e52266ce0
6
+ metadata.gz: 5ea7c453ae3e32d17f4bc35204fcf4c29f4eae7d95e9232f56b6ff65dcfaab9f3f6e41fb5562817ab3d3bd10a3ee2e5c92c440045bbdc8a51bc88d155fac7ab4
7
+ data.tar.gz: fb28a9011ea58505b9923589b62f0010eb97dea9f1df81e4c0483682669715c455fff5a150888f06f117e127497a98e7c93535f683d924e86ffcfeb007cf9fc5
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ # 0.13 2025-08-28
2
+
3
+ - Reimplement module loading
4
+ - Refactor RoutingTree
5
+
1
6
  # 0.12 2025-08-28
2
7
 
3
8
  - Add routing info to request: `#route`, `#route_params`
data/TODO.md CHANGED
@@ -1,3 +1,8 @@
1
+ - [ ] Improve serving of static files:
2
+ - [ ] support for compression
3
+ - [ ] support for caching headers
4
+ - [ ] add `Request#render_static_file(route, fn)
5
+
1
6
  ## Support for applets
2
7
 
3
8
  - can be implemented as separate gems
data/lib/syntropy/app.rb CHANGED
@@ -23,10 +23,10 @@ module Syntropy
23
23
 
24
24
  # for apps with a _site.rb file
25
25
  def site_file_app(opts)
26
- site_fn = File.join(opts[:root_dir], '_site.rb')
27
- return nil if !File.file?(site_fn)
26
+ fn = File.join(opts[:root_dir], '_site.rb')
27
+ return nil if !File.file?(fn)
28
28
 
29
- loader = Syntropy::ModuleLoader.new(opts[:root_dir], opts)
29
+ loader = Syntropy::ModuleLoader.new(opts)
30
30
  loader.load('_site')
31
31
  end
32
32
 
@@ -37,13 +37,14 @@ module Syntropy
37
37
  end
38
38
 
39
39
  attr_reader :module_loader, :routing_tree, :root_dir, :mount_path, :opts
40
- def initialize(root_dir:, mount_path:, **opts)
40
+
41
+ def initialize(**opts)
41
42
  @machine = opts[:machine]
42
- @root_dir = root_dir
43
- @mount_path = mount_path
43
+ @root_dir = opts[:root_dir]
44
+ @mount_path = opts[:mount_path]
44
45
  @opts = opts
45
46
 
46
- @module_loader = Syntropy::ModuleLoader.new(@root_dir, opts)
47
+ @module_loader = Syntropy::ModuleLoader.new(app: self, **opts)
47
48
  setup_routing_tree
48
49
  start_app
49
50
  end
@@ -69,6 +70,8 @@ module Syntropy
69
70
  proc = route[:proc] ||= compute_route_proc(route)
70
71
  proc.(req)
71
72
  rescue StandardError => e
73
+ # p e
74
+ # p e.backtrace
72
75
  error_handler = get_error_handler(route)
73
76
  error_handler.(req, e)
74
77
  end
@@ -4,8 +4,8 @@ require 'p2'
4
4
 
5
5
  module Syntropy
6
6
  class ModuleLoader
7
- def initialize(root, env)
8
- @root = root
7
+ def initialize(env)
8
+ @root_dir = env[:root_dir]
9
9
  @env = env
10
10
  @loaded = {} # maps ref to code
11
11
  @fn_map = {} # maps filename to ref
@@ -26,27 +26,20 @@ module Syntropy
26
26
  private
27
27
 
28
28
  def load_module(ref)
29
- fn = File.expand_path(File.join(@root, "#{ref}.rb"))
29
+ fn = File.expand_path(File.join(@root_dir, "#{ref}.rb"))
30
30
  @fn_map[fn] = ref
31
- raise "File not found #{fn}" if !File.file?(fn)
32
-
33
- mod_body = IO.read(fn)
34
- mod_ctx = Class.new(Syntropy::Module)
35
- mod_ctx.prepare(loader: self, env: @env, ref: ref)
36
- mod_ctx.module_eval(mod_body, fn, 1)
31
+ raise Syntropy::Error, "File not found #{fn}" if !File.file?(fn)
37
32
 
38
- export_value = mod_ctx.__export_value__
39
- wrap_module(mod_ctx, export_value)
33
+ code = IO.read(fn)
34
+ env = @env.merge(module_loader: self, ref: ref)
35
+ export_value = Syntropy::Module.load(env, code)
36
+ transform_module_export_value(export_value)
40
37
  end
41
38
 
42
- def wrap_module(mod_ctx, export_value)
39
+ def transform_module_export_value(export_value)
43
40
  case export_value
44
41
  when nil
45
- raise 'No export found'
46
- when Symbol
47
- o = mod_ctx.new(@env)
48
- # TODO: verify export_value denotes a valid method
49
- ->(req) { o.send(export_value, req) }
42
+ raise Syntropy::Error, 'No export found'
50
43
  when String
51
44
  ->(req) { req.respond(export_value) }
52
45
  when Class
@@ -58,74 +51,44 @@ module Syntropy
58
51
  end
59
52
 
60
53
  class Module
61
- def initialize(env)
62
- @env = env
54
+ def self.load(env, code)
55
+ m = new(**env)
56
+ m.instance_eval(code)
57
+ m.__export_value__
63
58
  end
64
59
 
65
- class << self
66
- def prepare(loader:, env:, ref:)
67
- @loader = loader
68
- @env = env
69
- @machine = env[:machine]
70
- @ref = ref
71
- const_set(:MODULE, self)
72
- end
73
-
74
- attr_reader :__export_value__
75
-
76
- def import(ref)
77
- @loader.load(ref)
78
- end
79
-
80
- def export(ref)
81
- @__export_value__ = ref
82
- end
60
+ attr_reader
61
+ def initialize(**env)
62
+ @env = env
63
+ @machine = env[:machine]
64
+ @module_loader = env[:module_loader]
65
+ @app = env[:app]
66
+ @ref = env[:ref]
67
+ singleton_class.const_set(:MODULE, self)
68
+ end
69
+
70
+ attr_reader :__export_value__
71
+ def export(v)
72
+ @__export_value__ = v
73
+ end
83
74
 
84
- def template(proc = nil, &block)
85
- proc ||= block
86
- raise "No template block/proc given" if !proc
75
+ def import(ref)
76
+ @module_loader.load(ref)
77
+ end
87
78
 
88
- P2::Template.new(proc)
89
- end
90
- alias_method :html, :template
91
-
92
- def route_by_host(map = nil)
93
- root = @env[:root_dir]
94
- sites = Dir[File.join(root, '*')]
95
- .reject { File.basename(it) =~ /^_/ }
96
- .select { File.directory?(it) }
97
- .each_with_object({}) { |fn, h|
98
- name = File.basename(fn)
99
- opts = @env.merge(root_dir: fn)
100
- h[name] = Syntropy::App.new(**opts)
101
- }
102
-
103
- map&.each do |k, v|
104
- sites[k] = sites[v]
105
- end
106
-
107
- lambda { |req|
108
- site = sites[req.host]
109
- site ? site.call(req) : req.respond(nil, ':status' => Status::BAD_REQUEST)
110
- }
111
- end
79
+ def template(proc = nil, &block)
80
+ proc ||= block
81
+ raise "No template block/proc given" if !proc
112
82
 
113
- def page_list(ref)
114
- full_path = File.join(@env[:root_dir], ref)
115
- raise 'Not a directory' if !File.directory?(full_path)
83
+ P2::Template.new(proc)
84
+ end
116
85
 
117
- Dir[File.join(full_path, '*.md')].sort.map {
118
- atts, markdown = Syntropy.parse_markdown_file(it, @env)
119
- { atts:, markdown: }
120
- }
121
- end
86
+ def page_list(ref)
87
+ Syntropy.page_list(@env, ref)
88
+ end
122
89
 
123
- def app(root_dir = nil, mount_path = nil)
124
- root_dir ||= @env[:root_dir]
125
- mount_path ||= @env[:mount_path]
126
- opts = @env.merge(root_dir:, mount_path:)
127
- Syntropy::App.new(**opts)
128
- end
90
+ def app(**env)
91
+ Syntropy::App.new(**(@env.merge(env)))
129
92
  end
130
93
  end
131
94
  end
@@ -149,8 +149,13 @@ module Syntropy
149
149
  #
150
150
  # The routing tree is complemented with two maps:
151
151
  #
152
- # - `static_map` - maps URL paths to the corresponding static route entries.
153
- # - `dynamic_map` - maps URL paths to the corresponding dynamic route entries.
152
+ # - `static_map` - maps URL paths to the corresponding static route entries,
153
+ # which includes all non-parametric routes, as well as all static files.
154
+ # - `dynamic_map` - maps URL paths to the corresponding parametric route
155
+ # entries.
156
+ #
157
+ # The reason we use two separate maps is to prevent accidentally hitting a
158
+ # false lookup for a a URL with segments containing square brackets!
154
159
  #
155
160
  # @return [Hash] root entry
156
161
  def compute_tree
@@ -165,9 +170,9 @@ module Syntropy
165
170
  def compute_route_directory(dir:, rel_path:, parent: nil)
166
171
  param = (m = File.basename(dir).match(/^\[(.+)\]$/)) ? m[1] : nil
167
172
  entry = {
168
- parent: parent,
173
+ parent:,
169
174
  path: rel_path_to_abs_path(rel_path),
170
- param: param,
175
+ param:,
171
176
  hook: find_aux_module_entry(dir, '_hook.rb'),
172
177
  error: find_aux_module_entry(dir, '_error.rb')
173
178
  }
@@ -185,7 +190,7 @@ module Syntropy
185
190
  # @return [String, nil] file path if found
186
191
  def find_aux_module_entry(dir, name)
187
192
  fn = File.join(dir, name)
188
- File.file?(fn) ? ({ kind: :module, fn: fn }) : nil
193
+ File.file?(fn) ? ({ kind: :module, fn: }) : nil
189
194
  end
190
195
 
191
196
  # Returns a hash mapping file/dir names to route entries.
@@ -223,76 +228,51 @@ module Syntropy
223
228
  # @return [void]
224
229
  def compute_route_file(fn:, rel_path:, parent:)
225
230
  abs_path = rel_path_to_abs_path(rel_path)
226
- case
231
+
227
232
  # index.rb, index+.rb, index.md
233
+ case
228
234
  when (m = fn.match(/\/index(\+)?(\.(?:rb|md))$/))
229
- # Index files (modules or markdown) files) are applied as targets to the
230
- # immediate containing directory. A + suffix indicates this route
231
- # handles requests to its subtree
232
- plus, ext = m[1..2]
233
- kind = FILE_TYPE[ext]
234
- handle_subtree = (plus == '+') && (kind == :module)
235
- set_index_route_target(parent:, path: abs_path, kind:, fn:, handle_subtree:)
236
-
235
+ make_index_module_route(m:, parent:, path: abs_path, fn:)
236
+
237
237
  # index.html
238
238
  when fn.match(/\/index\.html$/)
239
- # HTML index files are applied as targets to the immediate containing
240
- # directory. It is considered static and therefore not added to the
241
- # routing tree.
242
239
  set_index_route_target(parent:, path: abs_path, kind: :static, fn:)
243
240
 
244
241
  # foo.rb, foo+.rb, foo.md, [foo].rb, [foo]+.rb
245
242
  when (m = fn.match(/\/(\[)?([^\]\/\+]+)(\])?(\+)?(\.(?:rb|md))$/))
246
- # Module and markdown route targets. A + suffix indicates the module
247
- # also handles requests to the subtree. For example, `/foo/bar.rb` will
248
- # handle requests to `/foo/bar`, but `/foo/bar+.rb` will also handle
249
- # requests to `/foo/bar/baz/bug`.
250
- #
251
- # parametric, or wildcard, routes convert segments of the URL path into
252
- # parameters that are added to the HTTP request. Parametric routes are
253
- # denoted using square brackets around the file/directory name. For
254
- # example, `/api/posts/[id].rb`` will handle requests to
255
- # `/api/posts/42`, and will extract the parameter `posts => 42` to add
256
- # to the incoming request.
257
- #
258
- # A + suffix indicates the module also handles the subtree, so e.g.
259
- # `/api/posts/[id]+.rb` will also handle requests to
260
- # `/api/posts/42/fans` etc.
261
- ob, param, cb, plus, ext = m[1..5]
262
- kind = FILE_TYPE[ext]
263
- make_route_entry(
264
- parent:,
265
- path: abs_path,
266
- param: ob && cb ? param : nil,
267
- target: make_route_target(kind:, fn:),
268
- handle_subtree: (plus == '+') && (kind == :module)
269
- )
270
-
271
- # foo.html
272
- when (m = fn.match(/\/[^\/]+\.html$/))
273
- # HTML files are routed by their clean URL, i.e. without the `.html`
274
- # extension. Those are considered as static routes, and do not have
275
- # entries in the routing tree. Instead they are stored in the static
276
- # map.
277
- make_route_entry(
278
- parent: parent,
279
- path: abs_path,
280
- target: make_route_target(kind: :static, fn:)
281
- )
243
+ make_module_route(m:, parent:, path: abs_path, fn:)
282
244
 
283
245
  # everything else
284
246
  else
285
247
  # static files resolved using the static map, and are not added to the
286
- # routing tree, which is used for resolving dynamic routes.
287
- make_route_entry(
288
- parent: parent,
289
- path: abs_path,
290
- target: { kind: :static, fn: fn }
291
- )
248
+ # routing tree, which is used for resolving dynamic routes. HTML files
249
+ # are routed by their clean URL, i.e. without the `.html` extension.
250
+ target = { kind: :static, fn: }
251
+ make_route_entry(parent:, path: abs_path, target:)
292
252
  end
293
253
  end
294
254
 
295
- # Sets an index route target for the given parent entry.
255
+ # Creates a route entry for an index module (ruby/markdown). Index files
256
+ # (modules or markdown) files) are applied as targets to the immediate
257
+ # containing directory. A + suffix indicates this route handles requests to
258
+ # its subtree
259
+ #
260
+ # @param m [MatchData] path match data
261
+ # @param parent [Hash] parent route entry
262
+ # @param path [String] route path
263
+ # @param fn [String] route target filename
264
+ # @return [nil] (prevents addition of an index route)
265
+ def make_index_module_route(m:, parent:, path:, fn:)
266
+ plus, ext = m[1..2]
267
+ kind = FILE_TYPE[ext]
268
+ handle_subtree = (plus == '+') && (kind == :module)
269
+ set_index_route_target(parent:, path:, kind:, fn:, handle_subtree:)
270
+ end
271
+
272
+
273
+ # Sets an index route target for the given parent entry. Index files are
274
+ # applied as targets to the immediate containing directory. HTML index files
275
+ # are considered static and therefore not added to the routing tree.
296
276
  #
297
277
  # @param parent [Hash] parent route entry
298
278
  # @param path [String] route path
@@ -303,12 +283,12 @@ module Syntropy
303
283
  def set_index_route_target(parent:, path:, kind:, fn:, handle_subtree: nil)
304
284
  if is_parametric_route?(parent) || handle_subtree
305
285
  @dynamic_map[path] = parent
306
- parent[:target] = make_route_target(kind:, fn:)
286
+ parent[:target] = { kind:, fn: }
307
287
  parent[:handle_subtree] = handle_subtree
308
288
  else
309
289
  @static_map[path] = {
310
290
  parent: parent[:parent],
311
- path: path,
291
+ path:,
312
292
  target: { kind:, fn: },
313
293
  # In case we're at the tree root, we need to copy over the hook and
314
294
  # error refs.
@@ -319,6 +299,36 @@ module Syntropy
319
299
  nil
320
300
  end
321
301
 
302
+ # Creates a route entry for normal module and markdown files. A + suffix
303
+ # indicates the module also handles requests to the subtree. For example,
304
+ # `/foo/bar.rb` will handle requests to `/foo/bar`, but `/foo/bar+.rb` will
305
+ # also handle requests to `/foo/bar/baz/bug`.
306
+ #
307
+ # parametric, or wildcard, routes convert segments of the URL path into
308
+ # parameters that are added to the HTTP request. Parametric routes are
309
+ # denoted using square brackets around the file/directory name. For example,
310
+ # `/api/posts/[id].rb`` will handle requests to `/api/posts/42`, and will
311
+ # extract the parameter `posts => 42` to add to the incoming request.
312
+ #
313
+ # A + suffix indicates the module also handles the subtree, so e.g.
314
+ # `/api/posts/[id]+.rb` will also handle requests to `/api/posts/42/fans`
315
+ # etc.
316
+ #
317
+ # @param m [MatchData] path match data
318
+ # @param parent [Hash] parent route entry
319
+ # @param path [String] route path
320
+ # @param fn [String] route target filename
321
+ # @return [Hash] route entry
322
+ def make_module_route(m:, parent:, path:, fn:)
323
+ ob, param, cb, plus, ext = m[1..5]
324
+ kind = FILE_TYPE[ext]
325
+ make_route_entry(
326
+ parent:, path:, param: ob && cb ? param : nil,
327
+ target: { kind:, fn: },
328
+ handle_subtree: (plus == '+') && (kind == :module)
329
+ )
330
+ end
331
+
322
332
  # Creates a new route entry, registering it in the static or dynamic map,
323
333
  # according to its type.
324
334
  #
@@ -357,19 +367,6 @@ module Syntropy
357
367
  entry[:param] ? '[]' : File.basename(entry[:path]).gsub(/\+$/, '')
358
368
  end
359
369
 
360
- # Returns a hash representing a route target for the given route target kind
361
- # and file name.
362
- #
363
- # @param kind [Symbol] route target kind
364
- # @param fn [String] filename
365
- # @return [Hash] route target hash
366
- def make_route_target(kind:, fn:)
367
- {
368
- kind: kind,
369
- fn: fn
370
- }
371
- end
372
-
373
370
  # Generates and returns a router proc based on the routing tree.
374
371
  #
375
372
  # @return [Proc] router proc
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Syntropy
4
+ # Utilities for use in modules
5
+ module Utilities
6
+ # Returns a request handler that routes request according to
7
+ def route_by_host(env, map = nil)
8
+ root = env[:root_dir]
9
+ sites = Dir[File.join(root, '*')]
10
+ .reject { File.basename(it) =~ /^_/ }
11
+ .select { File.directory?(it) }
12
+ .each_with_object({}) { |fn, h|
13
+ name = File.basename(fn)
14
+ h[name] = Syntropy::App.new(**env.merge(root_dir: fn))
15
+ }
16
+
17
+ # copy over map refs
18
+ map&.each { |k, v| sites[k] = sites[v] }
19
+
20
+ #
21
+ lambda { |req|
22
+ site = sites[req.host]
23
+ site ? site.call(req) : req.respond(nil, ':status' => Status::BAD_REQUEST)
24
+ }
25
+ end
26
+
27
+ def page_list(env, ref)
28
+ full_path = File.join(env[:root_dir], ref)
29
+ raise 'Not a directory' if !File.directory?(full_path)
30
+
31
+ Dir[File.join(full_path, '*.md')].sort.map {
32
+ atts, markdown = Syntropy.parse_markdown_file(it, env)
33
+ { atts:, markdown: }
34
+ }
35
+ end
36
+
37
+ def app(**env)
38
+ Syntropy::App.new(**env)
39
+ end
40
+ end
41
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
- VERSION = '0.12'
4
+ VERSION = '0.13.1'
5
5
  end
data/lib/syntropy.rb CHANGED
@@ -15,10 +15,13 @@ require 'syntropy/request_extensions'
15
15
  require 'syntropy/routing_tree'
16
16
  require 'syntropy/rpc_api'
17
17
  require 'syntropy/side_run'
18
+ require 'syntropy/utils'
18
19
 
19
20
  module Syntropy
20
21
  Status = Qeweney::Status
21
22
 
23
+ extend Utilities
24
+
22
25
  class << self
23
26
  attr_accessor :machine
24
27
 
@@ -10,4 +10,4 @@ def bar
10
10
  @env[:baz]
11
11
  end
12
12
 
13
- export :call
13
+ export self
@@ -0,0 +1,21 @@
1
+ def env
2
+ @env
3
+ end
4
+
5
+ def machine
6
+ @machine
7
+ end
8
+
9
+ def module_loader
10
+ @module_loader
11
+ end
12
+
13
+ def app
14
+ @app
15
+ end
16
+
17
+ def module_const
18
+ MODULE
19
+ end
20
+
21
+ export MODULE
data/test/app/tmp.rb CHANGED
@@ -3,4 +3,4 @@
3
3
  def call(req)
4
4
  req.respond('foo')
5
5
  end
6
- export :call
6
+ export self
@@ -1 +1 @@
1
- export route_by_host
1
+ export Syntropy.route_by_host(@env)
data/test/test_module.rb CHANGED
@@ -6,8 +6,8 @@ class ModuleTest < Minitest::Test
6
6
  def setup
7
7
  @machine = UM.new
8
8
  @root = File.join(__dir__, 'app')
9
- @env = { baz: 42 }
10
- @loader = Syntropy::ModuleLoader.new(@root, @env)
9
+ @env = { root_dir: @root, baz: 42, machine: @machine, app: 42 }
10
+ @loader = Syntropy::ModuleLoader.new(@env)
11
11
  end
12
12
 
13
13
  def test_module_loading
@@ -15,11 +15,11 @@ class ModuleTest < Minitest::Test
15
15
  assert_equal :bar, mod.foo
16
16
  assert_equal 42, mod.bar
17
17
 
18
- assert_raises(RuntimeError) { @loader.load('_lib/foo') }
19
- assert_raises(RuntimeError) { @loader.load('_lib/missing-export') }
18
+ assert_raises(Syntropy::Error) { @loader.load('_lib/missing-module') }
19
+ assert_raises(Syntropy::Error) { @loader.load('_lib/missing-export') }
20
20
 
21
21
  mod = @loader.load('_lib/callable')
22
- assert_kind_of Proc, mod
22
+ assert_kind_of Syntropy::Module, mod
23
23
  assert_equal 'barbarbar', mod.call(3)
24
24
  assert_raises(NoMethodError) { mod.foo(2) }
25
25
 
@@ -34,4 +34,14 @@ class ModuleTest < Minitest::Test
34
34
  assert_kind_of Syntropy::Module, mod
35
35
  assert_equal :bar, mod.foo
36
36
  end
37
+
38
+ def test_module_env
39
+ mod = @loader.load('_lib/env')
40
+
41
+ assert_equal mod, mod.module_const
42
+ assert_equal @env.merge(module_loader: @loader, ref: '_lib/env'), mod.env
43
+ assert_equal @machine, mod.machine
44
+ assert_equal @loader, mod.module_loader
45
+ assert_equal 42, mod.app
46
+ end
37
47
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: syntropy
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.12'
4
+ version: 0.13.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
@@ -215,11 +215,13 @@ files:
215
215
  - lib/syntropy/routing_tree.rb
216
216
  - lib/syntropy/rpc_api.rb
217
217
  - lib/syntropy/side_run.rb
218
+ - lib/syntropy/utils.rb
218
219
  - lib/syntropy/version.rb
219
220
  - syntropy.gemspec
220
221
  - test/app/_hook.rb
221
222
  - test/app/_layout/default.rb
222
223
  - test/app/_lib/callable.rb
224
+ - test/app/_lib/env.rb
223
225
  - test/app/_lib/klass.rb
224
226
  - test/app/_lib/missing-export.rb
225
227
  - test/app/_lib/self.rb