frontman-ssg 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (141) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +42 -0
  3. data/.github/CODE_OF_CONDUCT.md +9 -0
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +25 -0
  5. data/.github/ISSUE_TEMPLATE/feature_request.md +22 -0
  6. data/.github/PULL_REQUEST_TEMPLATE.md +22 -0
  7. data/.gitignore +5 -0
  8. data/.rubocop.yml +88 -0
  9. data/CHANGELOG.md +11 -0
  10. data/CONTRIBUTING.md +42 -0
  11. data/Gemfile +5 -0
  12. data/LICENSE.md +21 -0
  13. data/Rakefile +94 -0
  14. data/SECURITY.md +6 -0
  15. data/bin/frontman +6 -0
  16. data/frontman-ssg.gemspec +48 -0
  17. data/frontman.svg +2 -0
  18. data/lib/frontman.rb +15 -0
  19. data/lib/frontman/app.rb +175 -0
  20. data/lib/frontman/bootstrapper.rb +70 -0
  21. data/lib/frontman/builder/asset_pipeline.rb +55 -0
  22. data/lib/frontman/builder/builder.rb +193 -0
  23. data/lib/frontman/builder/file.rb +55 -0
  24. data/lib/frontman/builder/mapping.rb +54 -0
  25. data/lib/frontman/builder/statistics_collector.rb +37 -0
  26. data/lib/frontman/cli.rb +6 -0
  27. data/lib/frontman/commands/build.rb +76 -0
  28. data/lib/frontman/commands/init.rb +58 -0
  29. data/lib/frontman/commands/serve.rb +110 -0
  30. data/lib/frontman/concerns/dispatch_events.rb +56 -0
  31. data/lib/frontman/concerns/forward_calls_to_app.rb +28 -0
  32. data/lib/frontman/config.rb +52 -0
  33. data/lib/frontman/context.rb +125 -0
  34. data/lib/frontman/custom_struct.rb +44 -0
  35. data/lib/frontman/data_store.rb +106 -0
  36. data/lib/frontman/data_store_file.rb +60 -0
  37. data/lib/frontman/helpers/app_helper.rb +18 -0
  38. data/lib/frontman/helpers/link_helper.rb +35 -0
  39. data/lib/frontman/helpers/render_helper.rb +76 -0
  40. data/lib/frontman/helpers/url_helper.rb +11 -0
  41. data/lib/frontman/iterator.rb +48 -0
  42. data/lib/frontman/process/chain.rb +43 -0
  43. data/lib/frontman/process/processor.rb +11 -0
  44. data/lib/frontman/renderers/erb_renderer.rb +21 -0
  45. data/lib/frontman/renderers/haml_renderer.rb +22 -0
  46. data/lib/frontman/renderers/markdown_renderer.rb +26 -0
  47. data/lib/frontman/renderers/renderer.rb +26 -0
  48. data/lib/frontman/renderers/renderer_resolver.rb +26 -0
  49. data/lib/frontman/resource.rb +279 -0
  50. data/lib/frontman/sitemap_tree.rb +211 -0
  51. data/lib/frontman/toolbox/timer.rb +49 -0
  52. data/lib/frontman/version.rb +6 -0
  53. data/project-templates/default/.gitignore +2 -0
  54. data/project-templates/default/Gemfile +3 -0
  55. data/project-templates/default/config.rb +17 -0
  56. data/project-templates/default/data/site.yml +4 -0
  57. data/project-templates/default/helpers/site_helper.rb +7 -0
  58. data/project-templates/default/public/code.css +77 -0
  59. data/project-templates/default/public/frontman-logo.svg +2 -0
  60. data/project-templates/default/public/main.css +27 -0
  61. data/project-templates/default/public/main.js +1 -0
  62. data/project-templates/default/source/index.html.md.erb +7 -0
  63. data/project-templates/default/source/sitemap.xml.erb +11 -0
  64. data/project-templates/default/views/layouts/main.erb +19 -0
  65. data/project-templates/default/views/layouts/main.haml +15 -0
  66. data/project-templates/default/views/partials/menu.erb +7 -0
  67. data/project-templates/webpack/.gitignore +4 -0
  68. data/project-templates/webpack/Gemfile +3 -0
  69. data/project-templates/webpack/README.md +54 -0
  70. data/project-templates/webpack/assets/css/code.css +77 -0
  71. data/project-templates/webpack/assets/css/style.css +27 -0
  72. data/project-templates/webpack/assets/images/.gitkeep +0 -0
  73. data/project-templates/webpack/assets/images/frontman_logo.svg +2 -0
  74. data/project-templates/webpack/assets/js/index.js +1 -0
  75. data/project-templates/webpack/config.rb +24 -0
  76. data/project-templates/webpack/data/site.yml +4 -0
  77. data/project-templates/webpack/helpers/assets_helper.rb +24 -0
  78. data/project-templates/webpack/helpers/site_helper.rb +7 -0
  79. data/project-templates/webpack/package-lock.json +7603 -0
  80. data/project-templates/webpack/package.json +34 -0
  81. data/project-templates/webpack/source/index.html.md.erb +7 -0
  82. data/project-templates/webpack/source/sitemap.xml.erb +11 -0
  83. data/project-templates/webpack/views/layouts/main.erb +20 -0
  84. data/project-templates/webpack/views/layouts/main.haml +14 -0
  85. data/project-templates/webpack/views/partials/menu.erb +7 -0
  86. data/project-templates/webpack/views/partials/script_with_vendors.haml +5 -0
  87. data/project-templates/webpack/webpack/base.config.js +51 -0
  88. data/project-templates/webpack/webpack/dev.config.js +6 -0
  89. data/project-templates/webpack/webpack/prod.config.js +30 -0
  90. data/readme.md +80 -0
  91. data/sorbet/config +2 -0
  92. data/sorbet/rbi/hidden-definitions/errors.txt +27259 -0
  93. data/sorbet/rbi/hidden-definitions/hidden.rbi +45122 -0
  94. data/sorbet/rbi/sorbet-typed/lib/rainbow/all/rainbow.rbi +276 -0
  95. data/sorbet/rbi/todo.rbi +6 -0
  96. data/spec/frontman/app_spec.rb +48 -0
  97. data/spec/frontman/bootstrapper_spec.rb +26 -0
  98. data/spec/frontman/builder/builder_spec.rb +79 -0
  99. data/spec/frontman/builder/file_spec.rb +45 -0
  100. data/spec/frontman/builder/mapping_spec.rb +8 -0
  101. data/spec/frontman/concerns/dispatch_events_spec.rb +70 -0
  102. data/spec/frontman/concerns/forward_calls_to_app_spec.rb +21 -0
  103. data/spec/frontman/config_spec.rb +54 -0
  104. data/spec/frontman/context_spec.rb +48 -0
  105. data/spec/frontman/custom_struct_spec.rb +51 -0
  106. data/spec/frontman/data_store_file_spec.rb +9 -0
  107. data/spec/frontman/data_store_spec.rb +36 -0
  108. data/spec/frontman/frontman_ssg_spec.rb +7 -0
  109. data/spec/frontman/helpers/app_helper_spec.rb +24 -0
  110. data/spec/frontman/helpers/link_helper_spec.rb +37 -0
  111. data/spec/frontman/helpers/render_helper_spec.rb +55 -0
  112. data/spec/frontman/helpers/url_helper_spec.rb +21 -0
  113. data/spec/frontman/iterator_spec.rb +47 -0
  114. data/spec/frontman/mocks/asset.css +3 -0
  115. data/spec/frontman/mocks/config.rb +0 -0
  116. data/spec/frontman/mocks/helpers/formatting_helper.rb +5 -0
  117. data/spec/frontman/mocks/helpers/language_helper.rb +5 -0
  118. data/spec/frontman/mocks/helpers/link_helper.rb +5 -0
  119. data/spec/frontman/mocks/helpers/test_command.rb +5 -0
  120. data/spec/frontman/mocks/html_file.html +8 -0
  121. data/spec/frontman/mocks/html_file.html.md.erb +9 -0
  122. data/spec/frontman/mocks/html_file.md.html +8 -0
  123. data/spec/frontman/mocks/info.yml +4 -0
  124. data/spec/frontman/mocks/layouts/raw_without_body.haml +1 -0
  125. data/spec/frontman/mocks/nested/data.yml +4 -0
  126. data/spec/frontman/mocks/nested/more_data.yml +4 -0
  127. data/spec/frontman/mocks/partials/paragraph.haml +2 -0
  128. data/spec/frontman/mocks/snippet/html_file.html +8 -0
  129. data/spec/frontman/mocks/snippet/html_file.yml +670 -0
  130. data/spec/frontman/mocks/test.html +8 -0
  131. data/spec/frontman/mocks/wrap.haml +3 -0
  132. data/spec/frontman/process/chain_spec.rb +56 -0
  133. data/spec/frontman/renderers/erb_renderer_spec.rb +22 -0
  134. data/spec/frontman/renderers/haml_renderer_spec.rb +12 -0
  135. data/spec/frontman/renderers/markdown_renderer_spec.rb +12 -0
  136. data/spec/frontman/renderers/renderer_spec.rb +16 -0
  137. data/spec/frontman/resource_spec.rb +151 -0
  138. data/spec/frontman/sitemap_tree_spec.rb +128 -0
  139. data/spec/frontman/toolbox/timer_spec.rb +34 -0
  140. data/spec/spec_setup.rb +19 -0
  141. metadata +507 -0
