confinement 0.0.2 → 0.0.3

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: a991bdb8646d092772f6ae7154bc34c7c89ac4faf8d51822e7b1361ed8f7ec05
4
- data.tar.gz: 073003314b979b4bbf03dc3baf487030e6a07199feef361b5addad3928a7b06c
3
+ metadata.gz: 317875d42d9d6449fe4f5ba24a5411defb9211b88a22270cac6457afa55308bb
4
+ data.tar.gz: 331794f2b0b08897f03c40b5147d08cc474d76ee941f8e87d264042256f84d77
5
5
  SHA512:
6
- metadata.gz: 72cffafd98539f4655a039c674d9324e998ef508610bad89f9f6e21a5d06cd158487413b60bdeb6b961d76f338c44013bb4ff9f35ea0733ffba74856f01046d8
7
- data.tar.gz: 1d6076cc1b834d0a3bf3cd9fcaee3dc7cabd6e048e3f5dc298a7d201c6b60d44f63f0a5f164378c0a9883d9a19e6c83e422a0b9b07d2727164e6e7f2a2493343
6
+ metadata.gz: 53a6285bc8cacda51cb4a4edd45562ce41ba7b483f643032276b5720dae0b774137324c3ca410dde65b9a007369a334d8b71cd0f427788edc67a0db244f4cc91
7
+ data.tar.gz: b6713bcc1eff930f4e4007d85579090409e2f56196abc533170b85ea5858be96876a6e8c029cb0ad4045fdbe501962c9439582231cbf46363803587db96687eb
@@ -1,24 +1,28 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- confinement (0.0.2)
4
+ confinement (0.0.3)
5
5
  erubi
6
+ puma (~> 4.3)
7
+ rack (~> 2.2)
8
+ zeitwerk (~> 2.3)
6
9
 
7
10
  GEM
8
11
  remote: https://rubygems.org/
9
12
  specs:
10
- byebug (11.1.1)
11
13
  coderay (1.1.2)
12
14
  erubi (1.9.0)
13
15
  method_source (1.0.0)
14
16
  minitest (5.14.0)
17
+ nio4r (2.5.2)
15
18
  pry (0.13.0)
16
19
  coderay (~> 1.1)
17
20
  method_source (~> 1.0)
18
- pry-byebug (3.9.0)
19
- byebug (~> 11.0)
20
- pry (~> 0.13.0)
21
+ puma (4.3.3)
22
+ nio4r (~> 2.0)
23
+ rack (2.2.2)
21
24
  rake (13.0.1)
25
+ zeitwerk (2.3.0)
22
26
 
23
27
  PLATFORMS
24
28
  ruby
@@ -27,7 +31,7 @@ DEPENDENCIES
27
31
  bundler (~> 2.0)
28
32
  confinement!
29
33
  minitest (~> 5.0)
30
- pry-byebug
34
+ pry
31
35
  rake (~> 13.0)
32
36
 
33
37
  BUNDLED WITH
data/README.md CHANGED
@@ -28,6 +28,8 @@ Or install it yourself as:
28
28
 
29
29
  ```sh
30
30
  confinement init path/to/new/site
31
+ cd path/to/new/site
32
+ confinement server # or confinement build
31
33
  ```
32
34
 
33
35
 
@@ -26,7 +26,10 @@ Gem::Specification.new do |spec|
26
26
  spec.add_development_dependency "bundler", "~> 2.0"
27
27
  spec.add_development_dependency "rake", "~> 13.0"
28
28
  spec.add_development_dependency "minitest", "~> 5.0"
29
- spec.add_development_dependency "pry-byebug"
29
+ spec.add_development_dependency "pry"
30
30
 
31
31
  spec.add_runtime_dependency("erubi")
32
+ spec.add_runtime_dependency("zeitwerk", "~> 2.3")
33
+ spec.add_runtime_dependency("rack", "~> 2.2")
34
+ spec.add_runtime_dependency("puma", "~> 4.3")
32
35
  end
@@ -9,6 +9,7 @@ end
9
9
  require "optparse"
10
10
  require "pathname"
11
11
 
12
+ require "confinement"
12
13
  require "confinement/version"
13
14
 
14
15
  module Confinement
@@ -16,6 +17,8 @@ module Confinement
16
17
  def self.subcommands
17
18
  @subcommands ||= {
18
19
  "init" => Init.new,
20
+ "server" => Server.new,
21
+ "build" => Build.new,
19
22
  }
20
23
  end
21
24
 
@@ -34,6 +37,12 @@ module Confinement
34
37
 
35
38
  opts.on("-h", "--help", "Prints this help message") do
36
39
  end
40
+
41
+ opts.separator("")
42
+ opts.separator("Subcommands")
43
+ self.class.subcommands.each do |name, _|
44
+ opts.separator(" #{name}")
45
+ end
37
46
  end
38
47
  end
39
48
 
@@ -60,6 +69,202 @@ module Confinement
60
69
 
61
70
  private
