actionview-svelte-handler 0.3.0 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.forgejo/workflows/javascript_police.yml +22 -0
- data/.forgejo/workflows/ruby_police.yml +27 -0
- data/.gitignore +14 -0
- data/.ruby-version +1 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +284 -0
- data/README.md +90 -15
- data/Steepfile +12 -0
- data/actionview-svelte-handler.gemspec +21 -0
- data/bin/test +5 -0
- data/build.esbuild.js +22 -0
- data/lib/generators/svelte/install_generator.rb +13 -0
- data/lib/generators/svelte/templates/install/initializer.rb +17 -0
- data/lib/svelte/errors.rb +151 -0
- data/lib/svelte/handler.rb +49 -21
- data/lib/svelte/helpers.rb +13 -3
- data/lib/svelte/js/.DO_NOT_MODIFY.md +2 -0
- data/lib/svelte/js/builder.js +122 -0
- data/lib/svelte/railtie.rb +7 -5
- data/lib/svelte/templates/assembler.js.erb +16 -0
- data/lib/svelte/templates/island.html.erb +20 -0
- data/lib/svelte/variabilization.rb +11 -0
- data/lib/svelte/version.rb +1 -1
- data/lib/svelte.rb +27 -4
- data/lib/tasks/svelte_tasks.rake +4 -0
- data/lib/ts/builder.ts +174 -0
- data/lib/ts/types/.DO_NOT_MODIFY.md +2 -0
- data/lib/ts/types/builder.d.ts +42 -0
- data/package-lock.json +6643 -0
- data/package.json +39 -0
- data/rbs_collection.lock.yaml +344 -0
- data/rbs_collection.yaml +18 -0
- data/sig/lib/svelte/errors.rbs +38 -0
- data/sig/lib/svelte/handler.rbs +9 -0
- data/sig/lib/svelte/helpers.rbs +7 -0
- data/sig/lib/svelte/version.rbs +3 -0
- data/sig/lib/svelte.rbs +19 -0
- data/svelte-on-rails.png +0 -0
- data/tsconfig.json +15 -0
- data/watch.esbuild.js +4 -0
- metadata +37 -3
@@ -0,0 +1,151 @@
|
|
1
|
+
require "active_support/syntax_error_proxy"
|
2
|
+
require "action_view/template/error"
|
3
|
+
module Svelte
|
4
|
+
class CompilerError < ActiveSupport::SyntaxErrorProxy # steep:ignore UnknownConstant
|
5
|
+
attr_accessor :location, :suggestion, :template
|
6
|
+
|
7
|
+
def initialize(message, location)
|
8
|
+
@location = location
|
9
|
+
@sugggestion = @location.dig(:suggestion) || ""
|
10
|
+
# steep:ignore:start
|
11
|
+
super(message) # : self
|
12
|
+
# steep:ignore:end
|
13
|
+
end
|
14
|
+
|
15
|
+
def message
|
16
|
+
to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
def backtrace
|
20
|
+
if suggestion
|
21
|
+
["#{Rails.root.join location[:file]}:#{location[:line]}:#{message}, #{suggestion}"] + caller
|
22
|
+
else
|
23
|
+
["#{Rails.root.join location[:file]}:#{location[:line]}:#{message}"] + caller
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def cause
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def bindings
|
32
|
+
[template.send(:binding)]
|
33
|
+
end
|
34
|
+
|
35
|
+
def backtrace_locations
|
36
|
+
traces = backtrace.map { |trace|
|
37
|
+
file, line = trace.match(/^(.+?):(\d+).*$/, &:captures) || trace
|
38
|
+
ActiveSupport::SyntaxErrorProxy::BacktraceLocation.new(file, line.to_i, trace) # steep:ignore UnknownConstant
|
39
|
+
}
|
40
|
+
|
41
|
+
traces.map { |loc| ActiveSupport::SyntaxErrorProxy::BacktraceLocationProxy.new(loc, self) } # steep:ignore UnknownConstant
|
42
|
+
end
|
43
|
+
|
44
|
+
def annotated_source_code
|
45
|
+
location[:lineText].split("\n").map.with_index(1) { |line, index|
|
46
|
+
indentation = " " * 4
|
47
|
+
"#{index}:#{indentation}#{line}"
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class TemplateError < StandardError
|
53
|
+
attr_reader :cause, :template
|
54
|
+
|
55
|
+
SOURCE_CODE_RADIUS = 3
|
56
|
+
|
57
|
+
def initialize(template, error)
|
58
|
+
raise("Did not provide cause error") unless error
|
59
|
+
@cause = error
|
60
|
+
|
61
|
+
raise("Did not provide template") unless template
|
62
|
+
@template, @sub_templates = template, nil
|
63
|
+
|
64
|
+
raise("Cause error is nil for TemplateError") unless @cause
|
65
|
+
@cause.template = template
|
66
|
+
|
67
|
+
# steep:ignore:start
|
68
|
+
super(@cause.message) # : self
|
69
|
+
# steep:ignore:end
|
70
|
+
end
|
71
|
+
|
72
|
+
def message
|
73
|
+
@cause.message
|
74
|
+
end
|
75
|
+
|
76
|
+
def annotated_source_code
|
77
|
+
@cause.annotated_source_code
|
78
|
+
end
|
79
|
+
|
80
|
+
# Following is copypasta-ed from ActionView::Template::Error
|
81
|
+
def backtrace
|
82
|
+
@cause.backtrace
|
83
|
+
end
|
84
|
+
|
85
|
+
def backtrace_locations
|
86
|
+
@cause.backtrace_locations
|
87
|
+
end
|
88
|
+
|
89
|
+
def file_name
|
90
|
+
@template.identifier
|
91
|
+
end
|
92
|
+
|
93
|
+
def sub_template_message
|
94
|
+
if @sub_templates
|
95
|
+
"Trace of template inclusion: " +
|
96
|
+
@sub_templates.collect(&:inspect).join(", ")
|
97
|
+
else
|
98
|
+
""
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def source_extract(indentation = 0)
|
103
|
+
return [] unless (num = line_number)
|
104
|
+
num = num.to_i
|
105
|
+
|
106
|
+
source_code = @template.encode!.split("\n")
|
107
|
+
|
108
|
+
start_on_line = [num - SOURCE_CODE_RADIUS - 1, 0].max
|
109
|
+
end_on_line = [num + SOURCE_CODE_RADIUS - 1, source_code.length].min
|
110
|
+
|
111
|
+
indent = end_on_line.to_s.size + indentation
|
112
|
+
return [] unless (source_code = source_code[start_on_line..end_on_line])
|
113
|
+
|
114
|
+
formatted_code_for(source_code, start_on_line, indent)
|
115
|
+
end
|
116
|
+
|
117
|
+
def sub_template_of(template_path)
|
118
|
+
@sub_templates ||= []
|
119
|
+
@sub_templates << template_path
|
120
|
+
end
|
121
|
+
|
122
|
+
def line_number
|
123
|
+
@line_number ||=
|
124
|
+
if file_name
|
125
|
+
regexp = /#{Regexp.escape File.basename(file_name)}:(\d+)/
|
126
|
+
$1 if message =~ regexp || backtrace.find { |line| line =~ regexp }
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def source_location
|
133
|
+
if line_number
|
134
|
+
"on line ##{line_number} of "
|
135
|
+
else
|
136
|
+
"in "
|
137
|
+
end + file_name
|
138
|
+
end
|
139
|
+
|
140
|
+
def formatted_code_for(source_code, line_counter, indent)
|
141
|
+
raise("line counter is nil") if line_counter.nil?
|
142
|
+
indent_template = "%#{indent}s: %s"
|
143
|
+
source_code.map do |line|
|
144
|
+
line_counter += 1
|
145
|
+
indent_template % [line_counter, line]
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# {:id=>"", :location=>{:column=>13, :file=>"demo/e.html.svelte", :length=>3, :line=>7, :lineText=>"<h1>{hola}, {nam}</h1>", :namespace=>"file", :suggestion=>""}, :notes=>[], :pluginName=>"esbuild-svelte", :text=>"'nam' is not defined"}
|
data/lib/svelte/handler.rb
CHANGED
@@ -1,37 +1,65 @@
|
|
1
|
-
require
|
1
|
+
require "erb"
|
2
|
+
require "svelte/helpers"
|
2
3
|
|
3
4
|
module Svelte
|
5
|
+
DISCARD_PROPS = %w[lookup_context view_renderer current_template output_buffer view_flow rendered_format marked_for_same_origin_verification virtual_path]
|
6
|
+
ISLAND_ATTRS = {
|
7
|
+
"on:visible": "",
|
8
|
+
"on:idle": ""
|
9
|
+
}
|
10
|
+
|
4
11
|
class Handler
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
12
|
+
def call(template, source)
|
13
|
+
digest = Digest::SHA256.base64digest(source)
|
14
|
+
|
15
|
+
<<-RUBY
|
16
|
+
template = ObjectSpace._id2ref(#{template.object_id})
|
17
|
+
|
18
|
+
Svelte.props.merge!(
|
19
|
+
instance_values.reject { |k,v|
|
20
|
+
k.start_with?("_") || Svelte::DISCARD_PROPS.include?(k)
|
21
|
+
}
|
22
|
+
) { |key,old,new| old }
|
23
|
+
|
24
|
+
Svelte.props.deep_symbolize_keys!
|
25
|
+
|
26
|
+
if local_assigns.dig(:svelte) == nil
|
27
|
+
local_assigns[:svelte] = {}
|
28
|
+
end
|
29
|
+
|
30
|
+
assembler = nil
|
31
|
+
|
32
|
+
assembler = Tempfile.new(['assembler', ".mjs"], '#{Svelte.gem_dir}/tmp')
|
33
|
+
|
34
|
+
assembler.write(ERB.new(File.read('#{Svelte.gem_dir}/lib/svelte/templates/assembler.js.erb')).result_with_hash({
|
35
|
+
path: '#{Rails.root.join template.identifier}',
|
16
36
|
locals: local_assigns,
|
17
|
-
|
37
|
+
compiled_client: j(Rails.cache.read('svelte/template/client' + '#{digest}')),
|
38
|
+
compiled_server: j(Rails.cache.read('svelte/template/server' + '#{digest}')),
|
39
|
+
ssr: Svelte.precedence(local_assigns.dig(:svelte, :ssr), template.variant.nil? ? nil : (template.variant == "server"), Svelte.ssr)
|
18
40
|
}))
|
19
|
-
assembler.rewind
|
20
41
|
|
42
|
+
assembler.rewind
|
43
|
+
|
21
44
|
result = JSON.parse(`NODE_NO_WARNINGS=1 node --experimental-vm-modules \#{assembler.path}`).deep_symbolize_keys
|
22
|
-
|
23
|
-
|
45
|
+
|
46
|
+
if result[:error]
|
47
|
+
e = Svelte::CompilerError.new(result.dig(:error, :text), result.dig(:error, :location))
|
48
|
+
raise Svelte::TemplateError.new(template, e)
|
49
|
+
end
|
24
50
|
|
25
|
-
assembler
|
26
|
-
assembler
|
51
|
+
assembler&.close
|
52
|
+
assembler&.unlink
|
27
53
|
|
28
|
-
|
54
|
+
Rails.cache.write("svelte/template/client/" + '#{digest}', result[:compiled][:client], expires_in: 14.days)
|
55
|
+
Rails.cache.write("svelte/template/server/" + '#{digest}', result[:compiled][:server], expires_in: 14.days)
|
56
|
+
|
57
|
+
ERB.new(File.read("#{Svelte.gem_dir}/lib/svelte/templates/island.html.erb")).result_with_hash({ result:, locals: local_assigns, digest: "#{digest}"})
|
29
58
|
RUBY
|
30
59
|
end
|
31
|
-
|
32
60
|
|
33
61
|
def self.call(template, source)
|
34
62
|
new.call(template, source)
|
35
63
|
end
|
36
64
|
end
|
37
|
-
end
|
65
|
+
end
|
data/lib/svelte/helpers.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
module Svelte
|
2
|
-
module Helpers
|
2
|
+
module Helpers
|
3
3
|
include ActionView::Helpers::JavaScriptHelper
|
4
4
|
def svelte_tags
|
5
5
|
js = <<-JS
|
@@ -9,8 +9,18 @@ module Svelte
|
|
9
9
|
|
10
10
|
window.props = readable(JSON.parse("#{j(Svelte.props.to_json)}"))
|
11
11
|
JS
|
12
|
-
|
12
|
+
|
13
13
|
tag.script(js.html_safe, type: "module")
|
14
14
|
end
|
15
|
+
|
16
|
+
delegate :destructure_attrs, to: :class
|
17
|
+
|
18
|
+
def self.destructure_attrs(hash)
|
19
|
+
string = ""
|
20
|
+
hash.each do |k, v|
|
21
|
+
string += "#{k}='#{j v}' "
|
22
|
+
end
|
23
|
+
string
|
24
|
+
end
|
15
25
|
end
|
16
|
-
end
|
26
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
/*
|
2
|
+
IF YOU ARE READING THIS, YOU ARE VIEWING THE BUILT JS FROM THE SOURCE TYPESCRIPT AT ../ts/ .
|
3
|
+
|
4
|
+
DO NOT MODIFY.
|
5
|
+
*/
|
6
|
+
|
7
|
+
import { readable } from "svelte/store";
|
8
|
+
import { importFromStringSync } from "module-from-string";
|
9
|
+
import * as esbuild from "esbuild";
|
10
|
+
import sveltePlugin from "esbuild-svelte";
|
11
|
+
import { sveltePreprocess } from "svelte-preprocess";
|
12
|
+
import * as recast from "recast";
|
13
|
+
class Builder {
|
14
|
+
path;
|
15
|
+
props;
|
16
|
+
locals;
|
17
|
+
compiled;
|
18
|
+
ssr;
|
19
|
+
workingDir;
|
20
|
+
preprocess;
|
21
|
+
constructor(path, props, locals, client, server, ssr, workingDir, preprocess) {
|
22
|
+
this.path = path;
|
23
|
+
this.props = readable(props);
|
24
|
+
this.locals = locals;
|
25
|
+
this.compiled = {
|
26
|
+
client,
|
27
|
+
server
|
28
|
+
};
|
29
|
+
this.ssr = ssr;
|
30
|
+
this.workingDir = workingDir;
|
31
|
+
this.preprocess = preprocess;
|
32
|
+
}
|
33
|
+
async bundle(generate, sveltePath = "svelte") {
|
34
|
+
const bundle = await esbuild.build({
|
35
|
+
entryPoints: [this.path],
|
36
|
+
mainFields: ["svelte", "browser", "module", "main"],
|
37
|
+
conditions: ["svelte", "browser"],
|
38
|
+
absWorkingDir: this.workingDir,
|
39
|
+
write: false,
|
40
|
+
outfile: "component.js",
|
41
|
+
bundle: true,
|
42
|
+
format: "esm",
|
43
|
+
metafile: true,
|
44
|
+
plugins: [
|
45
|
+
// @ts-expect-error
|
46
|
+
sveltePlugin({
|
47
|
+
compilerOptions: {
|
48
|
+
generate,
|
49
|
+
css: "injected",
|
50
|
+
hydratable: true,
|
51
|
+
sveltePath
|
52
|
+
},
|
53
|
+
preprocess: sveltePreprocess(this.preprocess),
|
54
|
+
filterWarnings: (warning) => {
|
55
|
+
if (warning.code === "missing-declaration" && warning.message.includes("'props'")) {
|
56
|
+
return false;
|
57
|
+
}
|
58
|
+
return true;
|
59
|
+
}
|
60
|
+
})
|
61
|
+
]
|
62
|
+
});
|
63
|
+
const throwables = [].concat(bundle.errors, bundle.warnings);
|
64
|
+
if (throwables.length > 0) {
|
65
|
+
throw throwables[0];
|
66
|
+
}
|
67
|
+
return bundle.outputFiles[0].text;
|
68
|
+
}
|
69
|
+
async client() {
|
70
|
+
return this.compiled?.client || this.standardizeClient(await this.bundle("dom", "https://esm.sh/svelte"));
|
71
|
+
}
|
72
|
+
standardizeClient(code) {
|
73
|
+
const ast = recast.parse(code);
|
74
|
+
let name;
|
75
|
+
recast.visit(ast, {
|
76
|
+
visitExportNamedDeclaration: (path) => {
|
77
|
+
const stagingName = path.node?.specifiers?.[0].local?.name;
|
78
|
+
name = typeof stagingName !== "string" ? "" : stagingName;
|
79
|
+
path.prune();
|
80
|
+
return false;
|
81
|
+
}
|
82
|
+
});
|
83
|
+
recast.visit(ast, {
|
84
|
+
visitIdentifier: (path) => {
|
85
|
+
if (path.node.name === name) {
|
86
|
+
path.node.name = "App";
|
87
|
+
}
|
88
|
+
return false;
|
89
|
+
}
|
90
|
+
});
|
91
|
+
return recast.print(ast).code;
|
92
|
+
}
|
93
|
+
async server() {
|
94
|
+
const output = this.compiled?.server || await this.bundle("ssr");
|
95
|
+
const Component = importFromStringSync(output, {
|
96
|
+
globals: { props: this.props }
|
97
|
+
}).default;
|
98
|
+
const { html, css } = await Component.render(this.locals);
|
99
|
+
return { output, html, css: css.code };
|
100
|
+
}
|
101
|
+
async build() {
|
102
|
+
try {
|
103
|
+
const serv = this.ssr ? await this.server() : null;
|
104
|
+
const cli = await this.client();
|
105
|
+
const comp = {
|
106
|
+
client: cli,
|
107
|
+
server: serv?.output
|
108
|
+
};
|
109
|
+
return {
|
110
|
+
client: cli,
|
111
|
+
server: serv,
|
112
|
+
compiled: comp
|
113
|
+
};
|
114
|
+
} catch (e) {
|
115
|
+
return { error: e };
|
116
|
+
}
|
117
|
+
}
|
118
|
+
}
|
119
|
+
var builder_default = Builder;
|
120
|
+
export {
|
121
|
+
builder_default as default
|
122
|
+
};
|
data/lib/svelte/railtie.rb
CHANGED
@@ -3,13 +3,15 @@ require "svelte/helpers"
|
|
3
3
|
|
4
4
|
module Svelte
|
5
5
|
class Railtie < ::Rails::Railtie
|
6
|
-
initializer "svelte" do
|
6
|
+
initializer "svelte" do |app|
|
7
7
|
ActiveSupport.on_load :action_view do
|
8
8
|
ActionView::Template.register_template_handler :svelte, Svelte::Handler
|
9
|
-
ActionView::Base.send :include, Svelte::Helpers
|
9
|
+
ActionView::Base.send :include, Svelte::Helpers
|
10
10
|
end
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
|
12
|
+
ActionController::Base.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
13
|
+
include Svelte::Variabilization
|
14
|
+
RUBY
|
15
|
+
end
|
14
16
|
end
|
15
17
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import Builder from "<%= "#{Svelte.gem_dir}/lib/svelte/js/builder.js" %>";
|
2
|
+
|
3
|
+
const bob = new Builder(
|
4
|
+
'<%= path %>',
|
5
|
+
JSON.parse('<%= Svelte.props.to_json || "{}" %>'),
|
6
|
+
JSON.parse('<%= locals.to_json || "{}" %>'),
|
7
|
+
'<%= compiled_client || "" %>',
|
8
|
+
'<%= compiled_server || "" %>',
|
9
|
+
<%= ssr %>,
|
10
|
+
'<%= Rails.root %>',
|
11
|
+
JSON.parse('<%= Svelte.preprocess.to_json || "{}" %>')
|
12
|
+
)
|
13
|
+
|
14
|
+
const built = await bob.build()
|
15
|
+
|
16
|
+
process.stdout.write(JSON.stringify(built))
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<is-land <%= Svelte::Helpers.destructure_attrs((locals.dig(:svelte, :island) rescue nil) || Svelte::ISLAND_ATTRS) %>>
|
2
|
+
<div id="hydratee-<%= digest %>">
|
3
|
+
<%= result.dig(:server, :html) %>
|
4
|
+
</div>
|
5
|
+
|
6
|
+
<style>
|
7
|
+
<%= result.dig(:server, :css) %>
|
8
|
+
</style>
|
9
|
+
<template data-island>
|
10
|
+
<script type="module">
|
11
|
+
<%= result.dig(:client) %>
|
12
|
+
|
13
|
+
new App({
|
14
|
+
target: document.getElementById("hydratee-<%= digest %>"),
|
15
|
+
props: JSON.parse('<%= locals.to_json %>'),
|
16
|
+
hydrate: true
|
17
|
+
});
|
18
|
+
</script>
|
19
|
+
</template>
|
20
|
+
</is-land>
|
data/lib/svelte/version.rb
CHANGED
data/lib/svelte.rb
CHANGED
@@ -1,17 +1,40 @@
|
|
1
|
+
require "active_support/core_ext"
|
2
|
+
require "svelte/variabilization"
|
1
3
|
require "svelte/railtie"
|
2
|
-
require "svelte/
|
4
|
+
require "svelte/errors"
|
3
5
|
require "svelte/helpers"
|
6
|
+
require "svelte/handler"
|
4
7
|
require "svelte/version"
|
5
8
|
require "active_support/isolated_execution_state"
|
6
9
|
|
7
10
|
module Svelte
|
11
|
+
include ActionView::Helpers::JavaScriptHelper
|
12
|
+
|
13
|
+
def self.configure
|
14
|
+
yield(self)
|
15
|
+
end
|
16
|
+
|
17
|
+
# @dynamic self.ssr, self.aliases, self.preprocess
|
18
|
+
# @dynamic self.ssr=, self.aliases=, self.preprocess=
|
8
19
|
mattr_accessor :ssr, default: true
|
9
|
-
|
20
|
+
mattr_accessor :aliases, default: {}
|
21
|
+
mattr_accessor :preprocess, default: {}
|
22
|
+
|
10
23
|
def self.props
|
11
|
-
ActiveSupport::IsolatedExecutionState[:svelte_props] ||= {}
|
24
|
+
ActiveSupport::IsolatedExecutionState[:svelte_props] ||= {} # steep:ignore UnknownConstant
|
12
25
|
end
|
13
|
-
|
26
|
+
|
14
27
|
def self.gem_dir
|
15
28
|
Gem::Specification.find_by_name("actionview-svelte-handler").gem_dir
|
16
29
|
end
|
30
|
+
|
31
|
+
def self.precedence(*args)
|
32
|
+
args.each do |v|
|
33
|
+
if !v.nil?
|
34
|
+
return v
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
nil
|
39
|
+
end
|
17
40
|
end
|
data/lib/ts/builder.ts
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
import { readable, Stores } from 'svelte/store'
|
2
|
+
import { importFromStringSync } from 'module-from-string'
|
3
|
+
import * as esbuild from 'esbuild'
|
4
|
+
import sveltePlugin from 'esbuild-svelte'
|
5
|
+
import { sveltePreprocess } from 'svelte-preprocess'
|
6
|
+
import type { Warning } from 'svelte/types/compiler/interfaces'
|
7
|
+
import * as recast from 'recast'
|
8
|
+
|
9
|
+
export interface BuildSuccess {
|
10
|
+
client: string
|
11
|
+
server: {
|
12
|
+
html: string
|
13
|
+
css: string
|
14
|
+
} | null
|
15
|
+
compiled: {
|
16
|
+
client: string
|
17
|
+
server: string | undefined
|
18
|
+
}
|
19
|
+
}
|
20
|
+
|
21
|
+
export interface BuildError {
|
22
|
+
error: any
|
23
|
+
}
|
24
|
+
|
25
|
+
export type BuildResult = BuildSuccess | BuildError
|
26
|
+
|
27
|
+
/**
|
28
|
+
* Builder is a class that builds, compiles and bundles Svelte components into a nice object for the template handler
|
29
|
+
*/
|
30
|
+
class Builder {
|
31
|
+
path: string
|
32
|
+
props: Stores
|
33
|
+
locals: object
|
34
|
+
compiled: {
|
35
|
+
client: string | null
|
36
|
+
server: string | null
|
37
|
+
}
|
38
|
+
|
39
|
+
ssr: boolean
|
40
|
+
workingDir: string
|
41
|
+
preprocess: object
|
42
|
+
|
43
|
+
constructor (
|
44
|
+
path: string,
|
45
|
+
props: object,
|
46
|
+
locals: object,
|
47
|
+
client: string | null,
|
48
|
+
server: string | null,
|
49
|
+
ssr: boolean,
|
50
|
+
workingDir: string,
|
51
|
+
preprocess: object
|
52
|
+
) {
|
53
|
+
this.path = path
|
54
|
+
this.props = readable(props)
|
55
|
+
this.locals = locals
|
56
|
+
this.compiled = {
|
57
|
+
client,
|
58
|
+
server
|
59
|
+
}
|
60
|
+
this.ssr = ssr
|
61
|
+
this.workingDir = workingDir
|
62
|
+
this.preprocess = preprocess
|
63
|
+
}
|
64
|
+
|
65
|
+
async bundle (generate: 'ssr' | 'dom', sveltePath = 'svelte'): Promise<string> {
|
66
|
+
const bundle = await esbuild.build({
|
67
|
+
entryPoints: [this.path],
|
68
|
+
mainFields: ['svelte', 'browser', 'module', 'main'],
|
69
|
+
conditions: ['svelte', 'browser'],
|
70
|
+
absWorkingDir: this.workingDir,
|
71
|
+
write: false,
|
72
|
+
outfile: 'component.js',
|
73
|
+
bundle: true,
|
74
|
+
format: 'esm',
|
75
|
+
metafile: true,
|
76
|
+
plugins: [
|
77
|
+
// @ts-expect-error
|
78
|
+
sveltePlugin({
|
79
|
+
compilerOptions: {
|
80
|
+
generate,
|
81
|
+
css: 'injected',
|
82
|
+
hydratable: true,
|
83
|
+
sveltePath
|
84
|
+
},
|
85
|
+
preprocess: sveltePreprocess(this.preprocess),
|
86
|
+
filterWarnings: (warning: Warning) => {
|
87
|
+
if (
|
88
|
+
warning.code === 'missing-declaration' &&
|
89
|
+
warning.message.includes("'props'")
|
90
|
+
) {
|
91
|
+
return false
|
92
|
+
}
|
93
|
+
return true
|
94
|
+
}
|
95
|
+
})
|
96
|
+
]
|
97
|
+
})
|
98
|
+
|
99
|
+
// @ts-expect-error
|
100
|
+
const throwables = [].concat(bundle.errors, bundle.warnings)
|
101
|
+
|
102
|
+
if (throwables.length > 0) {
|
103
|
+
throw throwables[0] // eslint-disable-line @typescript-eslint/no-throw-literal
|
104
|
+
}
|
105
|
+
|
106
|
+
return bundle.outputFiles[0].text
|
107
|
+
}
|
108
|
+
|
109
|
+
async client (): Promise<string> {
|
110
|
+
return (
|
111
|
+
this.compiled?.client || (this.standardizeClient(await this.bundle('dom', 'https://esm.sh/svelte'))) // eslint-disable-line
|
112
|
+
)
|
113
|
+
}
|
114
|
+
|
115
|
+
standardizeClient (code: string): string {
|
116
|
+
const ast = recast.parse(code)
|
117
|
+
|
118
|
+
let name: string | undefined
|
119
|
+
|
120
|
+
recast.visit(ast, {
|
121
|
+
visitExportNamedDeclaration: (path) => {
|
122
|
+
const stagingName: any = path.node?.specifiers?.[0].local?.name
|
123
|
+
name = typeof stagingName !== 'string' ? '' : stagingName
|
124
|
+
path.prune()
|
125
|
+
return false
|
126
|
+
}
|
127
|
+
})
|
128
|
+
|
129
|
+
recast.visit(ast, {
|
130
|
+
visitIdentifier: (path) => {
|
131
|
+
if (path.node.name === name) {
|
132
|
+
path.node.name = 'App'
|
133
|
+
}
|
134
|
+
return false
|
135
|
+
}
|
136
|
+
})
|
137
|
+
|
138
|
+
return recast.print(ast).code
|
139
|
+
}
|
140
|
+
|
141
|
+
async server (): Promise<{ output: string, html: string, css: string }> {
|
142
|
+
const output = this.compiled?.server || (await this.bundle('ssr')) // eslint-disable-line
|
143
|
+
|
144
|
+
const Component = importFromStringSync(output, {
|
145
|
+
globals: { props: this.props }
|
146
|
+
}).default
|
147
|
+
|
148
|
+
const { html, css } = await Component.render(this.locals)
|
149
|
+
|
150
|
+
return { output, html, css: css.code }
|
151
|
+
}
|
152
|
+
|
153
|
+
async build (): Promise<BuildResult> {
|
154
|
+
try {
|
155
|
+
const serv = this.ssr ? await this.server() : null
|
156
|
+
const cli = await this.client()
|
157
|
+
|
158
|
+
const comp = {
|
159
|
+
client: cli,
|
160
|
+
server: serv?.output
|
161
|
+
}
|
162
|
+
|
163
|
+
return {
|
164
|
+
client: cli,
|
165
|
+
server: serv,
|
166
|
+
compiled: comp
|
167
|
+
}
|
168
|
+
} catch (e) {
|
169
|
+
return { error: e }
|
170
|
+
}
|
171
|
+
}
|
172
|
+
}
|
173
|
+
|
174
|
+
export default Builder
|