actionview-svelte-handler 0.3.0 → 0.5.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,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