62
71
 
72
+ class Build
73
+ def optparser
74
+ @options ||= {
75
+ "--setup" => "config/setup.rb",
76
+ "--rules" => "rules.rb",
77
+ }
78
+ @optparser ||= OptionParser.new do |opts|
79
+ opts.banner = "Usage: #{CLI.script_name} build [options]"
80
+ opts.on("--setup=PATH", "Path to setup file (default: #{@options["--setup"]})") do |path|
81
+ @options["--setup"] = path
82
+ end
83
+ opts.on("--rules=PATH", "Path to rules file (default: #{@options["--rules"]})") do |path|
84
+ @options["--rules"] = path
85
+ end
86
+ opts.on("--help", "Print this help message") do
87
+ puts self
88
+ exit
89
+ end
90
+ end
91
+ end
92
+
93
+ def call(argv)
94
+ setup_path = @options.fetch("--setup")
95
+ rules_path = @options.fetch("--rules")
96
+
97
+ load Pathname.new(setup_path).expand_path
98
+ @compiler = Compiler.new(Confinement.config)
99
+
100
+ load Pathname.new(rules_path).expand_path
101
+ @compiler.compile_everything(Confinement.site)
102
+ end
103
+ end
104
+
105
+ class Server
106
+ using Easier
107
+
108
+ def optparser
109
+ @options ||= {
110
+ "--setup" => "config/setup.rb",
111
+ "--rules" => "rules.rb",
112
+ "--port" => "7000"
113
+ }
114
+ @optparser ||= OptionParser.new do |opts|
115
+ opts.banner = "Usage: #{CLI.script_name} server [options]"
116
+ opts.on("--setup=PATH", "Path to setup file (default: #{@options["--setup"]})") do |path|
117
+ @options["--setup"] = path
118
+ end
119
+ opts.on("--rules=PATH", "Path to rules file (default: #{@options["--rules"]})") do |path|
120
+ @options["--rules"] = path
121
+ end
122
+ opts.on("--port=PORT", "Port to listen to (default: #{@options["--port"]})") do |port|
123
+ @options["--port"] = port
124
+ end
125
+ end
126
+ end
127
+
128
+ def call(argv)
129
+ if argv.size > 1
130
+ puts optparser
131
+ exit
132
+ end
133
+
134
+ setup_path = @options.fetch("--setup")
135
+ rules_path = @options.fetch("--rules")
136
+
137
+ setup_path = Pathname.new(setup_path).expand_path
138
+ rules_path = Pathname.new(rules_path).expand_path
139
+
140
+ if !setup_path.exist?
141
+ raise "Cannot find setup file at: #{setup_path}"
142
+ end
143
+
144
+ require "rack"
145
+ require "puma"
146
+ require "puma/configuration"
147
+ require "confinement/filewatcher/filewatcher"
148
+
149
+ load setup_path
150
+
151
+ dirty = Dirty.new
152
+
153
+ puma_config = Puma::Configuration.new do |user_config, file_config, default_config|
154
+ user_config.port(@options.fetch("--port").to_i)
155
+ user_config.app(Rack::CommonLogger.new(App.new(Confinement.config, rules_path, dirty)))
156
+ end
157
+
158
+ assets_filewatcher = Filewatcher.new(Confinement.config.watcher.assets)
159
+ contents_filewatcher = Filewatcher.new(Confinement.config.watcher.contents)
160
+ puma_launcher = Puma::Launcher.new(puma_config, events: Puma::Events.stdio)
161
+
162
+ assets_thread = Thread.new do
163
+ assets_filewatcher.watch { dirty.dirty_assets! }
164
+ end
165
+
166
+ contents_thread = Thread.new do
167
+ contents_filewatcher.watch { dirty.dirty_contents! }
168
+ end
169
+
170
+ puma_launcher.run
171
+ end
172
+
173
+ class Dirty
174
+ def initialize
175
+ @dirty_assets = true
176
+ @dirty_contents = true
177
+ @mutex = Mutex.new
178
+ end
179
+
180
+ def dirty_assets!
181
+ @mutex.synchronize { @dirty_assets = true }
182
+ end
183
+
184
+ def dirty_contents!
185
+ @mutex.synchronize { @dirty_contents = true }
186
+ end
187
+
188
+ def clean!
189
+ @mutex.synchronize do
190
+ if @dirty_assets || @dirty_contents
191
+ yield(@dirty_assets, @dirty_contents)
192
+ end
193
+
194
+ @dirty_assets = false
195
+ @dirty_contents = false
196
+ end
197
+ end
198
+ end
199
+
200
+ class App
201
+ def initialize(config, rules_path, dirty)
202
+ @config = config
203
+ @logger = config.logger
204
+ @rules_path = rules_path
205
+ @dirty = dirty
206
+ @output_root_path = config.compiler.output_root_path
207
+ @output_directory_index = config.compiler.output_directory_index
208
+ @compiler = Compiler.new(config)
209
+ @reload = false
210
+ end
211
+
212
+ def call(env)
213
+ if !["GET", "HEAD"].include?(env["REQUEST_METHOD"])
214
+ return [405, { "Content-Type" => "text/plain" }, ["Unsupported method: ", env["REQUEST_METHOD"]]]
215
+ end
216
+
217
+ @dirty.clean! do |is_dirty_assets, is_dirty_contents|
218
+ @logger.debug { "dirty: assets=#{is_dirty_assets}, contents=#{is_dirty_contents}" }
219
+
220
+ if @reload
221
+ @logger.debug { "reloading with Zeitwerk" }
222
+ Confinement.config.loader.reload
223
+ else
224
+ @reload = true
225
+ end
226
+
227
+ if is_dirty_assets
228
+ @logger.debug { "loading: #{@rules_path}" }
229
+ load @rules_path
230
+ @compiler.compile_everything(Confinement.site)
231
+ elsif is_dirty_contents
232
+ partial_compilation = Confinement.site.partial_compilation
233
+ @logger.debug { "loading: #{@rules_path}" }
234
+ load @rules_path
235
+ precompilation_partial_compilation = Confinement.site.partial_compilation
236
+
237
+ if @compiler.partial_compilation_dirty?(before: partial_compilation, after: precompilation_partial_compilation)
238
+ @logger.info { "detected asset change" }
239
+ @compiler.compile_everything(Confinement.site)
240
+ else
241
+ Confinement.site.partial_compilation = partial_compilation
242
+ @compiler.compile_contents(Confinement.site)
243
+ end
244
+ end
245
+
246
+ @logger.debug { "dirty: finished!" }
247
+ end
248
+
249
+ file_path = @output_root_path.concat(env["SCRIPT_NAME"] + env["PATH_INFO"])
250
+
251
+ if file_path.directory?
252
+ file_path = file_path.concat(@output_directory_index)
253
+ end
254
+
255
+ if !file_path.exist?
256
+ return [404, {}, ["Page not found!\n", file_path.to_s]]
257
+ end
258
+
259
+ if env["REQUEST_METHOD"] == "HEAD"
260
+ return [204, {}, []]
261
+ end
262
+
263
+ [200, {}, [file_path.read]]
264
+ end
265
+ end
266
+ end
267
+
63
268
  class Init
