confinement 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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: