impression 0.1 → 0.5

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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/test.yml +32 -0
  4. data/CHANGELOG.md +23 -0
  5. data/Gemfile +5 -0
  6. data/Gemfile.lock +63 -32
  7. data/README.md +44 -2
  8. data/Rakefile +10 -0
  9. data/examples/markdown/_assets/style.css +98 -0
  10. data/examples/markdown/app.rb +11 -5
  11. data/examples/markdown/docs/tutorial.md +15 -0
  12. data/impression.gemspec +11 -7
  13. data/lib/impression/app.rb +9 -0
  14. data/lib/impression/file_tree.rb +124 -0
  15. data/lib/impression/file_watcher.rb +50 -0
  16. data/lib/impression/jamstack.rb +179 -0
  17. data/lib/impression/request_extensions/responses.rb +48 -0
  18. data/lib/impression/request_extensions/routing.rb +52 -0
  19. data/lib/impression/request_extensions.rb +8 -2
  20. data/lib/impression/resource.rb +168 -0
  21. data/lib/impression/version.rb +1 -1
  22. data/lib/impression.rb +8 -0
  23. data/test/helper.rb +94 -0
  24. data/test/jamstack/_layouts/article.rb +9 -0
  25. data/test/jamstack/_layouts/default.rb +12 -0
  26. data/test/jamstack/articles/2008-06-14-manu.md +6 -0
  27. data/test/jamstack/articles/2009-06-12-noatche.md +6 -0
  28. data/test/jamstack/articles/a.md +6 -0
  29. data/test/jamstack/assets/js/a.js +1 -0
  30. data/test/jamstack/bar.html +1 -0
  31. data/test/jamstack/baz/index.md +7 -0
  32. data/test/jamstack/foo.rb +7 -0
  33. data/test/jamstack/foobar.rb +10 -0
  34. data/test/jamstack/index.md +4 -0
  35. data/test/run.rb +5 -0
  36. data/test/static/bar/index.html +1 -0
  37. data/test/static/foo.html +1 -0
  38. data/test/static/index.html +1 -0
  39. data/test/static/js/a.js +1 -0
  40. data/test/test_app.rb +52 -0
  41. data/test/test_file_tree.rb +116 -0
  42. data/test/test_file_watcher.rb +57 -0
  43. data/test/test_jamstack.rb +263 -0
  44. data/test/test_resource.rb +201 -0
  45. metadata +57 -35
  46. data/lib/impression/pages.rb +0 -120
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'yaml'
5
+ require 'date'
6
+ require 'modulation'
7
+ require 'papercraft'
8
+
9
+ require_relative './resource'
10
+ require_relative './file_tree'
11
+
12
+ module Impression
13
+
14
+ # `Jamstack` implements a resource that maps to a Jamstack app directory.
15
+ class Jamstack < FileTree
16
+ def initialize(**props)
17
+ super
18
+ @layouts = {}
19
+ end
20
+
21
+ # Returns a list of pages found in the given directory (relative to the base
22
+ # directory). Each entry containins the absolute file path, the pretty URL,
23
+ # the possible date parsed from the file name, and any other front matter
24
+ # attributes (for .md files). This method will detect only pages with the
25
+ # extensions .html, .md, .rb. The returned entries are sorted by file path.
26
+ #
27
+ # @param dir [String] relative directory
28
+ # @return [Array<Hash>] array of page entries
29
+ def page_list(dir)
30
+ base = File.join(@directory, dir)
31
+ Dir.glob('*.{html,md}', base: base)
32
+ .map { |fn| page_entry(fn, dir) }
33
+ .sort_by { |i| i[:path] }
34
+ end
35
+
36
+ private
37
+
38
+ DATE_REGEXP = /^(\d{4}\-\d{2}\-\d{2})/.freeze
39
+ MARKDOWN_PAGE_REGEXP = /\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)/m.freeze
40
+ MD_EXT_REGEXP = /\.md$/.freeze
41
+ PAGE_EXT_REGEXP = /^(.+)\.(md|html|rb)$/.freeze
42
+ INDEX_PAGE_REGEXP = /^(.+)\/index$/.freeze
43
+
44
+ # Returns a page entry for the given file.
45
+ #
46
+ # @param fn [String] file name
47
+ # @param dir [String] relative directory
48
+ # @return [Hash] page entry
49
+ def page_entry(fn, dir)
50
+ relative_path = File.join(dir, fn)
51
+ absolute_path = File.join(@directory, relative_path)
52
+ info = {
53
+ path: absolute_path,
54
+ url: pretty_url(relative_path)
55
+ }
56
+ if fn =~ MD_EXT_REGEXP
57
+ atts, _ = parse_markdown_file(absolute_path)
58
+ info.merge!(atts)
59
+ end
60
+
61
+ if (m = fn.match(DATE_REGEXP))
62
+ info[:date] ||= Date.parse(m[1])
63
+ end
64
+
65
+ info
66
+ end
67
+
68
+ # Returns the pretty URL for the given relative path. For pages, the
69
+ # extension is removed. For index pages, the index suffix is removed.
70
+ #
71
+ # @param relative_path [String] relative path
72
+ # @return [String] pretty URL
73
+ def pretty_url(relative_path)
74
+ if (m = relative_path.match(PAGE_EXT_REGEXP))
75
+ relative_path = m[1]
76
+ end
77
+ if (m = relative_path.match(INDEX_PAGE_REGEXP))
78
+ relative_path = m[1]
79
+ end
80
+ File.join(absolute_path, relative_path)
81
+ end
82
+
83
+ # Renders a file response for the given request and the given path info.
84
+ #
85
+ # @param req [Qeweney::Request] request
86
+ # @param path_info [Hash] path info
87
+ # @return [void]
88
+ def render_file(req, path_info)
89
+ case path_info[:ext]
90
+ when '.rb'
91
+ render_papercraft_module(req, path_info[:path])
92
+ when '.md'
93
+ render_markdown_file(req, path_info[:path])
94
+ else
95
+ req.serve_file(path_info[:path])
96
+ end
97
+ end
98
+
99
+ # Renders a Papercraft module. The module is loaded using Modulation.
100
+ #
101
+ # @param req [Qeweney::Request] reqest
102
+ # @param path [String] file path
103
+ # @return [void]
104
+ def render_papercraft_module(req, path)
105
+ mod = import path
106
+
107
+ html = H(mod).render(request: req, resource: self)
108
+ req.respond(html, 'Content-Type' => Qeweney::MimeTypes[:html])
109
+ end
110
+
111
+ # Renders a markdown file using a layout.
112
+ #
113
+ # @param req [Qeweney::Request] reqest
114
+ # @param path [String] file path
115
+ # @return [void]
116
+ def render_markdown_file(req, path)
117
+ attributes, markdown = parse_markdown_file(path)
118
+
119
+ layout = get_layout(attributes[:layout])
120
+
121
+ html = layout.render(request: req, resource: self, **attributes) { emit_markdown markdown }
122
+ req.respond(html, 'Content-Type' => Qeweney::MimeTypes[:html])
123
+ end
124
+
125
+ # Returns a layout component based on the given name. The given name
126
+ # defaults to 'default' if nil.
127
+ #
128
+ # @param layout [String, nil] layout name
129
+ # @return [Papercraft::Component] layout component
130
+ def get_layout(layout)
131
+ layout ||= 'default'
132
+ path = File.join(@directory, "_layouts/#{layout}.rb")
133
+ raise "Layout not found #{path}" unless File.file?(path)
134
+
135
+ import path
136
+ end
137
+
138
+ # Parses the markdown file at the given path.
139
+ #
140
+ # @param path [String] file path
141
+ # @return [Array] an tuple containing properties<Hash>, contents<String>
142
+ def parse_markdown_file(path)
143
+ data = IO.read(path) || ''
144
+ atts = {}
145
+
146
+ # Parse date from file name
147
+ if (m = path.match(DATE_REGEXP))
148
+ atts[:date] ||= Date.parse(m[1])
149
+ end
150
+
151
+ if (m = data.match(MARKDOWN_PAGE_REGEXP))
152
+ front_matter = m[1]
153
+ data = m.post_match
154
+
155
+ YAML.load(front_matter).each_with_object(atts) do |(k, v), h|
156
+ h[k.to_sym] = v
157
+ end
158
+ end
159
+
160
+ [atts, data]
161
+ end
162
+
163
+ # Converts a hash with string keys to one with symbol keys.
164
+ #
165
+ # @param hash [Hash] input hash
166
+ # @return [Hash] output hash
167
+ def symbolize_keys(hash)
168
+
169
+ end
170
+
171
+ # Returns the supported path extensions used for searching for files based
172
+ # on pretty URLs.
173
+ #
174
+ # @return [Array] list of supported path extensions
175
+ def supported_path_extensions
176
+ [:html, :rb, :md]
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Impression
6
+
7
+ module RequestExtensions
8
+
9
+ # Response extensions for `Qeweney::Request`
10
+ module Responses
11
+
12
+ TEXT_HEADERS = { 'Content-Type' => Qeweney::MimeTypes[:txt] }.freeze
13
+ HTML_HEADERS = { 'Content-Type' => Qeweney::MimeTypes[:html] }.freeze
14
+ JSON_HEADERS = { 'Content-Type' => Qeweney::MimeTypes[:json] }.freeze
15
+
16
+ # Send an HTTP response with plain text content. The content type is set
17
+ # to `text/plain`.
18
+ #
19
+ # @param text [String] response body
20
+ # @param **headers [Hash] additional response headers
21
+ def respond_text(text, **headers)
22
+ headers = headers.empty? ? TEXT_HEADERS : headers.merge(TEXT_HEADERS)
23
+ respond(text, headers)
24
+ end
25
+
26
+ # Send an HTTP response with HTML content. The content type is set to
27
+ # `text/html`.
28
+ #
29
+ # @param html [String] response body
30
+ # @param **headers [Hash] additional response headers
31
+ def respond_html(html, **headers)
32
+ headers = headers.empty? ? HTML_HEADERS : headers.merge(HTML_HEADERS)
33
+ respond(html, headers)
34
+ end
35
+
36
+ # Send an JSON response. The given object is converted to JSON. The
37
+ # content type is set to `application/json`.
38
+ #
39
+ # @param object [any] object to convert to JSON
40
+ # @param **headers [Hash] additional response headers
41
+ def respond_json(object, **headers)
42
+ headers = headers.empty? ? JSON_HEADERS : headers.merge(JSON_HEADERS)
43
+ respond(object.to_json, headers)
44
+ end
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Impression
4
+
5
+ # Extensions for `Qeweney::Request`
6
+ module RequestExtensions
7
+
8
+ # Routing extensions for `Qeweney::Request`
9
+ module Routing
10
+
11
+ # Matches a route regexp against the relative request path. The relative
12
+ # request path is a separate string (stored in `@resource_relative_path`)
13
+ # that is updated as routes are matched against it. The `route_regexp`
14
+ # should be either `nil` for root routes (`/`) or a Regexp of the form
15
+ # `/^#{route}(\/.*)?$/`. See also `Resource#initialize`.
16
+ #
17
+ # @param route_regexp [Regexp, nil] Route regexp to match against
18
+ # @return [String, nil] The remainder of the path (relative to the route)
19
+ def match_resource_path?(route_regexp)
20
+ @resource_relative_path ||= path.dup
21
+
22
+ return @resource_relative_path unless route_regexp
23
+
24
+ # Simplified logic: no match returns nil, otherwise we set the relative path for
25
+ @resource_relative_path = match_resource_relative_path(
26
+ @resource_relative_path, route_regexp
27
+ )
28
+ end
29
+
30
+ # Returns the relative_path for the latest matched resource
31
+ #
32
+ # @return [String]
33
+ def resource_relative_path
34
+ @resource_relative_path ||= path.dup
35
+ end
36
+
37
+ private
38
+
39
+ # Matches the given path against the given route regexp. If the path matches
40
+ # the regexp, the relative path for the given route is returned. Otherwise,
41
+ # this method returns `nil`.
42
+ #
43
+ # @param path [String] path to match
44
+ # @param route_regexp [Regexp] route regexp to match against
45
+ # @return [String, nil] the relative path for the given route, or nil if no match.
46
+ def match_resource_relative_path(path, route_regexp)
47
+ match = path.match(route_regexp)
48
+ match && (match[1] || '/')
49
+ end
50
+ end
51
+ end
52
+ end
@@ -2,8 +2,14 @@
2
2
 