64
269
  def optparser
65
270
  @options ||= {}
@@ -99,10 +304,14 @@ module Confinement
99
304
  [Pathname.new(File.join(root, path.strip)), body.strip + "\n"]
100
305
  end
101
306
 
307
+ mkdir(root, root)
308
+ mkdir(root.join("lib"), root)
309
+ mkdir(root.join("tmp"), root)
310
+
102
311
  templates
103
312
  .map { |path, _| path.dirname }
104
313
  .uniq
105
- .each { |path| mkdir(path, root) }
314
+ .each { |path| mkdir(path, root) if path != root }
106
315
 
107
316
  templates.each do |path, body|
108
317
  write(path, body, root)
@@ -175,25 +384,47 @@ end
175
384
  Confinement::CLI.new.call(ARGV.dup)
176
385
 
177
386
  __END__
178
- ==> boot.rb
387
+ ==> config/boot.rb
179
388
  require "confinement"
180
389
 
181
- Confinement.root = __dir__
390
+ ==> config/setup.rb
391
+ require_relative "boot"
182
392
 
183
- Confinement.site = Confinement::Site.new(
184
- root: Confinement.root,
185
- assets: "assets",
186
- contents: "contents",
187
- layouts: "layouts",
188
- config: {
189
- index: "index.html",
190
- }
191
- )
393
+ Confinement.config = Confinement::Config.new(root: File.dirname(__dir__))
192
394
 
193
- ==> build.rb
194
- require_relative "boot"
395
+ Confinement.config.compiler do |compiler|
396
+ compiler.output_root = compiler.default_output_root
397
+ compiler.output_assets = "assets"
398
+ compiler.output_directory_index = "index.html"
399
+ end
400
+
401
+ Confinement.config.source do |source|
402
+ source.assets = "assets"
403
+ source.contents = "contents"
404
+ source.layouts = "layouts"
405
+ end
195
406
 
196
- Confinement.site.build do |assets:, layouts:, contents:, routes:|
407
+ Confinement.config.loader do |loader|
408
+ loader.push_dir("lib")
409
+ loader.enable_reloading
410
+ end
411
+
412
+ Confinement.config.watcher do |paths|
413
+ paths.assets.push("assets/")
414
+ paths.assets.push("package.json")
415
+ paths.assets.push("yarn.lock")
416
+ paths.contents.push("contents/")
417
+ paths.contents.push("lib/")
418
+ paths.contents.push("rules.rb")
419
+ end
420
+
421
+ ==> rules.rb
422
+ Confinement.site = Confinement::Site.new(Confinement.config) do |site|
423
+ site.view_context_helpers = []
424
+ site.guesses = Confinement::Renderer.guesses
425
+ end
426
+
427
+ Confinement.site.rules do |assets:, layouts:, contents:, routes:|
197
428
  assets.init("application.js", entrypoint: true)
