actionview-svelte-handler 0.2.0 → 0.4.1

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,53 +1,65 @@
1
+ require "erb"
2
+ require "svelte/helpers"
3
+
1
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
+
2
11
  class Handler
3
- include ActionView::Helpers::JavaScriptHelper
4
12
  def call(template, source)
5
- file = Tempfile.new
6
- src = Tempfile.new
7
- src.write source.b
8
- src.rewind
9
-
10
- file.write <<-JS
11
- import { compile } from 'svelte/compiler';
13
+ digest = Digest::SHA256.base64digest(source)
12
14
 
13
- const src = await Bun.file("#{src.path}").text()
14
- const result = compile(src, {generate: "dom", name: "Component", css: "injected", sveltePath: "https://esm.sh/svelte"})
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}',
36
+ locals: local_assigns,
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)
40
+ }))
41
+
42
+ assembler.rewind
15
43
 
16
- process.stdout.write(JSON.stringify(result))
17
- JS
18
-
19
- file.rewind
20
-
21
- compiled = JSON.parse(%x(bun run #{file.path})).deep_symbolize_keys
22
-
23
- return <<-RUBY
24
- Svelte::Handler.generate_island(#{(compiled.dig(:js, :code) || "").dump}, local_assigns.to_json.presence)
44
+ result = JSON.parse(`NODE_NO_WARNINGS=1 node --experimental-vm-modules \#{assembler.path}`).deep_symbolize_keys
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)
56
+
57
+ ERB.new(File.read("#{Svelte.gem_dir}/lib/svelte/templates/island.html.erb")).result_with_hash({ result:, locals: local_assigns, digest: "#{digest}"})
25
58
  RUBY
26
59
  end
27
60
 
28
61
  def self.call(template, source)
29
62
  new.call(template, source)
30
63
  end
31
-
32
- def self.generate_island(js, props)
33
- return <<-HTML
34
- <is-land on:visible on:idle>
35
- <template data-island>
36
- <div id="svelte-app-client">
37
- </div>
38
-
39
- <script type="module">
40
- #{js}
41
-
42
- new Component({
43
- target: document.getElementById("svelte-app-client"),
44
- props: JSON.parse(#{props.to_json.presence || "{}"})
45
- });
46
- </script>
47
-
48
- </template>
49
- </is-land>
50
- HTML
51
- end
52
64
  end
53
- end
65
+ end
@@ -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,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 } = 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
+ };
@@ -3,11 +3,15 @@ require "svelte/helpers"
3
3
 
4
4
  module Svelte
5
5
  class Railtie < ::Rails::Railtie
6
- initializer "svelte.register_actionview" 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
9
  ActionView::Base.send :include, Svelte::Helpers
10
10
  end
11
+
12
+ ActionController::Base.class_eval <<-RUBY, __FILE__, __LINE__ + 1
13
+ include Svelte::Variabilization
14
+ RUBY
11
15
  end
12
16
  end
13
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.2.0"
2
+ VERSION = "0.4.1"
3
3
  end
data/lib/svelte.rb CHANGED
@@ -1,11 +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=
19
+ mattr_accessor :ssr, default: true
20
+ mattr_accessor :aliases, default: {}
21
+ mattr_accessor :preprocess, default: {}
22
+
8
23
  def self.props
9
- ActiveSupport::IsolatedExecutionState[:svelte_props] ||= {}
24
+ ActiveSupport::IsolatedExecutionState[:svelte_props] ||= {} # steep:ignore UnknownConstant
25
+ end
26
+
27
+ def self.gem_dir
28
+ Gem::Specification.find_by_name("actionview-svelte-handler").gem_dir
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
10
39
  end
11
40
  end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :svelte do
3
+ # # Task goes here
4
+ # end