upkeep-rails 0.2.5-aarch64-linux-gnu
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/LICENSE.txt +21 -0
- data/README.md +244 -0
- data/docs/drafts/turbo-streams-is-cache-invalidation-you-write-by-hand.md +240 -0
- data/docs/how-it-works.md +329 -0
- data/docs/plans/sqlglot-active-record-migration.md +211 -0
- data/docs/plans/turbo-frame-subscription-composition.md +90 -0
- data/docs/spikes/SQLGLOT_VS_AREL_FINDINGS.md +136 -0
- data/docs/spikes/query-source-analysis-comparison/FINDINGS.md +169 -0
- data/docs/spikes/sqlglot-first-active-record/README.md +26 -0
- data/docs/spikes/sqlglot-query-analysis/FINDINGS.md +110 -0
- data/docs/spikes/sqlglot-query-analysis/PULSE_1802_FINDINGS.md +95 -0
- data/docs/spikes/sqlglot-semantic-bindings/README.md +29 -0
- data/lib/generators/upkeep/install/install_generator.rb +192 -0
- data/lib/generators/upkeep/install/templates/create_upkeep_subscriptions.rb.erb +49 -0
- data/lib/generators/upkeep/install/templates/subscription.js +288 -0
- data/lib/generators/upkeep/install/templates/upkeep.rb +65 -0
- data/lib/upkeep/active_record_query.rb +248 -0
- data/lib/upkeep/capture/request.rb +150 -0
- data/lib/upkeep/dag/subscription_shape.rb +244 -0
- data/lib/upkeep/dag.rb +454 -0
- data/lib/upkeep/delivery/action_cable_adapter.rb +48 -0
- data/lib/upkeep/delivery/async_dispatcher.rb +102 -0
- data/lib/upkeep/delivery/broadcast_transport.rb +89 -0
- data/lib/upkeep/delivery/transport.rb +194 -0
- data/lib/upkeep/delivery/turbo_streams.rb +339 -0
- data/lib/upkeep/delivery.rb +7 -0
- data/lib/upkeep/dependencies.rb +600 -0
- data/lib/upkeep/herb/developer_report.rb +135 -0
- data/lib/upkeep/herb/manifest_cache.rb +83 -0
- data/lib/upkeep/herb/manifest_diff.rb +183 -0
- data/lib/upkeep/herb/source_instrumenter.rb +149 -0
- data/lib/upkeep/herb/template_manifest.rb +548 -0
- data/lib/upkeep/invalidation/collection_append.rb +84 -0
- data/lib/upkeep/invalidation/collection_member_replace.rb +78 -0
- data/lib/upkeep/invalidation/collection_prepend.rb +84 -0
- data/lib/upkeep/invalidation/collection_remove.rb +57 -0
- data/lib/upkeep/invalidation/planner.rb +411 -0
- data/lib/upkeep/invalidation.rb +7 -0
- data/lib/upkeep/rails/action_view_capture.rb +1007 -0
- data/lib/upkeep/rails/activation_token.rb +55 -0
- data/lib/upkeep/rails/cable/channel.rb +165 -0
- data/lib/upkeep/rails/cable/subscriber_identity.rb +361 -0
- data/lib/upkeep/rails/cable.rb +4 -0
- data/lib/upkeep/rails/client_subscription.rb +65 -0
- data/lib/upkeep/rails/cluster_guard.rb +57 -0
- data/lib/upkeep/rails/configuration.rb +252 -0
- data/lib/upkeep/rails/controller_runtime.rb +187 -0
- data/lib/upkeep/rails/install.rb +28 -0
- data/lib/upkeep/rails/job_runtime.rb +43 -0
- data/lib/upkeep/rails/railtie.rb +44 -0
- data/lib/upkeep/rails/replay.rb +244 -0
- data/lib/upkeep/rails/testing.rb +259 -0
- data/lib/upkeep/rails.rb +466 -0
- data/lib/upkeep/replay.rb +462 -0
- data/lib/upkeep/runtime.rb +1276 -0
- data/lib/upkeep/shared_streams.rb +86 -0
- data/lib/upkeep/sql_dependency_analysis.rb +553 -0
- data/lib/upkeep/sqlglot/libsqlglot_rust.so +0 -0
- data/lib/upkeep/sqlglot/native.rb +121 -0
- data/lib/upkeep/sqlglot/native_library.rb +23 -0
- data/lib/upkeep/sqlglot.rb +367 -0
- data/lib/upkeep/subscriptions/active_record_store.rb +398 -0
- data/lib/upkeep/subscriptions/active_record_subscription_persistence.rb +411 -0
- data/lib/upkeep/subscriptions/active_registry.rb +80 -0
- data/lib/upkeep/subscriptions/base_store.rb +110 -0
- data/lib/upkeep/subscriptions/json_snapshot.rb +98 -0
- data/lib/upkeep/subscriptions/layered_reverse_index.rb +125 -0
- data/lib/upkeep/subscriptions/lookup_instrumentation.rb +32 -0
- data/lib/upkeep/subscriptions/persistent_reverse_index.rb +228 -0
- data/lib/upkeep/subscriptions/registrar.rb +36 -0
- data/lib/upkeep/subscriptions/reverse_index.rb +313 -0
- data/lib/upkeep/subscriptions/shape.rb +117 -0
- data/lib/upkeep/subscriptions/store.rb +349 -0
- data/lib/upkeep/subscriptions.rb +7 -0
- data/lib/upkeep/targeting.rb +146 -0
- data/lib/upkeep/version.rb +5 -0
- data/lib/upkeep-rails.rb +3 -0
- data/lib/upkeep.rb +15 -0
- data/upkeep-rails.gemspec +66 -0
- metadata +327 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "template_manifest"
|
|
4
|
+
|
|
5
|
+
module Upkeep
|
|
6
|
+
module HerbSupport
|
|
7
|
+
class DeveloperReport
|
|
8
|
+
FALLBACK_ACTIONS = {
|
|
9
|
+
"helper_hidden_collection" => "Move collection rendering out of helper-only HTML and into an explicit render site.",
|
|
10
|
+
"manifest_runtime_mismatch" => "Inspect manifest provenance mismatches before trusting narrow updates.",
|
|
11
|
+
"multi_root_partial" => "Wrap the partial in one stable root element so it can carry a fragment marker.",
|
|
12
|
+
"no_herb_render_site" => "Extract updateable repeated markup into a partial render so Herb can plan a render site.",
|
|
13
|
+
"page_dependency_without_narrower_frame" => "Add a fragment or render-site boundary around the data-dependent region.",
|
|
14
|
+
"parse_failure" => "Fix the Herb parse error before using source-derived update addresses.",
|
|
15
|
+
"parse_recovered" => "Herb recovered with non-strict parsing, but Upkeep kept narrow source-derived addresses disabled. Fix the strict warnings to enable narrow updates.",
|
|
16
|
+
"preloaded_plain_data" => "Keep record identity available to the view or attach summaries to their source records."
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
def initialize(manifests:, proof_report: nil)
|
|
20
|
+
@manifests = manifests
|
|
21
|
+
@proof_report = proof_report
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
actions = template_actions + fallback_actions
|
|
26
|
+
|
|
27
|
+
{
|
|
28
|
+
summary: TemplateManifest.summary(manifests).merge(
|
|
29
|
+
page_fallback_reasons: page_fallback_reasons,
|
|
30
|
+
actionable_items: actions.size,
|
|
31
|
+
gate_passed: actions.all? { |action| action.fetch(:message) }
|
|
32
|
+
),
|
|
33
|
+
templates: manifests.map { |manifest| template_report(manifest) },
|
|
34
|
+
actions: actions
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
attr_reader :manifests, :proof_report
|
|
41
|
+
|
|
42
|
+
def template_report(manifest)
|
|
43
|
+
{
|
|
44
|
+
path: manifest.path,
|
|
45
|
+
kind: manifest.partial? ? "partial" : "page",
|
|
46
|
+
parse_ok: manifest.parse.fetch(:ok),
|
|
47
|
+
parse_recovered: manifest.recovered?,
|
|
48
|
+
strict_parse_errors: strict_parse_errors(manifest),
|
|
49
|
+
parse_warnings: parse_warnings(manifest),
|
|
50
|
+
render_sites: manifest.render_nodes.size,
|
|
51
|
+
recovered_render_sites: manifest.recovery_render_nodes.size,
|
|
52
|
+
fragment_root_tags: manifest.frontend_tag_plan.count { |tag| tag.fetch(:kind) == "fragment_root" },
|
|
53
|
+
helper_lowered_elements: manifest.helper_lowered_elements.size,
|
|
54
|
+
blockers: template_blockers(manifest)
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def template_actions
|
|
59
|
+
manifests.flat_map do |manifest|
|
|
60
|
+
template_blockers(manifest).map do |blocker|
|
|
61
|
+
action = {
|
|
62
|
+
source: "template",
|
|
63
|
+
path: manifest.path,
|
|
64
|
+
reason: blocker,
|
|
65
|
+
message: template_action_message(blocker)
|
|
66
|
+
}
|
|
67
|
+
action[:recovered_render_sites] = manifest.recovery_render_nodes.size if blocker == "parse_recovered"
|
|
68
|
+
action
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def template_blockers(manifest)
|
|
74
|
+
blockers = []
|
|
75
|
+
blockers << (manifest.recovered? ? "parse_recovered" : "parse_failure") unless manifest.parse.fetch(:ok)
|
|
76
|
+
blockers << "partial_without_single_root" if manifest.partial? && manifest.parse.fetch(:ok) && !manifest.root_shape.fetch(:single_root, false)
|
|
77
|
+
blockers << "page_without_render_site" if !manifest.partial? && manifest.parse.fetch(:ok) && manifest.render_nodes.empty?
|
|
78
|
+
blockers
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def template_action_message(blocker)
|
|
82
|
+
case blocker
|
|
83
|
+
when "parse_failure"
|
|
84
|
+
FALLBACK_ACTIONS.fetch("parse_failure")
|
|
85
|
+
when "parse_recovered"
|
|
86
|
+
FALLBACK_ACTIONS.fetch("parse_recovered")
|
|
87
|
+
when "partial_without_single_root"
|
|
88
|
+
FALLBACK_ACTIONS.fetch("multi_root_partial")
|
|
89
|
+
when "page_without_render_site"
|
|
90
|
+
FALLBACK_ACTIONS.fetch("no_herb_render_site")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def fallback_actions
|
|
95
|
+
proof_cases.flat_map do |test_case|
|
|
96
|
+
test_case.fetch(:selected_targets).filter_map do |target|
|
|
97
|
+
next unless target.fetch(:kind) == "page"
|
|
98
|
+
|
|
99
|
+
fallback_reason = target[:fallback_reason]
|
|
100
|
+
next unless fallback_reason
|
|
101
|
+
|
|
102
|
+
{
|
|
103
|
+
source: "proof",
|
|
104
|
+
case: test_case.fetch(:name),
|
|
105
|
+
target: target.fetch(:id),
|
|
106
|
+
reason: fallback_reason,
|
|
107
|
+
message: FALLBACK_ACTIONS.fetch(fallback_reason, "Add a narrower template boundary or keep the page fallback.")
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def proof_cases
|
|
114
|
+
Array(proof_report&.fetch(:cases, []))
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def page_fallback_reasons
|
|
118
|
+
proof_report&.fetch(:summary, {})&.fetch(:page_fallback_reasons, {}) || {}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def strict_parse_errors(manifest)
|
|
122
|
+
Array(manifest.parse[:errors])
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def parse_warnings(manifest)
|
|
126
|
+
warnings = Array(manifest.parse[:warnings])
|
|
127
|
+
return warnings unless manifest.recovered?
|
|
128
|
+
|
|
129
|
+
warnings + strict_parse_errors(manifest).map do |error|
|
|
130
|
+
error.merge(severity: "warning", recovery: "non_strict")
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require_relative "manifest_diff"
|
|
5
|
+
|
|
6
|
+
module Upkeep
|
|
7
|
+
module HerbSupport
|
|
8
|
+
class ManifestCache
|
|
9
|
+
Entry = Data.define(:path, :source_digest, :source, :manifest, :last_update)
|
|
10
|
+
|
|
11
|
+
attr_reader :entries
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@entries = {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def fetch(path:, source:, parse_options: ManifestDiff::PARSE_OPTIONS)
|
|
18
|
+
source_digest = digest(source)
|
|
19
|
+
entry = entries[path]
|
|
20
|
+
|
|
21
|
+
return entry.manifest if entry&.source_digest == source_digest
|
|
22
|
+
|
|
23
|
+
update = update_for(path: path, old_source: entry&.source, new_source: source, parse_options: parse_options)
|
|
24
|
+
manifest = update[:new_manifest] || TemplateManifest.build(path: path, source: source, parse_options: parse_options)
|
|
25
|
+
entries[path] = Entry.new(path, source_digest, source.dup, manifest, update_payload(update))
|
|
26
|
+
|
|
27
|
+
manifest
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def last_update_for(path)
|
|
31
|
+
entries.fetch(path).last_update
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def summary
|
|
35
|
+
updates = entries.values.map(&:last_update)
|
|
36
|
+
|
|
37
|
+
{
|
|
38
|
+
entries: entries.size,
|
|
39
|
+
actions: updates.map { |update| update.fetch(:action) }.tally,
|
|
40
|
+
topology_changes: updates.count { |update| update.fetch(:topology_changed, false) }
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def clear
|
|
45
|
+
entries.clear
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def update_for(path:, old_source:, new_source:, parse_options:)
|
|
51
|
+
return initial_update(path: path, source: new_source, parse_options: parse_options) unless old_source
|
|
52
|
+
|
|
53
|
+
ManifestDiff.plan(path: path, old_source: old_source, new_source: new_source, parse_options: parse_options).to_h
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def initial_update(path:, source:, parse_options:)
|
|
57
|
+
manifest = TemplateManifest.build(path: path, source: source, parse_options: parse_options)
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
path: path,
|
|
61
|
+
action: "initial_build",
|
|
62
|
+
reason: "new_template",
|
|
63
|
+
topology_changed: true,
|
|
64
|
+
diff_identical: false,
|
|
65
|
+
operation_types: [],
|
|
66
|
+
operations: [],
|
|
67
|
+
new_manifest: manifest,
|
|
68
|
+
new_manifest_fingerprint: manifest.fingerprint,
|
|
69
|
+
stable_topology: false,
|
|
70
|
+
gate_passed: manifest.parse.fetch(:ok)
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def update_payload(update)
|
|
75
|
+
update.reject { |key, _value| %i[old_manifest new_manifest old_topology_signature new_topology_signature].include?(key) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def digest(source)
|
|
79
|
+
Digest::SHA256.hexdigest(source)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "template_manifest"
|
|
4
|
+
|
|
5
|
+
module Upkeep
|
|
6
|
+
module HerbSupport
|
|
7
|
+
class ManifestDiff
|
|
8
|
+
PARSE_OPTIONS = TemplateManifest::DEFAULT_PARSE_OPTIONS.merge(
|
|
9
|
+
action_view_helpers: false,
|
|
10
|
+
transform_conditionals: false
|
|
11
|
+
).freeze
|
|
12
|
+
|
|
13
|
+
CONTENT_ONLY_OPERATION_TYPES = %i[
|
|
14
|
+
attribute_added
|
|
15
|
+
attribute_removed
|
|
16
|
+
attribute_value_changed
|
|
17
|
+
text_changed
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
Plan = Data.define(
|
|
21
|
+
:path,
|
|
22
|
+
:action,
|
|
23
|
+
:reason,
|
|
24
|
+
:topology_changed,
|
|
25
|
+
:diff_identical,
|
|
26
|
+
:operation_types,
|
|
27
|
+
:operations,
|
|
28
|
+
:old_manifest,
|
|
29
|
+
:new_manifest,
|
|
30
|
+
:old_topology_signature,
|
|
31
|
+
:new_topology_signature,
|
|
32
|
+
:error
|
|
33
|
+
) do
|
|
34
|
+
def gate_passed?
|
|
35
|
+
error.nil?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def to_h
|
|
39
|
+
{
|
|
40
|
+
path: path,
|
|
41
|
+
action: action,
|
|
42
|
+
reason: reason,
|
|
43
|
+
topology_changed: topology_changed,
|
|
44
|
+
diff_identical: diff_identical,
|
|
45
|
+
operation_types: operation_types,
|
|
46
|
+
operations: operations,
|
|
47
|
+
old_manifest_fingerprint: old_manifest&.fingerprint,
|
|
48
|
+
new_manifest_fingerprint: new_manifest&.fingerprint,
|
|
49
|
+
stable_topology: old_topology_signature == new_topology_signature,
|
|
50
|
+
gate_passed: gate_passed?,
|
|
51
|
+
error: error
|
|
52
|
+
}.compact
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.plan(path:, old_source:, new_source:, parse_options: PARSE_OPTIONS)
|
|
57
|
+
new(path: path, old_source: old_source, new_source: new_source, parse_options: parse_options).plan
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def initialize(path:, old_source:, new_source:, parse_options: PARSE_OPTIONS)
|
|
61
|
+
@path = path
|
|
62
|
+
@old_source = old_source
|
|
63
|
+
@new_source = new_source
|
|
64
|
+
@parse_options = parse_options
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def plan
|
|
68
|
+
old_manifest = build_manifest(old_source)
|
|
69
|
+
new_manifest = build_manifest(new_source)
|
|
70
|
+
diff_result = ::Herb.diff(old_source, new_source)
|
|
71
|
+
old_signature = topology_signature(old_manifest)
|
|
72
|
+
new_signature = topology_signature(new_manifest)
|
|
73
|
+
action, reason, topology_changed = classify(
|
|
74
|
+
diff_result: diff_result,
|
|
75
|
+
old_manifest: old_manifest,
|
|
76
|
+
new_manifest: new_manifest,
|
|
77
|
+
old_signature: old_signature,
|
|
78
|
+
new_signature: new_signature
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
Plan.new(
|
|
82
|
+
path: path,
|
|
83
|
+
action: action,
|
|
84
|
+
reason: reason,
|
|
85
|
+
topology_changed: topology_changed,
|
|
86
|
+
diff_identical: diff_result.identical?,
|
|
87
|
+
operation_types: diff_result.map { |operation| operation.type.to_s },
|
|
88
|
+
operations: diff_result.map { |operation| operation_payload(operation) },
|
|
89
|
+
old_manifest: old_manifest,
|
|
90
|
+
new_manifest: new_manifest,
|
|
91
|
+
old_topology_signature: old_signature,
|
|
92
|
+
new_topology_signature: new_signature,
|
|
93
|
+
error: nil
|
|
94
|
+
)
|
|
95
|
+
rescue StandardError => error
|
|
96
|
+
Plan.new(
|
|
97
|
+
path: path,
|
|
98
|
+
action: "full_rebuild",
|
|
99
|
+
reason: "herb_diff_failed",
|
|
100
|
+
topology_changed: true,
|
|
101
|
+
diff_identical: false,
|
|
102
|
+
operation_types: [],
|
|
103
|
+
operations: [],
|
|
104
|
+
old_manifest: nil,
|
|
105
|
+
new_manifest: nil,
|
|
106
|
+
old_topology_signature: nil,
|
|
107
|
+
new_topology_signature: nil,
|
|
108
|
+
error: { class: error.class.name, message: error.message }
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
attr_reader :path, :old_source, :new_source, :parse_options
|
|
115
|
+
|
|
116
|
+
def build_manifest(source)
|
|
117
|
+
TemplateManifest.build(path: path, source: source, parse_options: parse_options)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def classify(diff_result:, old_manifest:, new_manifest:, old_signature:, new_signature:)
|
|
121
|
+
return ["full_rebuild", "manifest_parse_failure", true] unless old_manifest.parse.fetch(:ok) && new_manifest.parse.fetch(:ok)
|
|
122
|
+
return ["noop", "identical_source", false] if diff_result.identical?
|
|
123
|
+
|
|
124
|
+
operation_types = diff_result.map(&:type)
|
|
125
|
+
stable_topology = old_signature == new_signature
|
|
126
|
+
|
|
127
|
+
if content_only?(operation_types) && stable_topology
|
|
128
|
+
["refresh_manifest", "content_only_stable_topology", false]
|
|
129
|
+
elsif stable_topology
|
|
130
|
+
["rebuild_manifest", "dynamic_template_operation", true]
|
|
131
|
+
else
|
|
132
|
+
["rebuild_manifest", "manifest_topology_changed", true]
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def content_only?(operation_types)
|
|
137
|
+
(operation_types - CONTENT_ONLY_OPERATION_TYPES).empty?
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def topology_signature(manifest)
|
|
141
|
+
return { parse_ok: false } unless manifest.parse.fetch(:ok)
|
|
142
|
+
|
|
143
|
+
{
|
|
144
|
+
root_shape: manifest.root_shape.slice(:significant_children, :root_elements, :root_types, :single_root, :multi_root),
|
|
145
|
+
fragment_roots: fragment_root_signature(manifest),
|
|
146
|
+
render_sites: render_site_signature(manifest)
|
|
147
|
+
}
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def fragment_root_signature(manifest)
|
|
151
|
+
manifest.frontend_tag_plan
|
|
152
|
+
.select { |tag| tag.fetch(:kind) == "fragment_root" }
|
|
153
|
+
.map { |tag| { tag_name: tag.fetch(:tag_name), attributes: tag.fetch(:attributes).map { |attribute| attribute.fetch(:name) } } }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def render_site_signature(manifest)
|
|
157
|
+
manifest.render_nodes.map do |render_node|
|
|
158
|
+
{
|
|
159
|
+
kind: render_node.fetch(:kind),
|
|
160
|
+
partial: render_node.fetch(:partial),
|
|
161
|
+
template_path: render_node.fetch(:template_path),
|
|
162
|
+
collection: render_node.fetch(:collection),
|
|
163
|
+
object: render_node.fetch(:object),
|
|
164
|
+
as: render_node.fetch(:as),
|
|
165
|
+
locals: render_node.fetch(:locals),
|
|
166
|
+
block_arguments: render_node.fetch(:block_arguments)
|
|
167
|
+
}
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def operation_payload(operation)
|
|
172
|
+
{
|
|
173
|
+
type: operation.type.to_s,
|
|
174
|
+
path: operation.path,
|
|
175
|
+
old_node: operation.old_node&.class&.name,
|
|
176
|
+
new_node: operation.new_node&.class&.name,
|
|
177
|
+
old_index: operation.old_index,
|
|
178
|
+
new_index: operation.new_index
|
|
179
|
+
}.compact
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "template_manifest"
|
|
4
|
+
|
|
5
|
+
module Upkeep
|
|
6
|
+
module HerbSupport
|
|
7
|
+
class SourceInstrumenter
|
|
8
|
+
def initialize(manifest:)
|
|
9
|
+
@manifest = manifest
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def instrument(source)
|
|
13
|
+
return source unless manifest.parse.fetch(:ok) || manifest.recovered?
|
|
14
|
+
|
|
15
|
+
apply_replacements(source, replacements_for(source))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader :manifest
|
|
21
|
+
|
|
22
|
+
def replacements_for(source)
|
|
23
|
+
render_site_replacements + marker_replacements(source)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def render_site_replacements
|
|
27
|
+
return [] unless manifest.parse.fetch(:ok)
|
|
28
|
+
|
|
29
|
+
manifest.render_nodes.select { |render_node| render_node.fetch(:render_site_container) }.map do |render_node|
|
|
30
|
+
[
|
|
31
|
+
render_node.fetch(:start_offset),
|
|
32
|
+
render_node.fetch(:end_offset),
|
|
33
|
+
%(<%= upkeep_frame("#{render_node.fetch(:site_id)}", manifest_path: "#{manifest.path}", manifest_fingerprint: "#{manifest.fingerprint}") { #{render_node.fetch(:expression)} } %>)
|
|
34
|
+
]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def marker_replacements(source)
|
|
39
|
+
frontend_tag_plan_for_instrumentation
|
|
40
|
+
.filter_map { |tag| root_marker_replacement(source, tag) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def frontend_tag_plan_for_instrumentation
|
|
44
|
+
return manifest.frontend_tag_plan.select { |entry| %w[fragment_root page_root render_site].include?(entry.fetch(:kind)) } if manifest.parse.fetch(:ok)
|
|
45
|
+
|
|
46
|
+
manifest.recovery_frontend_tag_plan.select { |entry| %w[fragment_root page_root].include?(entry.fetch(:kind)) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def root_marker_replacement(source, tag)
|
|
50
|
+
return helper_marker_replacement(source, tag) if helper_lowered_tag?(tag)
|
|
51
|
+
|
|
52
|
+
offset = offset_for_location(source, tag.fetch(:location).fetch(:start))
|
|
53
|
+
open_tag_end = source.index(">", offset)
|
|
54
|
+
return unless open_tag_end
|
|
55
|
+
|
|
56
|
+
open_tag = source[offset...open_tag_end]
|
|
57
|
+
return if tag.fetch(:attributes).any? { |attribute| open_tag.include?(%(#{attribute.fetch(:name)}=)) }
|
|
58
|
+
|
|
59
|
+
insert_at = fragment_root_insert_offset(source, offset, tag.fetch(:tag_name))
|
|
60
|
+
[insert_at, insert_at, " #{attributes_source(tag.fetch(:attributes))}"]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def helper_marker_replacement(source, tag)
|
|
64
|
+
return unless tag_helper_source?(tag)
|
|
65
|
+
|
|
66
|
+
offset = offset_for_location(source, tag.fetch(:location).fetch(:start))
|
|
67
|
+
erb_end = source.index("%>", offset)
|
|
68
|
+
return unless erb_end
|
|
69
|
+
|
|
70
|
+
opening = source[offset...erb_end]
|
|
71
|
+
return if tag.fetch(:attributes).any? { |attribute| opening.include?(attribute.fetch(:name)) }
|
|
72
|
+
|
|
73
|
+
block_match = /\s+do(?:\s*\|[^|]*\|)?\s*\z/.match(opening)
|
|
74
|
+
return unless block_match
|
|
75
|
+
|
|
76
|
+
before_block = opening[0...block_match.begin(0)].rstrip
|
|
77
|
+
insert_at = offset + block_match.begin(0)
|
|
78
|
+
[
|
|
79
|
+
insert_at,
|
|
80
|
+
insert_at,
|
|
81
|
+
"#{helper_attribute_separator(before_block)}#{helper_attributes_source(tag.fetch(:attributes))}"
|
|
82
|
+
]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def helper_lowered_tag?(tag)
|
|
86
|
+
tag.fetch(:element_source) != "HTML"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def tag_helper_source?(tag)
|
|
90
|
+
[
|
|
91
|
+
"ActionView::Helpers::TagHelper#tag",
|
|
92
|
+
"ActionView::Helpers::TagHelper#content_tag"
|
|
93
|
+
].include?(tag.fetch(:element_source))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def helper_attribute_separator(before_block)
|
|
97
|
+
return ", " if before_block.match?(/\bcontent_tag\b/)
|
|
98
|
+
|
|
99
|
+
tag_call = before_block.match(/<%=?\s*tag\.[a-zA-Z_][a-zA-Z0-9_:-]*/)
|
|
100
|
+
return " " if tag_call && before_block[tag_call.end(0)..].to_s.strip.empty?
|
|
101
|
+
|
|
102
|
+
", "
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def helper_attributes_source(attributes)
|
|
106
|
+
attributes.map do |attribute|
|
|
107
|
+
%(#{attribute.fetch(:name).inspect} => #{ruby_attribute_value(attribute.fetch(:value))})
|
|
108
|
+
end.join(", ")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def ruby_attribute_value(value)
|
|
112
|
+
if (match = /\A<%=\s*(.*?)\s*%>\z/.match(value.to_s))
|
|
113
|
+
match[1]
|
|
114
|
+
else
|
|
115
|
+
value.inspect
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def fragment_root_insert_offset(source, offset, tag_name)
|
|
120
|
+
match = /\A<\s*#{Regexp.escape(tag_name)}\b/i.match(source[offset..])
|
|
121
|
+
raise "could not locate fragment root tag for #{manifest.path}" unless match
|
|
122
|
+
|
|
123
|
+
offset + match.end(0)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def attributes_source(attributes)
|
|
127
|
+
attributes.map { |attribute| %(#{attribute.fetch(:name)}="#{attribute.fetch(:value)}") }.join(" ")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def apply_replacements(source, replacements)
|
|
131
|
+
replacements.sort_by { |start_offset, end_offset, _replacement| [-start_offset, -end_offset] }.each_with_object(source.dup) do |(start_offset, end_offset, replacement), result|
|
|
132
|
+
result[start_offset...end_offset] = replacement
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def offset_for_location(source, position)
|
|
137
|
+
line_offsets(source).fetch(position.fetch(:line) - 1) + position.fetch(:column)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def line_offsets(source)
|
|
141
|
+
offsets = [0]
|
|
142
|
+
source.each_line(chomp: false).with_index do |line, index|
|
|
143
|
+
offsets[index + 1] = offsets[index] + line.bytesize
|
|
144
|
+
end
|
|
145
|
+
offsets
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|