198
429
  assets.init("application.css", entrypoint: false)
199
430
 
@@ -204,11 +435,6 @@ Confinement.site.build do |assets:, layouts:, contents:, routes:|
204
435
  end
205
436
  end
206
437
 
207
- ==> write.rb
208
- require_relative "build"
209
-
210
- Confinement::Publish.new(Confinement.site).write
211
-
212
438
  ==> package.json
213
439
  {
214
440
  "name": "website",
@@ -1,13 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- if Gem.loaded_specs.has_key?("pry-byebug")
4
- require "pry-byebug"
5
- elsif Gem.loaded_specs.has_key?("pry-byebug")
3
+ if Gem.loaded_specs.has_key?("pry")
6
4
  require "pry"
7
5
  end
8
6
 
9
7
  # standard library
10
8
  require "digest"
9
+ require "logger"
11
10
  require "open3"
12
11
  require "pathname"
13
12
  require "yaml"
@@ -15,6 +14,7 @@ require "yaml"
15
14
  # gems
16
15
  require "erubi"
17
16
  require "erubi/capture_end"
17
+ require "zeitwerk"
18
18
 
19
19
  # internal
20
20
  require_relative "confinement/version"
@@ -76,68 +76,183 @@ module Confinement
76
76
 
77
77
  using Easier
78
78
 
79
+ module BuilderGetterInitialization
80
+ def builder_getter(method_name, klass, ivar, new: [])
81
+ init_parameters = [*new, "&block"].join(", ")
82
+
83
+ class_eval(<<~RUBY, __FILE__, __LINE__)
84
+ def #{method_name}(&block)
85
+ if #{ivar}
86
+ if block_given?
87
+ raise "#{method_name} is already set up"
88
+ end
89
+
90
+ return #{ivar}
91
+ end
92
+
93
+ if !block_given?
94
+ raise "Can't initialize #{method_name} without block"
95
+ end
96
+
97
+ #{ivar} = #{klass}.new(#{init_parameters})
98
+ #{ivar}
99
+ end
100
+ RUBY
101
+ end
102
+ end
103
+
79
104
  class << self
80
- attr_reader :root
105
+ extend BuilderGetterInitialization
106
+
107
+ attr_accessor :config
81
108
  attr_accessor :site
109
+ attr_writer :env
82
110
 
83
- def root=(path)
84
- # NOTE: Pathname.new(Pathname.new(".")) == Pathname.new(".")
85
- path = Pathname.new(path).expand_path
86
- path = path.expand_path
111
+ def env
112
+ @env ||= ENV.fetch("CONFINEMENT_ENV", "development")
113
+ end
114
+ end
87
115
 
88
- if !path.exist?
89
- raise Error::PathDoesNotExist, "Root path does not exist: #{path}"
116
+ class Config
117
+ extend BuilderGetterInitialization
118
+
119
+ builder_getter("loader", "ZeitwerkProxy", "@loader")
120
+ builder_getter("watcher", "WatcherPaths", "@watcher", new: ["root: @root"])
121
+ builder_getter("compiler", "Config::Compiler", "@compiler", new: ["root: @root"])
122
+ builder_getter("source", "Config::Source", "@source", new: ["root: @root"])
123
+
124
+ def initialize(root:)
125
+ @root = Pathname.new(root).expand_path.cleanpath
126
+
127
+ if !@root.exist?
128
+ raise Error::PathDoesNotExist, "Root path does not exist: #{@root}"
90
129
  end
130
+ end
131
+
132
+ attr_reader :root
133
+ attr_writer :logger
134
+
135
+ def logger
136
+ @logger ||= default_logger
137
+ end
91
138
 
92
- @root = path
139
+ def default_logger
140
+ Logger.new($stdout).tap do |l|
141
+ l.level = Logger::INFO
142
+ end
143
+ end
144
+
145
+ class ZeitwerkProxy
146
+ def initialize
147
+ @loader = Zeitwerk::Loader.new
148
+ yield(self)
149
+ @loader.setup
150
+ end
151
+
152
+ def push_dir(dir)
153
+ @loader.push_dir(dir)
154
+ end
155
+
156
+ def enable_reloading
157
+ @loader.enable_reloading
158
+ end
159
+
160
+ def reload
161
+ @loader.reload
162
+ end
163
+ end
164
+
165
+ class WatcherPaths
166
+ def initialize(root:)
167
+ @root = root
168
+ @assets = []
169
+ @contents = []
170
+
171
+ yield(self)
172
+
173
+ @assets = @assets.map { |path| @root.concat(path) }
174
+ @contents = @contents.map { |path| @root.concat(path) }
175
+ end
176
+
177
+ attr_reader :assets
178
+ attr_reader :contents
179
+ end
180
+
181
+ class Compiler
182
+ def initialize(root:)
183
+ @root = root
184
+ yield(self)
185
+
186
+ self.output_root ||= default_output_root
187
+ end
188
+
189
+ attr_accessor :output_root
190
+ attr_accessor :output_assets
191
+ attr_accessor :output_directory_index
192
+
193
+ def output_root_path
194
+ @root.concat(output_root).cleanpath.expand_path
195
+ end
196
+
197
+ def output_assets_path
198
+ @root.concat(output_root, output_assets).cleanpath.expand_path
199
+ end
200
+
201
+ def default_output_root
202
+ "tmp/build-#{Confinement.env}"
203
+ end
204
+ end
205
+
206
+ class Source
207
+ def initialize(root:)
208
+ @root = root
209
+ yield(self)
210
+ end
211
+
212
+ attr_accessor :assets
213
+ attr_accessor :contents
214
+ attr_accessor :layouts
215
+
216
+ def assets_path
217
+ @root.concat(assets).cleanpath.expand_path
218
+ end
219
+
220
+ def contents_path
221
+ @root.concat(contents).cleanpath.expand_path
222
+ end
223
+
224
+ def layouts_path
225
+ @root.concat(layouts).cleanpath.expand_path
226
+ end
93
227
  end
94
228
  end
95
229
 
96
230
  class Site
97
- def initialize(
98
- root:,
99
- assets:,
100
- contents:,
101
- layouts:,
102
- view_context_helpers: [],
103
- guesses: Renderer.guesses,
104
- config: {}
105
- )
106
- @root = root
107
- @assets = assets
108
- @contents = contents
109
- @layouts = layouts
110
-
111
- @view_context_helpers = view_context_helpers
112
- @guessing_registry = guesses
113
-
114
- @config = {
115
- index: config.fetch(:index, "index.html")
116
- }
231
+ def initialize(config)
232
+ @root = config.root
233
+
234
+ yield(self)
117
235
 
118
- @output_root = root.concat(config.fetch(:destination_root, "public"))
119
- @assets_root = @output_root.concat(config.fetch(:assets_subdirectory, "assets"))
236
+ @view_context_helpers ||= []
237
+ @guesses ||= Rendering.guesses
120
238
 
121
239
  @route_identifiers = RouteIdentifiers.new
122
- @asset_blobs = Blobs.new(scoped_root: assets_path, file_abstraction_class: Asset)
123
- @content_blobs = Blobs.new(scoped_root: contents_path, file_abstraction_class: Content)
124
- @layout_blobs = Blobs.new(scoped_root: layouts_path, file_abstraction_class: Layout)
240
+ @asset_blobs = Blobs.new(scoped_root: config.source.assets_path, file_abstraction_class: Asset)
241
+ @content_blobs = Blobs.new(scoped_root: config.source.contents_path, file_abstraction_class: Content)
242
+ @layout_blobs = Blobs.new(scoped_root: config.source.layouts_path, file_abstraction_class: Layout)
125
243
  end
126
244
 
127
245
  attr_reader :root
128
- attr_reader :output_root
129
- attr_reader :assets_root
130
- attr_reader :config
131
246
 
132
247
  attr_reader :route_identifiers
133
248
  attr_reader :asset_blobs
134
249
  attr_reader :content_blobs
135
250
  attr_reader :layout_blobs
136
251
 
137
- attr_reader :view_context_helpers
138
- attr_reader :guessing_registry
252
+ attr_accessor :view_context_helpers
253
+ attr_accessor :guesses
139
254
 
140
- def build
255
+ def rules
141
256
  yield(
142
257
  assets: @asset_blobs,
143
258
  layouts: @layout_blobs,
@@ -145,7 +260,7 @@ module Confinement
145
260
  routes: @route_identifiers
146
261
  )
147
262
 
148
- guesser = Rendering::Guesser.new(guessing_registry)
263
+ guesser = Rendering::Guesser.new(guesses)
149
264
  guess_renderers(guesser, @layout_blobs)
150
265
  guess_renderers(guesser, @content_blobs)
151
266
 
@@ -157,6 +272,18 @@ module Confinement
157
272
  nil
158
273
  end
159
274
 
275
+ def partial_compilation
276
+ { asset_blobs: @asset_blobs }
277
+ end
278
+
279
+ def partial_compilation=(previous_partial_compilation)
280
+ return if previous_partial_compilation.nil?
281
+
282
+ @asset_blobs = previous_partial_compilation.fetch(:asset_blobs)
283
+
284
+ nil
285
+ end
286
+
160
287
  private
161
288
 
162
289
  def guess_renderers(guesser, blobs)
@@ -170,18 +297,6 @@ module Confinement
170
297
  end
171
298
  end
172
299
  end
173
-
174
- def contents_path
175
- @contents_path ||= @root.concat(@contents).cleanpath
176
- end
177
-
178
- def layouts_path
179
- @layouts_path ||= @root.concat(@layouts).cleanpath
180
- end
181
-
182
- def assets_path
183
- @assets_path ||= @root.concat(@assets).cleanpath
184
- end
185
300
  end
186
301
 
187
302
  # RouteIdentifiers is called such because it doesn't hold the actual
@@ -515,49 +630,50 @@ module Confinement
515
630
  end
516
631
 
517
632
  class Compiler
518
- def initialize(site)
519
- @site = site
520
- @lock = Mutex.new
633
+ def initialize(config)
634
+ @config = config
635
+ @logger = config.logger
521
636
  end
522
637
 
523
- attr_reader :site
524
-
525
- def compile_everything
526
- # All compilation happens inside the same lock. So we shouldn't have
527
- # to worry about deadlocks or anything
528
- @lock.synchronize do
529
- # Assets first since it's almost always a dependency of contents
530
- compile_assets(site.asset_blobs.send(:lookup))
531
- compile_contents(site.route_identifiers.send(:lookup).values)
532
- end
638
+ def compile_everything(site)
639
+ # Assets first since it's almost always a dependency of contents
640
+ compile_assets(site)
641
+ compile_contents(site)
533
642
  end
534
643
 
535
- private
536
-
537
644
  PARCEL_FILES_OUTPUT_REGEX = /^✨[^\n]+\n\n(.*)Done in(?:.*)\z/m
538
645
  PARCEL_FILE_OUTPUT_REGEX = /^(?<page>.*?)\s+(?<size>[0-9\.]+\s*[A-Z]?B)\s+(?<time>[0-9\.]+[a-z]?s)$/
539
646
 
540
- def compile_assets(asset_files)
647
+ def compile_assets(site)
648
+ @logger.info { "compiling assets" }
649
+ create_destination_directory
650
+ asset_files = site.asset_blobs.send(:lookup)
541
651
  asset_paths = asset_files.values
542
652
 
543
- out, status = Open3.capture2(
653
+ command = [
544
654
  "yarn",
545
655
  "run",
546
656
  "parcel",
547
657
  "build",
548
658
  "--no-cache",
549
- "--dist-dir", site.assets_root.to_s,
550
- "--public-url", site.assets_root.basename.to_s,
659
+ "--dist-dir", @config.compiler.output_assets_path.to_s,
660
+ "--public-url", @config.compiler.output_assets_path.basename.to_s,
551
661
  *asset_paths.select(&:entrypoint?).map(&:input_path).map(&:to_s)
552
- )
662
+ ]
663
+
664
+ @logger.debug { "running: #{command.join(" ")}" }
665
+
666
+ out, status = Open3.capture2(*command)
553
667
 
554
668
  if !status.success?
669
+ @logger.fatal { "asset compilation failed" }
555
670
  raise "Asset compilation failed"
556
671
  end
557
672
 
558
673
  matches = PARCEL_FILES_OUTPUT_REGEX.match(out)[1]
559
674
 
560
675
  if !matches
676
+ @logger.fatal { "asset compilation ouptut parsing failed" }
561
677
  raise "Asset compilation output parsing failed"
562
678
  end
563
679
 
@@ -566,30 +682,68 @@ module Confinement
566
682
  processed_file_paths.map do |file|
567
683
  output_file, *input_files = file.strip.split(/\n(?:└|├)── /)
568
684
 
569
- output_path = site.root.concat(output_file[PARCEL_FILE_OUTPUT_REGEX, 1])
685
+ output_path = @config.root.concat(output_file[PARCEL_FILE_OUTPUT_REGEX, 1])
570
686
 
571
687
  input_files.each do |input_file|
572
- input_path = site.root.concat(input_file[PARCEL_FILE_OUTPUT_REGEX, 1])
688
+ input_path = @config.root.concat(input_file[PARCEL_FILE_OUTPUT_REGEX, 1])
573
689
 
574
690
  if !asset_files.key?(input_path)
575
691
  next
576
692
  end
577
693
 
578
- url_path = output_path.relative_path_from(site.output_root)
694
+ url_path = output_path.relative_path_from(@config.compiler.output_root_path)
695
+ @logger.debug { "processesd asset: #{input_path}, #{url_path}, #{output_path}" }
579
696
  asset_files[input_path].url_path = url_path.to_s
580
697
  asset_files[input_path].output_path = output_path
581
698
  asset_files[input_path].body = output_path.read
582
699
  end
583
700
  end
701
+
702
+ @logger.info { "finished compiling assets" }
584
703
  end
585
704
 
586
- def compile_contents(contents)
705
+ def compile_contents(site)
706
+ @logger.info { "compiling contents" }
707
+ create_destination_directory
708
+ contents = site.route_identifiers.send(:lookup).values
587
709
  contents.each do |content|
588
- compile_content(content)
710
+ compile_content(site, content)
589
711
  end
712
+ @logger.info { "finished compiling contents" }
590
713
  end
591
714
 
592
- def compile_content(content)
715
+ def partial_compilation_dirty?(before:, after:)
716
+ return true if !before.key?(:asset_blobs)
717
+ return true if !after.key?(:asset_blobs)
718
+
719
+ before_assets = before[:asset_blobs].send(:lookup)
720
+ after_assets = after[:asset_blobs].send(:lookup)
721
+
722
+ return true if before_assets.keys.sort != after_assets.keys.sort
723
+ return true if before_assets.any? { |k, v| v.input_path != after_assets[k].input_path }
724
+ return true if before_assets.any? { |k, v| v.entrypoint? != after_assets[k].entrypoint? }
725
+
726
+ false
727
+ end
728
+
729
+ private
730
+
731
+ def create_destination_directory
732
+ destination = @config.compiler.output_root_path
733
+
734
+ if destination.exist?
735
+ return
736
+ end
737
+
738
+ if !destination.dirname.exist?
739
+ raise Error::PathDoesNotExist, "Destination's parent path does not exist: #{destination.dirname}"
740
+ end
741
+
742
+ destination.mkpath
743
+ end
744
+
745
+ def compile_content(site, content)
746
+ @logger.debug { "compiling content: #{content.input_path}, #{content.renderers}" }
593
747
  view_context = Rendering::ViewContext.new(
594
748
  routes: site.route_identifiers,
595
749
  layouts: site.layout_blobs,
@@ -607,9 +761,9 @@ module Confinement
607
761
 
608
762
  content.output_path =
609
763
  if content.url_path[-1] == "/"
610
- site.output_root.concat(content.url_path, site.config.fetch(:index))
764
+ @config.compiler.output_root_path.concat(content.url_path, @config.compiler.output_directory_index)
611
765
  else
612
- site.output_root.concat(content.url_path)
766
+ @config.compiler.output_root_path.concat(content.url_path)
613
767
  end
614
768
 
615
769
  if content.output_path.exist?
@@ -618,7 +772,7 @@ module Confinement
618
772
  end
619
773
  end
620
774
 
621
- if !site.output_root.include?(content.output_path)
775
+ if !@config.compiler.output_root_path.include?(content.output_path)
622
776
  return
623
777
  end
624
778
 
@@ -631,29 +785,4 @@ module Confinement
631
785
  nil
632
786
  end
633
787
  end
634
-
635
- class Publish
636
- def initialize(site)
637
- @site = site
638
- @compiler = Compiler.new(@site)
639
- end
640
-
641
- def write
642
- find_or_raise_or_mkdir(@site.output_root)
643
-
644
- @compiler.compile_everything
645
- end
646
-
647
- private
648
-
649
- def find_or_raise_or_mkdir(destination)
650
- if !destination.exist?
651
- if !destination.dirname.exist?
652
- raise Error::PathDoesNotExist, "Destination's parent path does not exist: #{destination.dirname}"
653
- end
654
-
655
- destination.mkpath
656
- end
657
- end
658
- end
659
788
  end
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 - 2018 Thomas Flemming
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'filewatcher/cycles'
4
+
5
+ # Simple file watcher. Detect changes in files and directories.
6
+ #
7
+ # Issues: Currently doesn't monitor changes in directorynames
8
+ class Filewatcher
9
+ include Filewatcher::Cycles
10
+
11
+ attr_accessor :interval
12
+ attr_reader :keep_watching
13
+
14
+ def update_spinner(label)
15
+ return unless @show_spinner
16
+ @spinner ||= %w[\\ | / -]
17
+ print "#{' ' * 30}\r#{label} #{@spinner.rotate!.first}\r"
18
+ end
19
+
20
+ def initialize(unexpanded_filenames, options = {})
21
+ @unexpanded_filenames = unexpanded_filenames
22
+ @unexpanded_excluded_filenames = options[:exclude]
23
+ @keep_watching = false
24
+ @pausing = false
25
+ @immediate = options[:immediate]
26
+ @show_spinner = options[:spinner]
27
+ @interval = options.fetch(:interval, 0.5)
28
+ end
29
+
30
+ def watch(&on_update)
31
+ @on_update = on_update
32
+ @keep_watching = true
33
+ yield('', '') if @immediate
34
+
35
+ main_cycle
36
+
37
+ @end_snapshot = mtime_snapshot
38
+ finalize(&on_update)
39
+ ensure
40
+ stop
41
+ end
42
+
43
+ def pause
44
+ @pausing = true
45
+ update_spinner('Initiating pause')
46
+ # Ensure we wait long enough to enter pause loop in #watch
47
+ sleep @interval
48
+ end
49
+
50
+ def resume
51
+ if !@keep_watching || !@pausing
52
+ raise "Can't resume unless #watch and #pause were first called"
53
+ end
54
+ @last_snapshot = mtime_snapshot # resume with fresh snapshot
55
+ @pausing = false
56
+ update_spinner('Resuming')
57
+ sleep @interval # Wait long enough to exit pause loop in #watch
58
+ end
59
+
60
+ # Ends the watch, allowing any remaining changes to be finalized.
61
+ # Used mainly in multi-threaded situations.
62
+ def stop
63
+ @keep_watching = false
64
+ update_spinner('Stopping')
65
+ nil
66
+ end
67
+
68
+ # Calls the update block repeatedly until all changes in the
69
+ # current snapshot are dealt with
70
+ def finalize(&on_update)
71
+ on_update = @on_update unless block_given?
72
+ while filesystem_updated?(@end_snapshot || mtime_snapshot)
73
+ update_spinner('Finalizing')
74
+ trigger_changes(on_update)
75
+ end
76
+ @end_snapshot = nil
77
+ end
78
+
79
+ def last_found_filenames
80
+ last_snapshot.keys
81
+ end
82
+
83
+ private
84
+
85
+ def last_snapshot
86
+ @last_snapshot ||= mtime_snapshot
87
+ end
88
+
89
+ # Takes a snapshot of the current status of watched files.
90
+ # (Allows avoidance of potential race condition during #finalize)
91
+ def mtime_snapshot
92
+ snapshot = {}
93
+ filenames = expand_directories(@unexpanded_filenames)
94
+
95
+ # Remove files in the exclude filenames list
96
+ filenames -= expand_directories(@unexpanded_excluded_filenames)
97
+
98
+ filenames.each do |filename|
99
+ mtime = File.exist?(filename) ? File.mtime(filename) : Time.new(0)
100
+ snapshot[filename] = mtime
101
+ end
102
+ snapshot
103
+ end
104
+
105
+ def filesystem_updated?(snapshot = mtime_snapshot)
106
+ @changes = {}
107
+
108
+ (snapshot.to_a - last_snapshot.to_a).each do |file, _mtime|
109
+ @changes[file] = last_snapshot[file] ? :updated : :created
110
+ end
111
+
112
+ (last_snapshot.keys - snapshot.keys).each do |file|
113
+ @changes[file] = :deleted
114
+ end
115
+
116
+ @last_snapshot = snapshot
117
+ @changes.any?
118
+ end
119
+
120
+ def expand_directories(patterns)
121
+ patterns = Array(patterns) unless patterns.is_a? Array
122
+ expanded_patterns = patterns.map do |pattern|
123
+ pattern = File.expand_path(pattern)
124
+ Dir[
125
+ File.directory?(pattern) ? File.join(pattern, '**', '*') : pattern
126
+ ]
127
+ end
128
+ expanded_patterns.flatten!
129
+ expanded_patterns.uniq!
130
+ expanded_patterns
131
+ end
132
+ end
133
+
134
+ # Require at end of file to not overwrite `Filewatcher` class
135
+ require_relative 'filewatcher/version'
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Filewatcher
4
+ # Module for all cycles in `Filewatcher#watch`
5
+ module Cycles
6
+ private
7
+
8
+ def main_cycle
9
+ while @keep_watching
10
+ @end_snapshot = mtime_snapshot if @pausing
11
+
12
+ pausing_cycle
13
+
14
+ watching_cycle
15
+
16
+ # test and clear @changes to prevent yielding the last
17
+ # changes twice if @keep_watching has just been set to false
18
+ trigger_changes
19
+ end
20
+ end
21
+
22
+ def pausing_cycle
23
+ while @keep_watching && @pausing
24
+ update_spinner('Pausing')
25
+ sleep @interval
26
+ end
27
+ end
28
+
29
+ def watching_cycle
30
+ while @keep_watching && !filesystem_updated? && !@pausing
31
+ update_spinner('Watching')
32
+ sleep @interval
33
+ end
34
+ end
35
+
36
+ def trigger_changes(on_update = @on_update)
37
+ thread = Thread.new do
38
+ on_update.call(@changes.dup)
39
+ @changes.clear
40
+ end
41
+ thread.join
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../filewatcher'
4
+
5
+ class Filewatcher
6
+ VERSION = '1.1.1'.freeze
7
+ end
@@ -1,3 +1,3 @@
1
1
  module Confinement
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: confinement
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zach Ahn
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-04-05 00:00:00.000000000 Z
11
+ date: 2020-04-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -53,7 +53,7 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '5.0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: pry-byebug
56
+ name: pry
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
@@ -80,6 +80,48 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: zeitwerk
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.3'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.3'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rack
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2.2'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.2'
111
+ - !ruby/object:Gem::Dependency
112
+ name: puma
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '4.3'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '4.3'
83
125
  description:
84
126
  email:
85
127
  - engineering@zachahn.com
@@ -102,6 +144,10 @@ files:
102
144
  - confinement.gemspec
103
145
  - exe/confinement
104
146
  - lib/confinement.rb
147
+ - lib/confinement/filewatcher/LICENSE
148
+ - lib/confinement/filewatcher/filewatcher.rb
149
+ - lib/confinement/filewatcher/filewatcher/cycles.rb
150
+ - lib/confinement/filewatcher/filewatcher/version.rb
105
151
  - lib/confinement/version.rb
106
152
  homepage: https://github.com/zachahn/confinement
107
153
  licenses: