vue_live 0.1.0

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.
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module VueLive
6
+ # Development live reload: a Server-Sent Events stream at <prefix>/-/events that announces
7
+ # changed components, and a small client (<prefix>/-/reload.js) that reloads the page.
8
+ #
9
+ # The stream polls the component directory's mtimes (config.live_reload_interval seconds) from
10
+ # the request's own thread, so it works with any threaded Rack server (Puma, Falcon, WEBrick)
11
+ # without extra processes or websockets. On Rack 3 it is a streaming body (`call(io)`), which
12
+ # servers write through unbuffered; on Rack 2 it is an `each` body.
13
+ class LiveReload
14
+ CLIENT = File.expand_path('assets/reload.js', __dir__)
15
+ HEARTBEAT = 5 # seconds between keep-alive comments (also how fast a gone client is noticed)
16
+
17
+ attr_reader :config
18
+
19
+ def initialize(config)
20
+ @config = config
21
+ end
22
+
23
+ def enabled?
24
+ config.live_reload?
25
+ end
26
+
27
+ # Rack response for the SSE stream.
28
+ def stream_response(_env = nil)
29
+ headers = {
30
+ 'content-type' => 'text/event-stream; charset=utf-8',
31
+ 'cache-control' => 'no-store',
32
+ 'x-accel-buffering' => 'no' # nginx: do not buffer
33
+ }
34
+ body = LiveReload.rack3? ? StreamingBody.new(config, self) : Stream.new(config, self)
35
+ [200, headers, body]
36
+ end
37
+
38
+ def client_response
39
+ body = File.read(CLIENT)
40
+ [200, { 'content-type' => 'text/javascript; charset=utf-8', 'cache-control' => 'no-cache',
41
+ 'content-length' => body.bytesize.to_s }, [body]]
42
+ end
43
+
44
+ # Snapshot of every servable file's mtime under the component root.
45
+ def snapshot
46
+ resolver = Resolver.new(config)
47
+ root = resolver.source_dir
48
+ resolver.files.each_with_object({}) do |rel, h|
49
+ h[rel] = File.mtime(File.join(root, rel)).to_f
50
+ rescue Errno::ENOENT
51
+ next
52
+ end
53
+ end
54
+
55
+ # Files whose mtime differs between two snapshots (added, removed or modified).
56
+ def self.diff(before, after)
57
+ (before.keys | after.keys).reject { |k| before[k] == after[k] }.sort
58
+ end
59
+
60
+ def self.rack3?
61
+ defined?(::Rack::RELEASE) && ::Rack::RELEASE.to_s.split('.').first.to_i >= 3
62
+ end
63
+
64
+ # Produces SSE frames until the consumer raises (client gone), +stop+ is called, or the
65
+ # stream reaches MAX_LIFETIME (the client reconnects at once; the cap keeps servers that join
66
+ # request threads on shutdown, such as WEBrick, from waiting on an idle tab forever).
67
+ class Frames
68
+ MAX_LIFETIME = 30 # seconds
69
+
70
+ def initialize(config, reloader)
71
+ @config = config
72
+ @reloader = reloader || LiveReload.new(config)
73
+ @stopped = false
74
+ end
75
+
76
+ def stop
77
+ @stopped = true
78
+ end
79
+
80
+ def each_frame
81
+ last = @reloader.snapshot # before the first frame, so nothing slips through the gap
82
+ yield "retry: 1000\n\n"
83
+ yield ": connected\n\n"
84
+ started = last_beat = Process.clock_gettime(Process::CLOCK_MONOTONIC)
85
+ until @stopped
86
+ sleep @config.live_reload_interval
87
+ current = @reloader.snapshot
88
+ changed = LiveReload.diff(last, current)
89
+ unless changed.empty?
90
+ VueLive.store.clear if @config.reload? # make sure the next request recompiles
91
+ yield "event: change\ndata: #{JSON.generate(files: changed)}\n\n"
92
+ last = current
93
+ end
94
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
95
+ break if now - started >= MAX_LIFETIME
96
+
97
+ if now - last_beat >= HEARTBEAT
98
+ yield ": ping\n\n"
99
+ last_beat = now
100
+ end
101
+ end
102
+ rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ENOTCONN, IOError
103
+ nil # client went away
104
+ end
105
+ end
106
+
107
+ # Rack 2 style body: the server pulls frames with #each.
108
+ class Stream < Frames
109
+ def each(&block)
110
+ each_frame(&block)
111
+ end
112
+
113
+ def close
114
+ stop
115
+ end
116
+ end
117
+
118
+ # Rack 3 streaming body: the server hands us the connection and we push frames into it.
119
+ # Deliberately has no #each, otherwise servers would buffer it as an ordinary body.
120
+ class StreamingBody < Frames
121
+ def call(io)
122
+ each_frame do |frame|
123
+ io.write(frame)
124
+ io.flush if io.respond_to?(:flush)
125
+ end
126
+ ensure
127
+ begin
128
+ io.close if io.respond_to?(:close)
129
+ rescue IOError, SystemCallError
130
+ nil
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ module VueLive
6
+ # Rack middleware answering requests under the configured prefix (default "/vue"):
7
+ #
8
+ # GET /vue/App.vue -> compiled ES module (also at /vue/App.vue.js)
9
+ # GET /vue/components/x.js -> any other allowed file under the component root, as-is
10
+ # GET /vue/-/vue.esm-browser.js -> a local copy of Vue when the project has one
11
+ # GET /vue/-/reload.js, /vue/-/events -> development live reload (client + SSE stream)
12
+ #
13
+ # Everything else is passed to the next app, so it can sit in front of Sprockets, Propshaft,
14
+ # Webpacker or ActionDispatch::Static without interfering with them.
15
+ #
16
+ # use VueLive::Middleware # uses VueLive.config
17
+ # use VueLive::Middleware, prefix: '/components', source_path: 'lib/vue'
18
+ class Middleware
19
+ MIME = {
20
+ '.vue' => 'text/javascript; charset=utf-8',
21
+ '.js' => 'text/javascript; charset=utf-8',
22
+ '.mjs' => 'text/javascript; charset=utf-8',
23
+ '.css' => 'text/css; charset=utf-8',
24
+ '.json' => 'application/json; charset=utf-8',
25
+ '.svg' => 'image/svg+xml',
26
+ '.png' => 'image/png',
27
+ '.jpg' => 'image/jpeg',
28
+ '.jpeg' => 'image/jpeg',
29
+ '.gif' => 'image/gif',
30
+ '.webp' => 'image/webp',
31
+ '.avif' => 'image/avif',
32
+ '.ico' => 'image/x-icon',
33
+ '.woff' => 'font/woff',
34
+ '.woff2' => 'font/woff2',
35
+ '.ttf' => 'font/ttf',
36
+ '.otf' => 'font/otf'
37
+ }.freeze
38
+
39
+ attr_reader :config
40
+
41
+ def initialize(app, options = {})
42
+ @app = app
43
+ options = options.dup
44
+ @config = if (cfg = options.delete(:config))
45
+ cfg
46
+ elsif options.empty?
47
+ VueLive.config
48
+ else
49
+ VueLive.config.dup.apply(options)
50
+ end
51
+ # Share the global store when running on the global config so VueLive.store.clear works.
52
+ @store = @config.equal?(VueLive.config) ? nil : Store.new(@config)
53
+ end
54
+
55
+ def store
56
+ @store || VueLive.store
57
+ end
58
+
59
+ def call(env)
60
+ path = env['PATH_INFO'].to_s
61
+ prefix = config.normalized_prefix
62
+ return @app.call(env) unless path.start_with?("#{prefix}/")
63
+ return method_not_allowed unless %w[GET HEAD].include?(env['REQUEST_METHOD'])
64
+
65
+ rest = path[(prefix.length + 1)..]
66
+ return serve_internal(env, rest) if rest.start_with?('-/')
67
+
68
+ target = store.resolver.resolve(rest)
69
+ return @app.call(env) unless target
70
+
71
+ target.vue? ? serve_component(env, target) : serve_file(env, target)
72
+ end
73
+
74
+ private
75
+
76
+ def serve_component(env, target)
77
+ compiled = store.fetch(target.relative_path)
78
+ return not_modified if fresh?(env, compiled.etag)
79
+
80
+ headers = {
81
+ 'content-type' => MIME['.vue'],
82
+ 'etag' => compiled.etag,
83
+ 'cache-control' => cache_control(env, compiled.digest),
84
+ 'x-vue-live' => compiled.backend.to_s
85
+ }
86
+ respond(env, 200, headers, compiled.code)
87
+ rescue CompileError => e
88
+ config.logger.error("[vue_live] #{e.message}")
89
+ if config.production?
90
+ respond(env, 500, { 'content-type' => 'text/plain; charset=utf-8', 'cache-control' => 'no-store' },
91
+ "vue_live: failed to compile #{target.relative_path}\n")
92
+ else
93
+ respond(env, 200, { 'content-type' => MIME['.vue'], 'cache-control' => 'no-store' }, error_module(e))
94
+ end
95
+ end
96
+
97
+ def serve_file(env, target)
98
+ stat = File.stat(target.absolute_path)
99
+ etag = %("#{Store.file_etag(stat)}")
100
+ return not_modified if fresh?(env, etag)
101
+
102
+ ext = File.extname(target.relative_path).downcase
103
+ headers = {
104
+ 'content-type' => MIME.fetch(ext, 'application/octet-stream'),
105
+ 'etag' => etag,
106
+ 'last-modified' => stat.mtime.httpdate,
107
+ 'cache-control' => cache_control(env, etag.delete('"'))
108
+ }
109
+ respond(env, 200, headers, File.binread(target.absolute_path))
110
+ end
111
+
112
+ # Internal endpoints under <prefix>/-/.
113
+ def serve_internal(env, rest)
114
+ case rest
115
+ when '-/vue.esm-browser.js' then serve_vendor(env)
116
+ when '-/reload.js' then live_reload.enabled? ? live_reload.client_response : @app.call(env)
117
+ when '-/events' then live_reload.enabled? ? live_reload.stream_response(env) : @app.call(env)
118
+ else @app.call(env)
119
+ end
120
+ end
121
+
122
+ def live_reload
123
+ @live_reload ||= LiveReload.new(config)
124
+ end
125
+
126
+ # /vue/-/vue.esm-browser.js : serve the project's own copy of Vue when there is one.
127
+ def serve_vendor(env)
128
+ return @app.call(env) unless (file = config.local_vue_file)
129
+
130
+ stat = File.stat(file)
131
+ etag = %("vue-#{stat.size.to_s(16)}-#{stat.mtime.to_i.to_s(16)}")
132
+ return not_modified if fresh?(env, etag)
133
+
134
+ headers = { 'content-type' => MIME['.js'], 'etag' => etag, 'cache-control' => cache_control(env, etag.delete('"')) }
135
+ respond(env, 200, headers, File.binread(file))
136
+ end
137
+
138
+ # Digested URLs (?v=<digest>) are immutable; plain URLs must revalidate.
139
+ def cache_control(env, digest)
140
+ query = env['QUERY_STRING'].to_s
141
+ if query.match?(/(?:\A|&)v=#{Regexp.escape(digest)}(?:&|\z)/) && !config.reload?
142
+ 'public, max-age=31536000, immutable'
143
+ else
144
+ 'no-cache'
145
+ end
146
+ end
147
+
148
+ def fresh?(env, etag)
149
+ inm = env['HTTP_IF_NONE_MATCH']
150
+ inm && inm.split(',').map(&:strip).include?(etag)
151
+ end
152
+
153
+ def not_modified
154
+ [304, { 'cache-control' => 'no-cache' }, []]
155
+ end
156
+
157
+ def method_not_allowed
158
+ [405, { 'content-type' => 'text/plain', 'allow' => 'GET, HEAD' }, ['Method Not Allowed']]
159
+ end
160
+
161
+ def respond(env, status, headers, body)
162
+ body = body.to_s
163
+ headers['content-length'] = body.bytesize.to_s
164
+ [status, headers, env['REQUEST_METHOD'] == 'HEAD' ? [] : [body]]
165
+ end
166
+
167
+ # In development a broken component becomes a module that throws a readable error, so the
168
+ # failure shows up in the browser console (and in Vue's error overlay if any) instead of a
169
+ # bare network error.
170
+ def error_module(error)
171
+ message = Emitter.js_string("[vue_live] #{error.message}")
172
+ <<~JS
173
+ const message = #{message}
174
+ console.error(message)
175
+ if (typeof document !== 'undefined') {
176
+ const pre = document.createElement('pre')
177
+ pre.setAttribute('data-vue-live-error', '')
178
+ pre.style.cssText = 'position:fixed;left:0;right:0;bottom:0;margin:0;padding:16px;max-height:50vh;overflow:auto;z-index:2147483647;background:#300;color:#fdd;font:13px/1.4 monospace;white-space:pre-wrap;'
179
+ pre.textContent = message
180
+ document.body ? document.body.appendChild(pre) : document.addEventListener('DOMContentLoaded', () => document.body.appendChild(pre))
181
+ }
182
+ throw new Error(message)
183
+ JS
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'json'
5
+
6
+ module VueLive
7
+ # Optional Node.js plumbing for the :node compiler backend.
8
+ #
9
+ # When the webpacker_cli gem is available its conventions are reused (package manager detection,
10
+ # project layout); otherwise a yarn.lock / package-lock.json sniff decides between yarn and npm.
11
+ module NodeTools
12
+ PACKAGES = ['@vue/compiler-sfc', 'vue'].freeze
13
+ # Node.js versions from here on strip TypeScript types themselves (node:module#stripTypeScriptTypes).
14
+ TYPE_STRIPPING_NODE = [22, 13].freeze
15
+ # npm packages compile.js can strip TypeScript with, when Node cannot.
16
+ TS_TRANSPILERS = %w[sucrase esbuild typescript @babel/core].freeze
17
+ DEFAULT_TS_TRANSPILER = 'sucrase'
18
+
19
+ module_function
20
+
21
+ def webpacker_cli?
22
+ return true if defined?(::WebpackerCli)
23
+
24
+ require 'webpacker_cli'
25
+ true
26
+ rescue LoadError
27
+ false
28
+ end
29
+
30
+ def node_version(config = VueLive.config)
31
+ out, status = Open3.capture2e(config.node_bin, '--version')
32
+ status.success? ? out.strip : nil
33
+ rescue Errno::ENOENT
34
+ nil
35
+ end
36
+
37
+ # true when the installed Node strips TypeScript on its own (>= 22.13).
38
+ def node_strips_types?(config = VueLive.config)
39
+ version = node_version(config)
40
+ return false unless version
41
+
42
+ major, minor = version.delete_prefix('v').split('.').map(&:to_i)
43
+ major > TYPE_STRIPPING_NODE[0] || (major == TYPE_STRIPPING_NODE[0] && minor >= TYPE_STRIPPING_NODE[1])
44
+ end
45
+
46
+ # The TypeScript transpiler packages installed under +root+.
47
+ def installed_ts_transpilers(root = VueLive.config.root)
48
+ TS_TRANSPILERS.select { |pkg| File.directory?(File.join(root, 'node_modules', *pkg.split('/'))) }
49
+ end
50
+
51
+ def package_manager(root = VueLive.config.root)
52
+ return ::WebpackerCli.package_manager(root) if webpacker_cli?
53
+ return 'npm' if File.exist?(File.join(root, 'package-lock.json')) && !File.exist?(File.join(root, 'yarn.lock'))
54
+ return 'yarn' if File.exist?(File.join(root, 'yarn.lock')) || which('yarn')
55
+
56
+ 'npm'
57
+ end
58
+
59
+ def compiler_sfc_version(root = VueLive.config.root)
60
+ JSON.parse(File.read(File.join(root, 'node_modules', '@vue', 'compiler-sfc', 'package.json')))['version']
61
+ rescue StandardError
62
+ nil
63
+ end
64
+
65
+ def vue_version(root = VueLive.config.root)
66
+ JSON.parse(File.read(File.join(root, 'node_modules', 'vue', 'package.json')))['version']
67
+ rescue StandardError
68
+ nil
69
+ end
70
+
71
+ # Install @vue/compiler-sfc (and vue, for a local copy of the browser build) into the project.
72
+ def setup(root = VueLive.config.root, packages: PACKAGES, dev: true)
73
+ raise Error, 'Node.js was not found in PATH; install it to use the :node compiler' unless node_version
74
+
75
+ pkg_json = File.join(root, 'package.json')
76
+ File.write(pkg_json, JSON.pretty_generate('name' => File.basename(root), 'private' => true)) unless File.exist?(pkg_json)
77
+ manager = package_manager(root)
78
+ cmd = if manager == 'yarn'
79
+ ['yarn', 'add', *(dev ? ['--dev'] : []), *packages]
80
+ else
81
+ ['npm', 'install', *(dev ? ['--save-dev'] : []), *packages]
82
+ end
83
+ puts " run #{cmd.join(' ')}"
84
+ success = system(*cmd, chdir: root)
85
+ raise Error, "#{manager} failed to install #{packages.join(', ')}" unless success
86
+
87
+ Compiler::Node.reset!
88
+ true
89
+ end
90
+
91
+ # Human-readable status for `vue-live check`.
92
+ def problems(config = VueLive.config)
93
+ problems = []
94
+ problems << "component directory does not exist: #{config.source_dir}" unless File.directory?(config.source_dir)
95
+ if %i[node].include?(config.compiler.to_sym)
96
+ problems << 'Node.js was not found in PATH (required by compiler: node)' unless node_version(config)
97
+ problems << '@vue/compiler-sfc is not installed (run `vue-live node-setup`)' unless compiler_sfc_version(config.root)
98
+ end
99
+ if %i[node auto].include?(config.compiler.to_sym) && node_version(config) && !node_strips_types?(config) &&
100
+ installed_ts_transpilers(config.root).empty?
101
+ problems << "Node.js #{node_version(config)} cannot strip TypeScript and no transpiler is installed; " \
102
+ '<script lang="ts"> components will fail (run `vue-live node-setup --with sucrase`)'
103
+ end
104
+ problems
105
+ end
106
+
107
+ def which(cmd)
108
+ ENV['PATH'].to_s.split(File::PATH_SEPARATOR).any? do |p|
109
+ f = File.join(p, cmd)
110
+ File.executable?(f) && !File.directory?(f)
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+
6
+ module VueLive
7
+ # Writes every component as a static `.vue.js` module (plus sibling assets and a manifest) so
8
+ # a CDN or plain file server can serve them. Optional: the middleware is the normal path.
9
+ #
10
+ # VueLive::Precompiler.new.run #=> public/vue/App.vue.js, public/vue/manifest.json
11
+ #
12
+ # The manifest maps "App.vue" => "/vue/App.vue.js?v=<digest>" and is used by vue_live_path when
13
+ # config.use_manifest? is true.
14
+ class Precompiler
15
+ attr_reader :config, :store, :errors
16
+
17
+ def initialize(config = VueLive.config, store: nil)
18
+ @config = config
19
+ @store = store || Store.new(config)
20
+ end
21
+
22
+ # Compiles everything it can. Components that fail are reported (and collected in #errors);
23
+ # pass strict: true to raise on the first failure instead.
24
+ def run(output: config.precompile_dir, quiet: false, strict: false, out: $stdout)
25
+ FileUtils.mkdir_p(output)
26
+ manifest = {}
27
+ @errors = errors = []
28
+ prefix = config.normalized_prefix
29
+
30
+ store.resolver.files.each do |rel|
31
+ src = File.join(store.resolver.source_dir, rel)
32
+ if rel.end_with?('.vue')
33
+ begin
34
+ compiled = store.fetch(rel)
35
+ rescue Error => e
36
+ errors << e
37
+ out.puts " FAIL #{rel}: #{e.message}" unless quiet
38
+ next
39
+ end
40
+ dest = File.join(output, "#{rel}.js")
41
+ write(dest, compiled.code)
42
+ manifest[rel] = "#{prefix}/#{rel}.js?v=#{compiled.digest}"
43
+ else
44
+ dest = File.join(output, rel)
45
+ FileUtils.mkdir_p(File.dirname(dest))
46
+ FileUtils.cp(src, dest)
47
+ manifest[rel] = "#{prefix}/#{rel}"
48
+ end
49
+ out.puts " write #{dest}" unless quiet
50
+ end
51
+
52
+ if (vue = config.local_vue_file)
53
+ dest = File.join(output, '-', 'vue.esm-browser.js')
54
+ FileUtils.mkdir_p(File.dirname(dest))
55
+ FileUtils.cp(vue, dest)
56
+ out.puts " write #{dest}" unless quiet
57
+ end
58
+
59
+ write(File.join(output, 'manifest.json'), JSON.pretty_generate(manifest))
60
+ raise errors.first if strict && errors.any?
61
+
62
+ manifest
63
+ end
64
+
65
+ def clobber(output: config.precompile_dir)
66
+ FileUtils.rm_rf(output)
67
+ end
68
+
69
+ def self.manifest(config = VueLive.config)
70
+ @manifests ||= {}
71
+ path = config.manifest_path
72
+ return @manifests[path] if @manifests[path] && !config.reload?
73
+
74
+ @manifests[path] = File.file?(path) ? JSON.parse(File.read(path)) : {}
75
+ end
76
+
77
+ def self.reset!
78
+ @manifests = {}
79
+ end
80
+
81
+ private
82
+
83
+ def write(dest, content)
84
+ FileUtils.mkdir_p(File.dirname(dest))
85
+ File.write(dest, content)
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VueLive
4
+ module Rails
5
+ # ActionView flavour of VueLive::Helpers: html_safe output, automatic CSP nonces and
6
+ # cooperation with importmap-rails (browsers honour only one import map per page).
7
+ module Helper
8
+ include VueLive::Helpers
9
+
10
+ def vue_live_import_map_tag(imports: {}, nonce: nil)
11
+ super(imports: imports, nonce: nonce || vue_live_csp_nonce).html_safe
12
+ end
13
+
14
+ def vue_live_mount_tag(component, selector = '#app', nonce: nil, **options)
15
+ super(component, selector, nonce: nonce || vue_live_csp_nonce, **options).html_safe
16
+ end
17
+
18
+ def vue_live_module_tag(javascript, nonce: nil)
19
+ super(javascript, nonce: nonce || vue_live_csp_nonce).html_safe
20
+ end
21
+
22
+ def vue_live_tags(component, selector = '#app', **options)
23
+ super.html_safe
24
+ end
25
+
26
+ def vue_live_reload_tag(nonce: nil)
27
+ super(nonce: nonce || vue_live_csp_nonce).html_safe
28
+ end
29
+
30
+ private
31
+
32
+ def vue_live_csp_nonce
33
+ respond_to?(:content_security_policy_nonce, true) ? content_security_policy_nonce : nil
34
+ end
35
+
36
+ # importmap-rails owns the page's import map; the Railtie pins `vue` into it instead.
37
+ # Set config.vue_live.importmap_pin = false to keep the two separate.
38
+ def vue_live_external_importmap?
39
+ defined?(::Importmap::Map) && VueLive.config.importmap_pin ? true : false
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/railtie'
4
+ require_relative '../vue_live'
5
+ require_relative 'rails/helper'
6
+
7
+ module VueLive
8
+ # Wires vue_live into a Rails application with no configuration:
9
+ #
10
+ # * `config.vue_live` exposes every Configuration option (config.vue_live.prefix = '/components')
11
+ # * config/vue_live.yml is read when present (`rails g vue_live:install` writes one)
12
+ # * the middleware is mounted ahead of the asset pipeline's static file server, answering
13
+ # only `/vue/...`, so Sprockets, Propshaft, Webpacker and jsbundling keep their own paths
14
+ # * view helpers are added to ActionView
15
+ # * when importmap-rails is present, `vue` is pinned into the app's import map
16
+ # * rake tasks: vue_live:precompile, vue_live:clobber, vue_live:check, vue_live:node_setup
17
+ class Railtie < ::Rails::Railtie
18
+ config.vue_live = ActiveSupport::OrderedOptions.new
19
+
20
+ # Make `Rails.root`, `Rails.env` and `Rails.logger` the source of truth before anything reads them.
21
+ initializer 'vue_live.configure', before: :load_environment_config do |app|
22
+ VueLive.config.root = app.root.to_s
23
+ VueLive.config.env = ::Rails.env.to_s
24
+ VueLive.config.logger = ::Rails.logger if ::Rails.logger
25
+ end
26
+
27
+ initializer 'vue_live.apply_configuration', after: :load_environment_config do |app|
28
+ VueLive.config.root = app.root.to_s
29
+ VueLive.config.env = ::Rails.env.to_s
30
+ VueLive.load_config_file
31
+ VueLive.config.apply(app.config.vue_live.to_h)
32
+ VueLive.reset_store!
33
+ end
34
+
35
+ initializer 'vue_live.middleware' do |app|
36
+ app.config.after_initialize do
37
+ VueLive.config.logger ||= ::Rails.logger
38
+ end
39
+ next unless app.config.vue_live.fetch(:middleware, true)
40
+
41
+ if defined?(::ActionDispatch::Static) && app.middleware.respond_to?(:insert_before)
42
+ begin
43
+ app.middleware.insert_before ::ActionDispatch::Static, VueLive::Middleware
44
+ rescue StandardError
45
+ app.middleware.use VueLive::Middleware
46
+ end
47
+ else
48
+ app.middleware.use VueLive::Middleware
49
+ end
50
+ end
51
+
52
+ initializer 'vue_live.helpers' do
53
+ ActiveSupport.on_load(:action_view) { include VueLive::Rails::Helper }
54
+ end
55
+
56
+ initializer 'vue_live.importmap', after: 'importmap' do |app|
57
+ next unless defined?(::Importmap) && app.respond_to?(:importmap)
58
+
59
+ app.config.after_initialize do
60
+ next unless VueLive.config.importmap_pin
61
+
62
+ map = app.importmap
63
+ next if map.packages.key?('vue')
64
+
65
+ map.draw { pin 'vue', to: VueLive.config.resolved_vue_url, preload: true }
66
+ end
67
+ end
68
+
69
+ rake_tasks do
70
+ load File.expand_path('tasks.rake', __dir__)
71
+ end
72
+
73
+ generators do
74
+ require_relative '../generators/vue_live/install/install_generator'
75
+ end
76
+ end
77
+ end