3
3
  require 'qeweney'
4
4
 
5
- require_relative './pages'
5
+ # require_relative './pages'
6
+ require_relative './request_extensions/routing'
7
+ require_relative './request_extensions/responses'
6
8
 
9
+ # Extensions to `Qeweney::Request`
7
10
  class Qeweney::Request
8
- include Impression::Pages::RequestMethods
11
+
12
+ # include Impression::Pages::RequestMethods
13
+ include Impression::RequestExtensions::Routing
14
+ include Impression::RequestExtensions::Responses
9
15
  end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'qeweney'
5
+
6
+ module Impression
7
+
8
+ # The `Resource` class represents an abstract web resource. Resources are
9
+ # organized in tree like structure, with the structure normally corresponding
10
+ # to the URL path hierarchy. Other ways of organising resources, according to
11
+ # other taxonomies, can be implemented as long as the resources implement the
12
+ # same interface as the `Resource` class, which includes the following
13
+ # methods:
14
+ #
15
+ # - `Resource#route` - returns the resource which should respond to the
16
+ # request.
17
+ # - `Resource#respond` - responds to the request.
18
+ #
19
+ class Resource
20
+ # Reference to the parent resource
21
+ attr_reader :parent
22
+
23
+ # The resource's path relative to its parent
24
+ attr_reader :path
25
+
26
+ # A hash mapping relative paths to child resources
27
+ attr_reader :children
28
+
29
+ # Initalizes a new resource instance.
30
+ #
31
+ # @param parent [Impression::Resource, nil] the parent resource (or nil)
32
+ # @param path [String] the resource's relative path
33
+ # @return [void]
34
+ def initialize(parent: nil, path:)
35
+ @parent = parent
36
+ @path = normalize_route_path(path)
37
+ @route_regexp = @path == '/' ? nil : /^#{@path}(\/.*)?$/.freeze
38
+ @children = {}
39
+
40
+ @parent&.add_child(self)
41
+ end
42
+
43
+ # Returns the resource's absolute path, according to its location in the
44
+ # resource hierarchy.
45
+ #
46
+ # @return [String] absolute path
47
+ def absolute_path
48
+ @absoulte_path ||= File.join(@parent ? @parent.absolute_path : '/', @path)
49
+ end
50
+
51
+ # Iterates over the resource and any of its sub-resources, passing each to
52
+ # the given block.
53
+ #
54
+ # @return [Impression::Resource] self
55
+ def each(&block)
56
+ block.(self)
57
+ @children.values.each { |c| c.each(&block) }
58
+ self
59
+ end
60
+
61
+ # Adds a child reference to the children map.
62
+ #
63
+ # @param child [Impression::Resource] child resource
64
+ # @return [Impression::Resource] self
65
+ def add_child(child)
66
+ @children[child.path] = child
67
+ self
68
+ end
69
+
70
+ # Responds to the given request by rendering a 404 Not found response.
71
+ #
72
+ # @param req [Qeweney::Request] request
73
+ # @return [void]
74
+ def call(req)
75
+ req.respond(nil, ':status' => Qeweney::Status::NOT_FOUND)
76
+ end
77
+
78
+ FIRST_PATH_SEGMENT_REGEXP = /^(\/[^\/]+)\//.freeze
79
+
80
+ # Routes the request by matching self and any children against the request
81
+ # path, returning the target resource, or nil if there's no match.
82
+ #
83
+ # @param req [Qeweney::Request] request
84
+ # @return [Impression::Resource, nil] target resource
85
+ def route(req)
86
+ case (relative_path = req.match_resource_path?(@route_regexp))
87
+ when nil
88
+ return nil
89
+ when '/'
90
+ return self
91
+ else
92
+ # naive case
93
+ child = @children[relative_path]
94
+ return child.route(req) if child
95
+
96
+ if (m = relative_path.match(FIRST_PATH_SEGMENT_REGEXP))
97
+ child = @children[m[1]]
98
+ return child.route(req) if child
99
+ end
100
+
101
+ return self
102
+ end
103
+ end
104
+
105
+ # Renders the resource and all of its sub-resources to static files.
106
+ #
107
+ # @param base_path [String] base path of target directory
108
+ # @return [Impression::Resource] self
109
+ def render_tree_to_static_files(base_path)
110
+ each do |r|
111
+ path = File.join(base_path, r.relative_static_file_path)
112
+ dir = File.dirname(path)
113
+ FileUtils.mkdir_p(dir) if !File.directory?(dir)
114
+ File.open(path, 'w') { |f| r.render_to_file(f) }
115
+ end
116
+ self
117
+ end
118
+
119
+ # Converts the resource to a Proc, for use as a Qeweney app.
120
+ #
121
+ # @return [Proc] web app proc
122
+ def to_proc
123
+ ->(req) do
124
+ resource = route(req) || self
125
+ resource.call(req)
126
+ end
127
+ end
128
+
129
+ # Returns a callable that responds with plain text using the given
130
+ # parameters.
131
+ #
132
+ # @param text [String] response body
133
+ # @param **headers [Hash] additional response headers
134
+ def text_response(text, **headers)
135
+ ->(req) { req.respond_text(text, **headers) }
136
+ end
137
+
138
+ # Returns a callable that responds with HTML using the given
139
+ # parameters.
140
+ #
141
+ # @param html [String] response body
142
+ # @param **headers [Hash] additional response headers
143
+ def html_response(html, **headers)
144
+ ->(req) { req.respond_html(html, **headers) }
145
+ end
146
+
147
+ # Returns a callable that responds with JSON using the given
148
+ # parameters.
149
+ #
150
+ # @param object [any] object to be converted to JSON
151
+ # @param **headers [Hash] additional response headers
152
+ def json_response(object, **headers)
153
+ ->(req) { req.respond_json(object, **headers) }
154
+ end
155
+
156
+ private
157
+
158
+ SLASH_PREFIXED_PATH_REGEXP = /^\//.freeze
159
+
160
+ # Normalizes the given path by ensuring it starts with a slash.
161
+ #
162
+ # @param path [String] path to normalize
163
+ # @return [String] normalized path
164
+ def normalize_route_path(path)
165
+ path =~ SLASH_PREFIXED_PATH_REGEXP ? path : "/#{path}"
166
+ end
167
+ end
168
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Impression
4
- VERSION = '0.1'
4
+ VERSION = '0.5'
5
5
  end