@@ -0,0 +1,55 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'sorbet-runtime'
5
+
6
+ module Frontman
7
+ module Builder
8
+ class File
9
+ extend T::Sig
10
+
11
+ attr_accessor :path, :status
12
+
13
+ sig { params(path: String, status: T.any(String, Symbol)).void }
14
+ def initialize(path, status)
15
+ unless valid_status?(status)
16
+ raise "#{status} is not a valid file status!"
17
+ end
18
+
19
+ @path = path
20
+ @status = status.to_sym
21
+ end
22
+
23
+ class << self
24
+ extend T::Sig
25
+
26
+ sig { params(path: String).returns(Frontman::Builder::File) }
27
+ def unchanged(path)
28
+ new(path, :unchanged)
29
+ end
30
+
31
+ sig { params(path: String).returns(Frontman::Builder::File) }
32
+ def deleted(path)
33
+ new(path, :deleted)
34
+ end
35
+
36
+ sig { params(path: String).returns(Frontman::Builder::File) }
37
+ def created(path)
38
+ new(path, :created)
39
+ end
40
+
41
+ sig { params(path: String).returns(Frontman::Builder::File) }
42
+ def updated(path)
43
+ new(path, :updated)
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ sig { params(status: T.any(String, Symbol)).returns(T::Boolean) }
50
+ def valid_status?(status)
51
+ %i[updated deleted created unchanged].include?(status.to_sym)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,54 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'fileutils'
5
+ require 'json'
6
+ require 'sorbet-runtime'
7
+
8
+ module Frontman
9
+ module Builder
10
+ class Mapping
11
+ extend T::Sig
12
+
13
+ attr_reader :output_path
14
+
15
+ sig { params(output_path: String).void }
16
+ def initialize(output_path)
17
+ @output_path = output_path
18
+ @mapping = {
19
+ updated: [],
20
+ created: [],
21
+ unchanged: [],
22
+ deleted: []
23
+ }
24
+ end
25
+
26
+ sig { returns(T::Hash[T.any(String, Symbol), T::Array[String]]) }
27
+ def all
28
+ @mapping
29
+ end
30
+
31
+ sig { params(build_file: Frontman::Builder::File).void }
32
+ def add_from_build_file(build_file)
33
+ add(build_file.status, build_file.path)
34
+ end
35
+
36
+ sig { params(status: T.any(String, Symbol), path: String).void }
37
+ def add(status, path)
38
+ @mapping[status.to_sym].push(path)
39
+ end
40
+
41
+ sig { void }
42
+ def save_file
43
+ ::File.open(output_path, 'w') do |f|
44
+ f.write(JSON.pretty_generate(@mapping))
45
+ end
46
+ end
47
+
48
+ sig { void }
49
+ def delete_file
50
+ FileUtils.rm_f(output_path)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,37 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'frontman/builder/builder'
5
+ require 'frontman/builder/mapping'
6
+ require 'frontman/toolbox/timer'
7
+ require 'sorbet-runtime'
8
+
9
+ module Frontman
10
+ module Builder
11
+ class StatisticsCollector
12
+ extend T::Sig
13
+
14
+ sig do
15
+ params(
16
+ builder: Frontman::Builder::Builder,
17
+ mapping: Frontman::Builder::Mapping,
18
+ timer: Frontman::Toolbox::Timer,
19
+ new_files: T::Array[String]
20
+ ).void
21
+ end
22
+ def self.output(builder, mapping, timer, new_files = [])
23
+ puts JSON.pretty_generate(mapping.all)
24
+ puts '================================================================='
25
+ puts "Previous build size : #{builder.current_build_files.size} files"
26
+ puts "Current build size : #{new_files.size} files"
27
+
28
+ %i[updated created deleted unchanged].each do |status|
29
+ puts "#{status} : #{Array(mapping.all[status]).length} files"
30
+ end
31
+
32
+ timer.stop
33
+ puts timer.output
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,6 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'frontman/commands/serve'
5
+ require 'frontman/commands/build'
6
+ require 'frontman/commands/init'
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: false
2
+
3
+ require 'thor'
4
+ require 'frontman/app'
5
+ require 'frontman/bootstrapper'
6
+ require 'frontman/builder/asset_pipeline'
7
+ require 'frontman/builder/builder'
8
+ require 'frontman/builder/mapping'
9
+ require 'frontman/builder/statistics_collector'
10
+ require 'frontman/config'
11
+ require 'frontman/toolbox/timer'
12
+ require 'frontman/sitemap_tree'
13
+
14
+ module Frontman
15
+ class CLI < Thor
16
+ option :parallel, type: :boolean
17
+ option :verbose, type: :boolean
18
+ desc 'build', 'Generate the HTML for your website'
19
+ def build
20
+ Frontman::Config.set(:mode, 'build')
21
+ Frontman::Bootstrapper.bootstrap_app(Frontman::App.instance)
22
+
23
+ assets_pipeline = Frontman::Builder::AssetPipeline.new(
24
+ Frontman::App.instance
25
+ .asset_pipelines
26
+ .filter { |p| %i[all build].include?(p[:mode]) }
27
+ )
28
+
29
+ assets_pipeline.run!(:before)
30
+
31
+ enable_parallel = options[:parallel]
32
+
33
+ Frontman::Config.set(:parallel, enable_parallel)
34
+
35
+ timer = Frontman::Toolbox::Timer.start
36
+
37
+ current_build_files = Dir.glob(Dir.pwd + '/build/**/*').reject do |f|
38
+ File.directory? f
39
+ end
40
+
41
+ public_dir = Frontman::Config.get(:public_dir, fallback: 'public/')
42
+ assets_to_build = Dir.glob(File.join(public_dir, '**/*')).reject do |f|
43
+ File.directory? f
44
+ end
45
+
46
+ mapping_path = Dir.pwd + '/_build.json'
47
+ mapping = Frontman::Builder::Mapping.new(mapping_path)
48
+ mapping.delete_file
49
+
50
+ build_directory = Dir.pwd + '/build/'
51
+ builder = Frontman::Builder::Builder.new
52
+ builder.build_directory = build_directory
53
+ builder.current_build_files = current_build_files
54
+
55
+ builder.on('created, updated, deleted, unchanged', lambda { |build_file|
56
+ mapping.add_from_build_file(build_file)
57
+ })
58
+
59
+ assets = builder.build_assets(assets_to_build)
60
+ redirects = builder.build_redirects
61
+
62
+ resources_paths = builder.build_from_resources(
63
+ Frontman::SitemapTree.resources
64
+ )
65
+
66
+ new_files = assets + redirects + resources_paths
67
+
68
+ builder.delete_files(current_build_files - new_files)
69
+ mapping.save_file
70
+
71
+ assets_pipeline.run!(:after)
72
+
73
+ Builder::StatisticsCollector.output(builder, mapping, timer, new_files)
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: false
2
+
3
+ require 'fileutils'
4
+ require 'thor'
5
+
6
+ module Frontman
7
+ class CLI < Thor
8
+ option :template
9
+ desc 'init', 'Bootstrap a new Frontman project'
10
+ def init(path)
11
+ template = options[:template] || 'default'
12
+ unless template_exists?(template)
13
+ raise "Template #{template} does not exist!"
14
+ end
15
+
16
+ target_dir = File.join(Dir.pwd, path == '.' ? '' : path)
17
+
18
+ unless allowed_to_modify_dir?(target_dir)
19
+ say 'Not bootstrapping new Frontman project'
20
+ return
21
+ end
22
+
23
+ copy_template(template, target_dir)
24
+
25
+ command = path == '.' ? '' : "cd #{path} && "
26
+ command += 'bundle exec frontman serve'
27
+
28
+ say "Your project is ready. Run `#{command}` and start developing!"
29
+ end
30
+
31
+ private
32
+
33
+ def copy_template(template, dest)
34
+ FileUtils.cp_r(
35
+ "#{path_to_template(template)}/.",
36
+ dest
37
+ )
38
+ end
39
+
40
+ def allowed_to_modify_dir?(dir)
41
+ return true if !Dir.exist?(dir) || Dir.empty?(dir)
42
+
43
+ say 'This folder already contains files. '
44
+ say 'Initializing a new Frontman project here may override these files.'
45
+ answer = ask('Are you sure you want to continue? [y/N]')
46
+
47
+ answer.to_s.downcase == 'y'
48
+ end
49
+
50
+ def template_exists?(template)
51
+ Dir.exist?(path_to_template(template))
52
+ end
53
+
54
+ def path_to_template(template)
55
+ File.join(__dir__, '../../../project-templates', template)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: false
2
+
3
+ require 'thor'
4
+ require 'sinatra/base'
5
+ require 'better_errors'
6
+ require 'listen'
7
+ require 'frontman/app'
8
+ require 'frontman/bootstrapper'
9
+ require 'frontman/builder/asset_pipeline'
10
+ require 'frontman/config'
11
+ require 'frontman/resource'
12
+
13
+ module Frontman
14
+ class CLI < Thor
15
+ desc 'serve', 'Serve your application'
16
+ def serve
17
+ Frontman::Config.set(:mode, 'serve')
18
+ app = Frontman::App.instance
19
+ Frontman::Bootstrapper.bootstrap_app(app)
20
+
21
+ assets_pipeline = Frontman::Builder::AssetPipeline.new(
22
+ app
23
+ .asset_pipelines
24
+ .filter { |p| %i[all serve].include?(p[:mode]) }
25
+ )
26
+ processes = assets_pipeline.run_in_background!(:before)
27
+
28
+ helpers_dir = Frontman::Config.get(:helpers_dir, fallback: 'helpers')
29
+ content_dir = Frontman::Config.get(:content_dir, fallback: 'source/')
30
+ listen_to_dirs = Frontman::Config.get(:observe_dirs, fallback:
31
+ [
32
+ Frontman::Config.get(:layout_dir, fallback: 'views/layouts'),
33
+ Frontman::Config.get(:partial_dir, fallback: 'views/partials'),
34
+ content_dir,
35
+ helpers_dir
36
+ ]).filter { |dir| Dir.exist?(dir) }
37
+ app.refresh_data_files = true
38
+
39
+ listener = Listen.to(*listen_to_dirs) do |modified, added|
40
+ (added + modified).each do |m|
41
+ resource_path = m.sub("#{Dir.pwd}/", '')
42
+ begin
43
+ if resource_path.start_with?(helpers_dir)
44
+ helper_name = File.basename(resource_path).gsub('.rb', '')
45
+ load("./#{resource_path}")
46
+ app.register_helpers(
47
+ [{
48
+ path: File.join(Dir.pwd, resource_path),
49
+ name: helper_name.split('_').collect(&:capitalize).join
50
+ }]
51
+ )
52
+ elsif resource_path.start_with?(*listen_to_dirs)
53
+ r = Frontman::Resource.from_path(resource_path)
54
+
55
+ if resource_path.start_with?(content_dir)
56
+ exists = app.sitemap_tree.from_resource(r)
57
+ app.sitemap_tree.add(r) unless exists
58
+ end
59
+
60
+ r&.parse_resource(true)
61
+ elsif resource_path.end_with?('.rb')
62
+ load("./#{resource_path}")
63
+ end
64
+ rescue Error
65
+ # We ignore all errors to prevent the listener from crashing.
66
+ # Errors will be surfaced by the server instead.
67
+ end
68
+ end
69
+ end
70
+
71
+ listener.start
72
+
73
+ FrontManServer.set :public_folder, Frontman::Config.get(
74
+ :public_dir, fallback: 'public'
75
+ )
76
+ FrontManServer.run! do
77
+ host = "http://localhost:#{FrontManServer.settings.port}"
78
+ print "== View your site at \"#{host}/\"\n"
79
+ processes += assets_pipeline.run_in_background!(:after)
80
+ at_exit { processes.each { |pid| Process.kill(0, pid) } }
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ class FrontManServer < Sinatra::Base
87
+ set :port, 4568
88
+ set :server_settings,
89
+ # Avoid having webrick displaying logs for every requests to the serve
90
+ AccessLog: [],
91
+ # Remove logger for WebRick, we have the one of sinatra already
92
+ Logger: Rack::NullLogger.new(self)
93
+
94
+ use BetterErrors::Middleware
95
+ BetterErrors.application_root = Dir.pwd
96
+
97
+ get '*' do |path|
98
+ app = Frontman::App.instance
99
+ return redirect to app.get_redirect(path), 302 if app.get_redirect(path)
100
+
101
+ tree = app.sitemap_tree.from_url(path)
102
+ if tree&.resource
103
+ extension = File.extname(tree.resource.destination_path)
104
+ headers['Content-Type'] = Rack::Mime.mime_type(extension)
105
+ tree.resource.render
106
+ else
107
+ halt 404
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,56 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'sorbet-runtime'
5
+
6
+ module Frontman
7
+ module DispatchEvents
8
+ extend T::Sig
9
+
10
+ sig { returns(T::Hash[Symbol, T::Array[T.untyped]]) }
11
+ def listeners
12
+ @listeners ||= {}
13
+ end
14
+
15
+ sig do
16
+ params(events: T.any(Symbol, String), callback: T.untyped)
17
+ .returns(T.self_type)
18
+ end
19
+ def on(events, callback)
20
+ list(events).each do |event_name|
21
+ listeners[event_name.to_sym] ||= []
22
+ T.must(listeners[event_name.to_sym]).push(callback)
23
+ end
24
+
25
+ self
26
+ end
27
+
28
+ # We don't annotate with sig because of bad support for splat arguments
29
+ def emit(events, *arguments)
30
+ list(events).each do |event_name|
31
+ event_listeners = listeners[event_name.to_sym] || []
32
+ event_listeners.each do |listener|
33
+ listener.call(*arguments)
34
+ end
35
+ end
36
+
37
+ self
38
+ end
39
+
40
+ sig { params(events: T.any(Symbol, String)).returns(T.self_type) }
41
+ def off(events)
42
+ list(events).each do |event_name|
43
+ listeners[event_name.to_sym] = []
44
+ end
45
+
46
+ self
47
+ end
48
+
49
+ private
50
+
51
+ sig { params(events: T.any(Symbol, String)).returns(T::Array[String]) }
52
+ def list(events)
53
+ events.to_s.split(',').map(&:strip)
54
+ end
55
+ end
56
+ end