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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +28 -0
- data/LICENSE-APACHE +201 -0
- data/LICENSE-MIT +21 -0
- data/README.md +464 -0
- data/exe/vue-live +7 -0
- data/lib/generators/vue_live/install/install_generator.rb +50 -0
- data/lib/vue_live/assets/reload.js +32 -0
- data/lib/vue_live/cache.rb +111 -0
- data/lib/vue_live/cli.rb +161 -0
- data/lib/vue_live/compiler/node/compile.js +294 -0
- data/lib/vue_live/compiler/node.rb +216 -0
- data/lib/vue_live/compiler/ruby.rb +104 -0
- data/lib/vue_live/compiler.rb +93 -0
- data/lib/vue_live/configuration.rb +219 -0
- data/lib/vue_live/emitter.rb +72 -0
- data/lib/vue_live/errors.rb +22 -0
- data/lib/vue_live/helpers.rb +118 -0
- data/lib/vue_live/live_reload.rb +135 -0
- data/lib/vue_live/middleware.rb +186 -0
- data/lib/vue_live/node_tools.rb +114 -0
- data/lib/vue_live/precompiler.rb +88 -0
- data/lib/vue_live/rails/helper.rb +43 -0
- data/lib/vue_live/railtie.rb +77 -0
- data/lib/vue_live/resolver.rb +66 -0
- data/lib/vue_live/scoped_css.rb +258 -0
- data/lib/vue_live/sfc/descriptor.rb +86 -0
- data/lib/vue_live/sfc/parser.rb +118 -0
- data/lib/vue_live/sinatra.rb +32 -0
- data/lib/vue_live/source_map.rb +100 -0
- data/lib/vue_live/store.rb +143 -0
- data/lib/vue_live/tasks.rake +50 -0
- data/lib/vue_live/tasks.rb +5 -0
- data/lib/vue_live/templates/HelloVueLive.vue +48 -0
- data/lib/vue_live/templates/vue_live.yml +32 -0
- data/lib/vue_live/version.rb +5 -0
- data/lib/vue_live.rb +107 -0
- metadata +292 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VueLive
|
|
4
|
+
# Maps request paths to files under the component root, refusing anything that escapes it.
|
|
5
|
+
class Resolver
|
|
6
|
+
Target = Struct.new(:relative_path, :absolute_path, :vue?, :digest_hint, keyword_init: true)
|
|
7
|
+
|
|
8
|
+
def initialize(config)
|
|
9
|
+
@config = config
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def source_dir
|
|
13
|
+
@config.source_dir
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# +path+ is the request path *after* the URL prefix, e.g. "components/App.vue.js".
|
|
17
|
+
# Returns a Target or nil when nothing under the root matches.
|
|
18
|
+
def resolve(path)
|
|
19
|
+
rel = clean(path)
|
|
20
|
+
return nil if rel.nil? || rel.empty?
|
|
21
|
+
|
|
22
|
+
rel = rel.sub(/\.vue\.js\z/, '.vue') # "App.vue.js" is the same component as "App.vue"
|
|
23
|
+
ext = File.extname(rel).downcase
|
|
24
|
+
return nil unless @config.extensions.include?(ext)
|
|
25
|
+
|
|
26
|
+
root = File.expand_path(source_dir)
|
|
27
|
+
abs = File.expand_path(rel, root)
|
|
28
|
+
return nil unless abs.start_with?(root + File::SEPARATOR)
|
|
29
|
+
return nil unless File.file?(abs)
|
|
30
|
+
|
|
31
|
+
Target.new(relative_path: rel, absolute_path: abs, vue?: ext == '.vue')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Every .vue component under the root, as relative paths.
|
|
35
|
+
def components
|
|
36
|
+
root = File.expand_path(source_dir)
|
|
37
|
+
return [] unless File.directory?(root)
|
|
38
|
+
|
|
39
|
+
Dir.glob('**/*.vue', base: root).sort
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# All servable files (components and their sibling assets).
|
|
43
|
+
def files
|
|
44
|
+
root = File.expand_path(source_dir)
|
|
45
|
+
return [] unless File.directory?(root)
|
|
46
|
+
|
|
47
|
+
Dir.glob('**/*', base: root).select do |rel|
|
|
48
|
+
File.file?(File.join(root, rel)) && @config.extensions.include?(File.extname(rel).downcase)
|
|
49
|
+
end.sort
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def clean(path)
|
|
55
|
+
return nil if path.nil?
|
|
56
|
+
|
|
57
|
+
decoded = path.to_s.gsub(/%([0-9a-fA-F]{2})/) { Regexp.last_match(1).hex.chr }
|
|
58
|
+
return nil if decoded.include?("\0") || decoded.include?('\\')
|
|
59
|
+
|
|
60
|
+
parts = decoded.split('/').reject { |p| p.empty? || p == '.' }
|
|
61
|
+
return nil if parts.any? { |p| p == '..' || p.start_with?('.') }
|
|
62
|
+
|
|
63
|
+
parts.join('/')
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VueLive
|
|
4
|
+
# Rewrites the selectors of a <style scoped> block so they only match elements carrying the
|
|
5
|
+
# component's scope attribute, exactly like @vue/compiler-sfc does:
|
|
6
|
+
#
|
|
7
|
+
# .a .b -> .a .b[data-v-xxx]
|
|
8
|
+
# .a :deep(.b) -> .a[data-v-xxx] .b
|
|
9
|
+
# .a ::v-deep .b -> .a[data-v-xxx] .b
|
|
10
|
+
# :slotted(.a) -> .a[data-v-xxx-s]
|
|
11
|
+
# :global(.a) -> .a
|
|
12
|
+
# @keyframes -> names left alone (animations are not scoped)
|
|
13
|
+
#
|
|
14
|
+
# The implementation is a small, forgiving CSS walker; it does not validate the stylesheet.
|
|
15
|
+
class ScopedCSS
|
|
16
|
+
def self.rewrite(css, scope_id)
|
|
17
|
+
new(scope_id).rewrite(css)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(scope_id)
|
|
21
|
+
@attr = "[#{scope_id}]"
|
|
22
|
+
@slot_attr = "[#{scope_id}-s]"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def rewrite(css)
|
|
26
|
+
out = +''
|
|
27
|
+
process(strip_comments(css), out, scope: true)
|
|
28
|
+
out
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def strip_comments(css)
|
|
34
|
+
css.gsub(%r{/\*.*?\*/}m, '')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Walk a stylesheet (or the body of a conditional at-rule) and append the rewritten text.
|
|
38
|
+
def process(css, out, scope:)
|
|
39
|
+
i = 0
|
|
40
|
+
len = css.length
|
|
41
|
+
while i < len
|
|
42
|
+
# whitespace between rules
|
|
43
|
+
if css[i] =~ /\s/
|
|
44
|
+
out << css[i]
|
|
45
|
+
i += 1
|
|
46
|
+
next
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
brace = find_rule_boundary(css, i)
|
|
50
|
+
if brace.nil?
|
|
51
|
+
out << css[i..]
|
|
52
|
+
break
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
kind, open_idx = brace
|
|
56
|
+
head = css[i...open_idx]
|
|
57
|
+
|
|
58
|
+
if kind == :statement # e.g. @import ...; @charset ...;
|
|
59
|
+
out << head << ';'
|
|
60
|
+
i = open_idx + 1
|
|
61
|
+
next
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
close_idx = matching_brace(css, open_idx)
|
|
65
|
+
body = css[(open_idx + 1)...close_idx]
|
|
66
|
+
|
|
67
|
+
if head.lstrip.start_with?('@')
|
|
68
|
+
name = head.strip[/\A@([\w-]+)/, 1].to_s.downcase
|
|
69
|
+
out << head << '{'
|
|
70
|
+
if %w[media supports layer container document].include?(name)
|
|
71
|
+
process(body, out, scope: scope)
|
|
72
|
+
else
|
|
73
|
+
# @keyframes, @font-face, @page, @property ... : copy verbatim
|
|
74
|
+
out << body
|
|
75
|
+
end
|
|
76
|
+
out << '}'
|
|
77
|
+
else
|
|
78
|
+
out << (scope ? rewrite_selector_list(head) : head) << '{' << body << '}'
|
|
79
|
+
end
|
|
80
|
+
i = close_idx + 1
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# From +i+ find whether the next rule is a block ({) or a statement (;). Returns
|
|
85
|
+
# [:block, index_of_brace] or [:statement, index_of_semicolon], ignoring quoted strings.
|
|
86
|
+
def find_rule_boundary(css, i)
|
|
87
|
+
quote = nil
|
|
88
|
+
depth = 0
|
|
89
|
+
while i < css.length
|
|
90
|
+
c = css[i]
|
|
91
|
+
if quote
|
|
92
|
+
quote = nil if c == quote && css[i - 1] != '\\'
|
|
93
|
+
elsif ['"', "'"].include?(c)
|
|
94
|
+
quote = c
|
|
95
|
+
elsif c == '('
|
|
96
|
+
depth += 1
|
|
97
|
+
elsif c == ')'
|
|
98
|
+
depth -= 1
|
|
99
|
+
elsif depth.zero? && c == '{'
|
|
100
|
+
return [:block, i]
|
|
101
|
+
elsif depth.zero? && c == ';'
|
|
102
|
+
return [:statement, i]
|
|
103
|
+
end
|
|
104
|
+
i += 1
|
|
105
|
+
end
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def matching_brace(css, open_idx)
|
|
110
|
+
depth = 0
|
|
111
|
+
quote = nil
|
|
112
|
+
i = open_idx
|
|
113
|
+
while i < css.length
|
|
114
|
+
c = css[i]
|
|
115
|
+
if quote
|
|
116
|
+
quote = nil if c == quote && css[i - 1] != '\\'
|
|
117
|
+
elsif ['"', "'"].include?(c)
|
|
118
|
+
quote = c
|
|
119
|
+
elsif c == '{'
|
|
120
|
+
depth += 1
|
|
121
|
+
elsif c == '}'
|
|
122
|
+
depth -= 1
|
|
123
|
+
return i if depth.zero?
|
|
124
|
+
end
|
|
125
|
+
i += 1
|
|
126
|
+
end
|
|
127
|
+
css.length
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def rewrite_selector_list(list)
|
|
131
|
+
leading = list[/\A\s*/]
|
|
132
|
+
trailing = list[/\s*\z/]
|
|
133
|
+
selectors = split_top_level(list.strip, ',')
|
|
134
|
+
leading + selectors.map { |s| rewrite_selector(s.strip) }.join(', ') + trailing
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Split on +sep+ while respecting parentheses and quotes.
|
|
138
|
+
def split_top_level(str, sep)
|
|
139
|
+
parts = []
|
|
140
|
+
depth = 0
|
|
141
|
+
quote = nil
|
|
142
|
+
buf = +''
|
|
143
|
+
str.each_char.with_index do |c, idx|
|
|
144
|
+
if quote
|
|
145
|
+
quote = nil if c == quote && str[idx - 1] != '\\'
|
|
146
|
+
buf << c
|
|
147
|
+
elsif ['"', "'"].include?(c)
|
|
148
|
+
quote = c
|
|
149
|
+
buf << c
|
|
150
|
+
elsif c == '('
|
|
151
|
+
depth += 1
|
|
152
|
+
buf << c
|
|
153
|
+
elsif c == ')'
|
|
154
|
+
depth -= 1
|
|
155
|
+
buf << c
|
|
156
|
+
elsif c == sep && depth.zero?
|
|
157
|
+
parts << buf
|
|
158
|
+
buf = +''
|
|
159
|
+
else
|
|
160
|
+
buf << c
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
parts << buf
|
|
164
|
+
parts
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def rewrite_selector(sel)
|
|
168
|
+
return sel if sel.empty?
|
|
169
|
+
|
|
170
|
+
# :global(...) escapes scoping entirely.
|
|
171
|
+
if (m = sel.match(/\A:global\((.*)\)\z/m))
|
|
172
|
+
return m[1].strip
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# :deep(x) / ::v-deep x / >>> x / /deep/ x : scope the part *before*, leave the rest alone.
|
|
176
|
+
if (m = sel.match(%r{\A(.*?)(?:\s*::v-deep\((.*?)\)|\s*:deep\((.*?)\)|\s*::v-deep\b|\s*>>>|\s*/deep/)(.*)\z}m))
|
|
177
|
+
before = m[1].strip
|
|
178
|
+
inner = (m[2] || m[3]).to_s.strip
|
|
179
|
+
after = m[4].to_s.strip
|
|
180
|
+
rest = [inner, after].reject(&:empty?).join(' ')
|
|
181
|
+
return rest if before.empty? # ":deep(.a)" alone scopes nothing
|
|
182
|
+
|
|
183
|
+
return "#{scope_compound(before)} #{rest}".strip
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# :slotted(x) -> x gets the slot scope attribute.
|
|
187
|
+
if (m = sel.match(/\A(.*?):slotted\((.*)\)(.*)\z/m))
|
|
188
|
+
before = m[1].strip
|
|
189
|
+
inner = m[2].strip
|
|
190
|
+
after = m[3].strip
|
|
191
|
+
scoped_inner = scope_compound(inner, attr: @slot_attr)
|
|
192
|
+
return [before, scoped_inner + after].reject(&:empty?).join(' ')
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
scope_compound(sel)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Append the scope attribute to the last compound selector, before any pseudo-element.
|
|
199
|
+
def scope_compound(sel, attr: @attr)
|
|
200
|
+
sel = sel.strip
|
|
201
|
+
return sel if sel.empty?
|
|
202
|
+
|
|
203
|
+
compounds = split_compounds(sel)
|
|
204
|
+
last = compounds.pop
|
|
205
|
+
compounds.push(insert_attr(last, attr))
|
|
206
|
+
compounds.join
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Split "a > b ~ c" into ["a > ", "b ~ ", "c"] keeping combinators attached to the previous part.
|
|
210
|
+
def split_compounds(sel)
|
|
211
|
+
parts = []
|
|
212
|
+
buf = +''
|
|
213
|
+
depth = 0
|
|
214
|
+
quote = nil
|
|
215
|
+
i = 0
|
|
216
|
+
while i < sel.length
|
|
217
|
+
c = sel[i]
|
|
218
|
+
if quote
|
|
219
|
+
quote = nil if c == quote && sel[i - 1] != '\\'
|
|
220
|
+
buf << c
|
|
221
|
+
elsif ['"', "'"].include?(c)
|
|
222
|
+
quote = c
|
|
223
|
+
buf << c
|
|
224
|
+
elsif c == '('
|
|
225
|
+
depth += 1
|
|
226
|
+
buf << c
|
|
227
|
+
elsif c == ')'
|
|
228
|
+
depth -= 1
|
|
229
|
+
buf << c
|
|
230
|
+
elsif depth.zero? && (c =~ /\s/ || c == '>' || c == '+' || c == '~')
|
|
231
|
+
# consume the whole combinator run
|
|
232
|
+
j = i
|
|
233
|
+
j += 1 while j < sel.length && (sel[j] =~ /\s/ || sel[j] == '>' || sel[j] == '+' || sel[j] == '~')
|
|
234
|
+
buf << sel[i...j]
|
|
235
|
+
parts << buf
|
|
236
|
+
buf = +''
|
|
237
|
+
i = j
|
|
238
|
+
next
|
|
239
|
+
else
|
|
240
|
+
buf << c
|
|
241
|
+
end
|
|
242
|
+
i += 1
|
|
243
|
+
end
|
|
244
|
+
parts << buf unless buf.empty?
|
|
245
|
+
parts
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def insert_attr(compound, attr)
|
|
249
|
+
# Split off pseudo-elements (::before, :before, :after, :first-line...) which must come last.
|
|
250
|
+
if (m = compound.match(/\A(.*?)((?:::[\w-]+(?:\([^)]*\))?|:(?:before|after|first-line|first-letter|selection|placeholder|marker|backdrop)\b)+)\z/m)) # rubocop:disable Layout/LineLength
|
|
251
|
+
base = m[1]
|
|
252
|
+
pseudo = m[2]
|
|
253
|
+
return "#{base.empty? ? '*' : base}#{attr}#{pseudo}"
|
|
254
|
+
end
|
|
255
|
+
compound.empty? ? "*#{attr}" : "#{compound}#{attr}"
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VueLive
|
|
4
|
+
module SFC
|
|
5
|
+
# One top-level block of a single-file component: <template>, <script>, <style> or a custom block.
|
|
6
|
+
class Block
|
|
7
|
+
attr_reader :type, :content, :attrs, :line
|
|
8
|
+
|
|
9
|
+
def initialize(type:, content:, attrs: {}, line: 1)
|
|
10
|
+
@type = type
|
|
11
|
+
@content = content
|
|
12
|
+
@attrs = attrs
|
|
13
|
+
@line = line
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def lang
|
|
17
|
+
attrs['lang']
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def src
|
|
21
|
+
attrs['src']
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def setup?
|
|
25
|
+
attrs.key?('setup')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def scoped?
|
|
29
|
+
attrs.key?('scoped')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def module?
|
|
33
|
+
attrs.key?('module')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def attr?(name)
|
|
37
|
+
attrs.key?(name)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The parsed shape of a .vue file. Mirrors @vue/compiler-sfc's SFCDescriptor closely enough
|
|
42
|
+
# that both compiler backends can reason about the same thing.
|
|
43
|
+
class Descriptor
|
|
44
|
+
attr_reader :filename, :source, :template, :script, :script_setup, :styles, :custom_blocks
|
|
45
|
+
|
|
46
|
+
def initialize(filename:, source:, template: nil, script: nil, script_setup: nil, styles: [], custom_blocks: [])
|
|
47
|
+
@filename = filename
|
|
48
|
+
@source = source
|
|
49
|
+
@template = template
|
|
50
|
+
@script = script
|
|
51
|
+
@script_setup = script_setup
|
|
52
|
+
@styles = styles
|
|
53
|
+
@custom_blocks = custom_blocks
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def scoped_styles?
|
|
57
|
+
styles.any?(&:scoped?)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def css_modules?
|
|
61
|
+
styles.any?(&:module?)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def template_lang
|
|
65
|
+
template&.lang
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def script_lang
|
|
69
|
+
(script_setup || script)&.lang
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Features the pure Ruby backend cannot provide. Empty means "Ruby can compile this".
|
|
73
|
+
def advanced_features
|
|
74
|
+
features = []
|
|
75
|
+
features << '<script setup>' if script_setup
|
|
76
|
+
features << "<script lang=\"#{script_lang}\">" if script_lang && !%w[js javascript].include?(script_lang)
|
|
77
|
+
features << "<template lang=\"#{template_lang}\">" if template_lang && template_lang != 'html'
|
|
78
|
+
styles.each do |s|
|
|
79
|
+
features << "<style lang=\"#{s.lang}\">" if s.lang && !%w[css postcss].include?(s.lang)
|
|
80
|
+
features << '<style module>' if s.module?
|
|
81
|
+
end
|
|
82
|
+
features.uniq
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'descriptor'
|
|
4
|
+
|
|
5
|
+
module VueLive
|
|
6
|
+
module SFC
|
|
7
|
+
# Splits a .vue file into its top-level blocks.
|
|
8
|
+
#
|
|
9
|
+
# Only the top level is parsed; the contents of <template> are handed to Vue's own compiler
|
|
10
|
+
# untouched, so nested <template v-slot> elements, comments and so on survive as-is.
|
|
11
|
+
class Parser
|
|
12
|
+
BLOCK_START = %r{<(template|script|style|[a-zA-Z][\w-]*)(\s[^>]*?)?(/?)>}m
|
|
13
|
+
ATTR = %r{([^\s=/"']+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?}
|
|
14
|
+
|
|
15
|
+
def self.parse(source, filename: 'anonymous.vue')
|
|
16
|
+
new(source, filename).parse
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(source, filename)
|
|
20
|
+
@source = source
|
|
21
|
+
@filename = filename
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def parse
|
|
25
|
+
template = nil
|
|
26
|
+
script = nil
|
|
27
|
+
script_setup = nil
|
|
28
|
+
styles = []
|
|
29
|
+
custom = []
|
|
30
|
+
|
|
31
|
+
pos = 0
|
|
32
|
+
while (m = BLOCK_START.match(@source, pos))
|
|
33
|
+
tag = m[1]
|
|
34
|
+
attrs = parse_attrs(m[2].to_s)
|
|
35
|
+
self_close = m[3] == '/'
|
|
36
|
+
start = m.end(0)
|
|
37
|
+
line = @source[0...m.begin(0)].count("\n") + 1
|
|
38
|
+
|
|
39
|
+
if self_close
|
|
40
|
+
content = ''
|
|
41
|
+
pos = start
|
|
42
|
+
else
|
|
43
|
+
close = find_close(tag, start)
|
|
44
|
+
raise CompileError.new("unterminated <#{tag}> block starting on line #{line}", file: @filename) unless close
|
|
45
|
+
|
|
46
|
+
content = @source[start...close[0]]
|
|
47
|
+
pos = close[1]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
block = Block.new(type: tag, content: content, attrs: attrs, line: line)
|
|
51
|
+
case tag
|
|
52
|
+
when 'template'
|
|
53
|
+
raise CompileError.new('only one top-level <template> block is allowed', file: @filename) if template
|
|
54
|
+
|
|
55
|
+
template = block
|
|
56
|
+
when 'script'
|
|
57
|
+
if block.setup?
|
|
58
|
+
raise CompileError.new('only one <script setup> block is allowed', file: @filename) if script_setup
|
|
59
|
+
|
|
60
|
+
script_setup = block
|
|
61
|
+
else
|
|
62
|
+
raise CompileError.new('only one plain <script> block is allowed', file: @filename) if script
|
|
63
|
+
|
|
64
|
+
script = block
|
|
65
|
+
end
|
|
66
|
+
when 'style'
|
|
67
|
+
styles << block
|
|
68
|
+
else
|
|
69
|
+
custom << block
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if template.nil? && script.nil? && script_setup.nil?
|
|
74
|
+
raise CompileError.new('at least a <template> or <script> block is required', file: @filename)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
Descriptor.new(filename: @filename, source: @source, template: template, script: script,
|
|
78
|
+
script_setup: script_setup, styles: styles, custom_blocks: custom)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
# Returns [content_end, after_close_tag] for the block opened at +start+.
|
|
84
|
+
def find_close(tag, start)
|
|
85
|
+
if tag == 'template'
|
|
86
|
+
# Templates nest (<template v-slot>), so balance open/close tags.
|
|
87
|
+
depth = 1
|
|
88
|
+
scan = start
|
|
89
|
+
re = %r{<(/?)template(?=[\s>/])[^>]*?(/?)>}m
|
|
90
|
+
while (m = re.match(@source, scan))
|
|
91
|
+
if m[1] == '/'
|
|
92
|
+
depth -= 1
|
|
93
|
+
return [m.begin(0), m.end(0)] if depth.zero?
|
|
94
|
+
elsif m[2] != '/'
|
|
95
|
+
depth += 1
|
|
96
|
+
end
|
|
97
|
+
scan = m.end(0)
|
|
98
|
+
end
|
|
99
|
+
nil
|
|
100
|
+
else
|
|
101
|
+
idx = @source.index(%r{</#{Regexp.escape(tag)}\s*>}i, start)
|
|
102
|
+
return nil unless idx
|
|
103
|
+
|
|
104
|
+
close_end = @source.index('>', idx) + 1
|
|
105
|
+
[idx, close_end]
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def parse_attrs(str)
|
|
110
|
+
attrs = {}
|
|
111
|
+
str.scan(ATTR) do |name, dq, sq, bare|
|
|
112
|
+
attrs[name] = dq || sq || bare || ''
|
|
113
|
+
end
|
|
114
|
+
attrs
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../vue_live'
|
|
4
|
+
|
|
5
|
+
module VueLive
|
|
6
|
+
# Sinatra extension:
|
|
7
|
+
#
|
|
8
|
+
# require 'vue_live/sinatra'
|
|
9
|
+
# class App < Sinatra::Base
|
|
10
|
+
# set :vue_live, source_path: 'app/vue', prefix: '/vue' # optional, before register
|
|
11
|
+
# register VueLive::Sinatra
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# Classic-style apps call `register VueLive::Sinatra` at the top level.
|
|
15
|
+
#
|
|
16
|
+
# Mounts the middleware (unless config.middleware is false), defaults the component root to the
|
|
17
|
+
# app's own root and adds the view helpers.
|
|
18
|
+
module Sinatra
|
|
19
|
+
def self.registered(app)
|
|
20
|
+
app.helpers VueLive::Helpers
|
|
21
|
+
app.set :vue_live, {} unless app.respond_to?(:vue_live)
|
|
22
|
+
|
|
23
|
+
app.configure do
|
|
24
|
+
VueLive.config.root = app.root if app.respond_to?(:root) && app.root && !VueLive.rails?
|
|
25
|
+
VueLive.config.env = app.environment.to_s if app.respond_to?(:environment) && app.environment
|
|
26
|
+
VueLive.config.apply(app.vue_live || {})
|
|
27
|
+
VueLive.load_config_file
|
|
28
|
+
app.use VueLive::Middleware if VueLive.config.middleware
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'base64'
|
|
5
|
+
|
|
6
|
+
module VueLive
|
|
7
|
+
# Minimal source map (v3) writer: enough to map generated lines back to lines of the .vue file.
|
|
8
|
+
#
|
|
9
|
+
# map = SourceMap.new('App.vue', source)
|
|
10
|
+
# map.add(generated_line: 3, original_line: 12) # 1-based
|
|
11
|
+
# map.to_h / map.to_json / map.inline_comment
|
|
12
|
+
class SourceMap
|
|
13
|
+
BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
|
14
|
+
|
|
15
|
+
attr_reader :file, :source
|
|
16
|
+
|
|
17
|
+
def initialize(file, source, generated_file: "#{file}.js")
|
|
18
|
+
@file = file
|
|
19
|
+
@source = source
|
|
20
|
+
@generated_file = generated_file
|
|
21
|
+
@mappings = {} # generated line (1-based) => [[gen_col, orig_line, orig_col], ...]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def add(generated_line:, original_line:, generated_column: 0, original_column: 0)
|
|
25
|
+
(@mappings[generated_line] ||= []) << [generated_column, original_line, original_column]
|
|
26
|
+
self
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Map +count+ consecutive generated lines starting at +generated_line+ to the source lines
|
|
30
|
+
# starting at +original_line+.
|
|
31
|
+
def add_block(generated_line:, original_line:, count:)
|
|
32
|
+
count.times { |i| add(generated_line: generated_line + i, original_line: original_line + i) }
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def empty?
|
|
37
|
+
@mappings.empty?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_h
|
|
41
|
+
{
|
|
42
|
+
'version' => 3,
|
|
43
|
+
'file' => @generated_file,
|
|
44
|
+
'sources' => [file],
|
|
45
|
+
'sourcesContent' => [source],
|
|
46
|
+
'names' => [],
|
|
47
|
+
'mappings' => encode_mappings
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def to_json(*)
|
|
52
|
+
JSON.generate(to_h)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# `//# sourceMappingURL=data:...` line for a map Hash (this one or a backend-supplied one).
|
|
56
|
+
def self.inline_comment(map_hash)
|
|
57
|
+
data = Base64.strict_encode64(JSON.generate(map_hash))
|
|
58
|
+
"//# sourceMappingURL=data:application/json;charset=utf-8;base64,#{data}\n"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def inline_comment
|
|
62
|
+
self.class.inline_comment(to_h)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.vlq(value)
|
|
66
|
+
vlq = value.negative? ? ((-value) << 1) | 1 : value << 1
|
|
67
|
+
out = +''
|
|
68
|
+
loop do
|
|
69
|
+
digit = vlq & 0x1f
|
|
70
|
+
vlq >>= 5
|
|
71
|
+
digit |= 0x20 if vlq.positive?
|
|
72
|
+
out << BASE64[digit]
|
|
73
|
+
break unless vlq.positive?
|
|
74
|
+
end
|
|
75
|
+
out
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def encode_mappings
|
|
81
|
+
last_line = @mappings.keys.max || 0
|
|
82
|
+
prev_orig_line = 0
|
|
83
|
+
prev_orig_col = 0
|
|
84
|
+
(1..last_line).map do |line|
|
|
85
|
+
segments = (@mappings[line] || []).sort_by(&:first)
|
|
86
|
+
prev_gen_col = 0
|
|
87
|
+
segments.map do |gen_col, orig_line, orig_col|
|
|
88
|
+
seg = self.class.vlq(gen_col - prev_gen_col) +
|
|
89
|
+
self.class.vlq(0) + # source index (always the one .vue file)
|
|
90
|
+
self.class.vlq((orig_line - 1) - prev_orig_line) +
|
|
91
|
+
self.class.vlq(orig_col - prev_orig_col)
|
|
92
|
+
prev_gen_col = gen_col
|
|
93
|
+
prev_orig_line = orig_line - 1
|
|
94
|
+
prev_orig_col = orig_col
|
|
95
|
+
seg
|
|
96
|
+
end.join(',')
|
|
97
|
+
end.join(';')
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|