data/lib/impression.rb CHANGED
@@ -1,3 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'polyphony'
4
+
3
5
  require_relative './impression/request_extensions'
6
+ # require_relative './impression/file_watcher'
7
+
8
+ require_relative './impression/resource'
9
+ require_relative './impression/file_tree'
10
+ require_relative './impression/jamstack'
11
+ require_relative './impression/app'
data/test/helper.rb ADDED
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require_relative './coverage' if ENV['COVERAGE']
5
+ require 'minitest/autorun'
6
+ require 'impression'
7
+ require 'qeweney/test_adapter'
8
+
9
+ module Kernel
10
+ def mock_req(**args)
11
+ Qeweney::TestAdapter.mock(**args)
12
+ end
13
+
14
+ def capture_exception
15
+ yield
16
+ rescue Exception => e
17
+ e
18
+ end
19
+
20
+ def trace(*args)
21
+ STDOUT.orig_write(format_trace(args))
22
+ end
23
+
24
+ def format_trace(args)
25
+ if args.first.is_a?(String)
26
+ if args.size > 1
27
+ format("%s: %p\n", args.shift, args)
28
+ else
29
+ format("%s\n", args.first)
30
+ end
31
+ else
32
+ format("%p\n", args.size == 1 ? args.first : args)
33
+ end
34
+ end
35
+ end
36
+
37
+ module Minitest::Assertions
38
+ def assert_in_range exp_range, act
39
+ msg = message(msg) { "Expected #{mu_pp(act)} to be in range #{mu_pp(exp_range)}" }
40
+ assert exp_range.include?(act), msg
41
+ end
42
+
43
+ def assert_response exp_body, exp_content_type, req
44
+ actual = req.response_body
45
+ assert_equal exp_body.gsub("\n", ''), actual&.gsub("\n", '')
46
+
47
+ return unless exp_content_type
48
+
49
+ if Symbol === exp_content_type
50
+ exp_content_type = Qeweney::MimeTypes[exp_content_type]
51
+ end
52
+ actual = req.response_content_type
53
+ assert_equal exp_content_type, actual
54
+ end
55
+ end
56
+
57
+ class Impression::Resource
58
+ def route_and_call(req)
59
+ route(req).call(req)
60
+ end
61
+ end
62
+
63
+ class PathRenderingResource < Impression::Resource
64
+ def call(req)
65
+ req.respond(absolute_path)
66
+ end
67
+ end
68
+
69
+ class CompletePathInfoRenderingResource < Impression::Resource
70
+ def call(req)
71
+ req.respond("#{absolute_path} #{req.resource_relative_path}")
72
+ end
73
+ end
74
+
75
+ # Extensions to be used in conjunction with `Qeweney::TestAdapter`
76
+ class Qeweney::Request
77
+ def response_headers
78
+ adapter.headers
79
+ end
80
+
81
+ def response_body
82
+ adapter.body
83
+ end
84
+
85
+ def response_status
86
+ adapter.status
87
+ end
88
+
89
+ def response_content_type
90
+ response_headers['Content-Type']
91
+ end
92
+ end
93
+
94
+ puts "Polyphony backend: #{Thread.current.backend.kind}"
@@ -0,0 +1,9 @@
1
+ require 'papercraft'
2
+
3
+ default = import('./default')
4
+
5
+ export_default default.apply { |**props|
6
+ article {
7
+ emit_yield
8
+ }
9
+ }
@@ -0,0 +1,12 @@
1
+ require 'papercraft'
2
+
3
+ export_default H { |**props|
4
+ html5 {
5
+ head {
6
+ title props[:title]
7
+ }
8
+ body {
9
+ emit_yield **props
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,6 @@
1
+ ---
2
+ title: MMM
3
+ layout: article
4
+ ---
5
+
6
+ ## BBB
@@ -0,0 +1,6 @@
1
+ ---
2
+ title: NNN
3
+ layout: article
4
+ ---
5
+
6
+ ## BBB
@@ -0,0 +1,6 @@
1
+ ---
2
+ title: AAA
3
+ layout: article
4
+ ---
5
+
6
+ ## BBB
@@ -0,0 +1 @@
1
+ console.log(42);
@@ -0,0 +1 @@
1
+ <h1>Bar</h1>
@@ -0,0 +1,7 @@
1
+ ---
2
+ title: BarBar
3
+ ---
4
+
5
+ <h1>BarIndex</h1>
6
+
7
+
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ layout = import('./_layouts/default')
4
+
5
+ export_default layout.apply(title: 'Foo title') {
6
+ h1 'foo'
7
+ }
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ layout = import('./_layouts/default')
4
+
5
+ export_default layout.apply(title: 'Foobar') { |resource:, request:, **props|
6
+ h1 request.query[:q]
7
+ resource.page_list('/articles').each do |i|
8
+ a i[:title], href: i[:url]
9
+ end
10
+ }
@@ -0,0 +1,4 @@
1
+ ---
2
+ title: Hello
3
+ ---
4
+ <h1>Index</h1>
data/test/run.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir.glob("#{__dir__}/test_*.rb").each do |path|
4
+ require(path)
5
+ end
@@ -0,0 +1 @@
1
+ <h1>BarIndex</h1>
@@ -0,0 +1 @@
1
+ <h1>Foo</h1>
@@ -0,0 +1 @@
1
+ <h1>Index</h1>
@@ -0,0 +1 @@
1
+ console.log(42);