actionview-svelte-handler 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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"}
@@ -1,37 +1,67 @@
1
- require 'erb'
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
- include ActionView::Helpers::JavaScriptHelper
6
- def call(template, source)
7
- return <<-RUBY
8
- locals = j(local_assigns.to_json).presence
9
- props = j(Svelte.props.to_json).presence
10
- source = j('#{source}')
11
- ssr = local_assigns.dig(:svelte, :ssr) != nil ? local_assigns.dig(:svelte, :ssr) : Svelte.ssr
12
-
13
- assembler = Tempfile.new(['assembler', '.mjs'], "#{Svelte.gem_dir}/tmp")
14
- assembler.write(ERB.new(File.read("#{Svelte.gem_dir}/lib/svelte/templates/assembler.js.erb")).result_with_hash({
15
- source: source,
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
- ssr: ssr
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
- island = ERB.new(File.read("#{Svelte.gem_dir}/lib/svelte/templates/island.html.erb")).result_with_hash({ result: result, locals: local_assigns })
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
50
+
51
+ assembler&.close
52
+ assembler&.unlink
53
+
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)
24
56
 
25
- assembler.close
26
- assembler.unlink
57
+ content_for(:head, (result.dig(:server, :head) || "").html_safe)
27
58
 
28
- return island
59
+ ERB.new(File.read("#{Svelte.gem_dir}/lib/svelte/templates/island.html.erb")).result_with_hash({ result:, locals: local_assigns, digest: "#{digest}", bind: binding })
29
60
  RUBY
30
61
  end
31
-
32
62
 
33
63
  def self.call(template, source)
34
64
  new.call(template, source)
35
65
  end
36
66
  end
37
- end
67
+ end
@@ -1,16 +1,25 @@
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
6
6
  import "https://esm.sh/@11ty/is-land";
7
- import "https://esm.sh/@11ty/is-land/is-land-autoinit.js";
8
7
  import { readable } from "https://esm.sh/svelte/store"
9
8
 
10
9
  window.props = readable(JSON.parse("#{j(Svelte.props.to_json)}"))
11
10
  JS
12
-
13
- tag.script(js.html_safe, type: "module")
11
+
12
+ content_for(:head) + "\n" + tag.script(js.html_safe, type: "module") # steep:ignore RequiredBlockMissing
13
+ end
14
+
15
+ delegate :destructure_attrs, to: :class
16
+
17
+ def self.destructure_attrs(hash)
18
+ string = ""
19
+ hash.each do |k, v|
20
+ string += "#{k}='#{j v}' "
21
+ end
22
+ string
14
23
  end
15
24
  end
16
- end
25
+ end
@@ -0,0 +1,2 @@
1
+ > [!CAUTION]
2
+ > This folder is intended to be built from TypeScript files with `npm run build`
@@ -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, head } = await Component.render(this.locals);
99
+ return { output, html, head, 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
+ };
@@ -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
- `npm install #{Svelte.gem_dir} --install-links --save=false`
13
- end
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>
@@ -0,0 +1,11 @@
1
+ module Svelte
2
+ module Variabilization
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ before_action do
7
+ request.variant = Array(request.variant).concat([:client, :server])
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,3 +1,3 @@
1
1
  module Svelte
2
- VERSION = "0.3.0"
2
+ VERSION = "0.5.0"
3
3
  end
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/handler"
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
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :svelte do
3
+ # # Task goes here
4
+ # end