trmnl_preview 0.7.1 → 0.8.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +54 -1
- data/README.md +180 -5
- data/bin/rake +6 -6
- data/bin/trmnlp +1 -0
- data/db/data/form_fields.yml +24 -0
- data/db/data/framework_versions.yml +72 -0
- data/lib/trmnlp/api_client.rb +41 -28
- data/lib/trmnlp/app.rb +73 -44
- data/lib/trmnlp/browser_pool.rb +82 -0
- data/lib/trmnlp/cli.rb +24 -11
- data/lib/trmnlp/commands/base.rb +33 -10
- data/lib/trmnlp/commands/build.rb +13 -8
- data/lib/trmnlp/commands/clone.rb +12 -7
- data/lib/trmnlp/commands/init.rb +17 -13
- data/lib/trmnlp/commands/lint.rb +42 -0
- data/lib/trmnlp/commands/list.rb +40 -0
- data/lib/trmnlp/commands/login.rb +28 -13
- data/lib/trmnlp/commands/pull.rb +14 -6
- data/lib/trmnlp/commands/push.rb +29 -19
- data/lib/trmnlp/commands/serve.rb +32 -3
- data/lib/trmnlp/commands.rb +3 -1
- data/lib/trmnlp/config/app.rb +6 -3
- data/lib/trmnlp/config/plugin.rb +56 -14
- data/lib/trmnlp/config/project.rb +59 -7
- data/lib/trmnlp/config.rb +3 -1
- data/lib/trmnlp/context.rb +21 -224
- data/lib/trmnlp/errors.rb +15 -0
- data/lib/trmnlp/form_field.rb +42 -0
- data/lib/trmnlp/framework_version.rb +69 -0
- data/lib/trmnlp/image_quantizer.rb +58 -0
- data/lib/trmnlp/lint/check.rb +31 -0
- data/lib/trmnlp/lint/checks/custom_fields_used.rb +32 -0
- data/lib/trmnlp/lint/checks/form_fields_valid.rb +20 -0
- data/lib/trmnlp/lint/checks/highcharts_animations_disabled.rb +23 -0
- data/lib/trmnlp/lint/checks/highcharts_elements_unique.rb +24 -0
- data/lib/trmnlp/lint/checks/image_links_reachable.rb +53 -0
- data/lib/trmnlp/lint/checks/layouts_have_content.rb +24 -0
- data/lib/trmnlp/lint/checks/limited_inline_styles.rb +26 -0
- data/lib/trmnlp/lint/checks/no_async_functions.rb +18 -0
- data/lib/trmnlp/lint/checks/no_opacity.rb +19 -0
- data/lib/trmnlp/lint/checks/no_size_classes.rb +19 -0
- data/lib/trmnlp/lint/checks/title_casing.rb +20 -0
- data/lib/trmnlp/lint/checks/title_length.rb +18 -0
- data/lib/trmnlp/lint/checks/waits_for_dom_load.rb +23 -0
- data/lib/trmnlp/lint/source.rb +42 -0
- data/lib/trmnlp/lint.rb +39 -0
- data/lib/trmnlp/paths.rb +28 -8
- data/lib/trmnlp/poller.rb +105 -0
- data/lib/trmnlp/renderer.rb +87 -0
- data/lib/trmnlp/reporter.rb +28 -0
- data/lib/trmnlp/screen.rb +16 -0
- data/lib/trmnlp/screen_generator.rb +11 -217
- data/lib/trmnlp/screenshot.rb +96 -0
- data/lib/trmnlp/transform_backend/http.rb +107 -0
- data/lib/trmnlp/transform_backend/subprocess.rb +130 -0
- data/lib/trmnlp/transform_backend/wrapper.rb +113 -0
- data/lib/trmnlp/transform_client.rb +47 -0
- data/lib/trmnlp/transform_pipeline.rb +65 -0
- data/lib/trmnlp/user_data_assembler.rb +96 -0
- data/lib/trmnlp/version.rb +1 -1
- data/lib/trmnlp/watcher.rb +60 -0
- data/lib/trmnlp.rb +6 -10
- data/templates/init/bin/trmnlp +1 -1
- data/templates/init/src/settings.yml +1 -0
- data/templates/init/src/transform.py.example +14 -0
- data/trmnl_preview.gemspec +34 -34
- data/web/public/index.css +6 -0
- data/web/public/index.js +31 -18
- data/web/views/index.erb +6 -1
- data/web/views/render_html.erb +4 -2
- metadata +81 -56
|
@@ -1,27 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative 'base'
|
|
4
|
+
require_relative '../api_client'
|
|
2
5
|
|
|
3
6
|
module TRMNLP
|
|
4
7
|
module Commands
|
|
5
8
|
class Login < Base
|
|
9
|
+
Options = Data.define(:dir, :quiet)
|
|
10
|
+
|
|
6
11
|
def call
|
|
7
12
|
if config.app.logged_in?
|
|
8
|
-
anonymous_key = config.app.api_key[0..10] + '*' * (config.app.api_key.length - 11)
|
|
9
|
-
|
|
10
|
-
confirm = prompt(
|
|
13
|
+
anonymous_key = config.app.api_key[0..10] + ('*' * (config.app.api_key.length - 11))
|
|
14
|
+
reporter.info "Currently authenticated as: #{anonymous_key}"
|
|
15
|
+
confirm = prompt('You are already authenticated. Do you want to re-authenticate? (y/N): ')
|
|
11
16
|
return unless confirm.strip.downcase == 'y'
|
|
12
17
|
end
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
api_key = prompt(
|
|
17
|
-
raise
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
reporter.info "Please visit #{config.app.account_uri} to grab your API key, then paste it here."
|
|
20
|
+
|
|
21
|
+
api_key = prompt('API Key: ')
|
|
22
|
+
raise InvalidApiKey, 'API key cannot be empty' if api_key.empty?
|
|
23
|
+
unless api_key.start_with?('user_')
|
|
24
|
+
raise InvalidApiKey,
|
|
25
|
+
'Invalid API key; did you copy it from the right place?'
|
|
26
|
+
end
|
|
27
|
+
|
|
20
28
|
config.app.api_key = api_key
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
|
|
30
|
+
api_client = APIClient.new(config)
|
|
31
|
+
begin
|
|
32
|
+
user_info = api_client.get_me
|
|
33
|
+
reporter.info "Authenticated as #{user_info['name']} (#{user_info['email']})"
|
|
34
|
+
config.app.save
|
|
35
|
+
reporter.info "Saved changes to #{paths.app_config}"
|
|
36
|
+
rescue StandardError => e
|
|
37
|
+
raise AuthenticationFailed, "Authentication failed; changes were not saved.\n#{e.message}"
|
|
38
|
+
end
|
|
24
39
|
end
|
|
25
40
|
end
|
|
26
41
|
end
|
|
27
|
-
end
|
|
42
|
+
end
|
data/lib/trmnlp/commands/pull.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'zip'
|
|
2
4
|
|
|
3
5
|
require_relative 'base'
|
|
@@ -6,16 +8,18 @@ require_relative '../api_client'
|
|
|
6
8
|
module TRMNLP
|
|
7
9
|
module Commands
|
|
8
10
|
class Pull < Base
|
|
11
|
+
Options = Data.define(:dir, :quiet, :id, :force)
|
|
12
|
+
|
|
9
13
|
def call
|
|
10
14
|
context.validate!
|
|
11
15
|
authenticate!
|
|
12
16
|
|
|
13
17
|
plugin_settings_id = options.id || config.plugin.id
|
|
14
|
-
raise
|
|
18
|
+
raise PluginIdRequired, 'plugin ID must be specified' if plugin_settings_id.nil?
|
|
15
19
|
|
|
16
20
|
unless options.force
|
|
17
|
-
answer = prompt(
|
|
18
|
-
raise
|
|
21
|
+
answer = prompt('Local plugin files will be overwritten. Are you sure? (y/n) ').downcase
|
|
22
|
+
raise Aborted, 'aborting' unless %w[y yes].include?(answer)
|
|
19
23
|
end
|
|
20
24
|
|
|
21
25
|
api = APIClient.new(config)
|
|
@@ -27,7 +31,11 @@ module TRMNLP
|
|
|
27
31
|
zip_file.each do |entry|
|
|
28
32
|
dest_path = paths.src_dir.join(entry.name)
|
|
29
33
|
dest_path.dirname.mkpath
|
|
30
|
-
|
|
34
|
+
# NOTE: delete-before-extract avoids EACCES on Linux when an
|
|
35
|
+
# existing template-generated file is non-writable.
|
|
36
|
+
# rubyzip's block-based overwrite confirmation does not chmod.
|
|
37
|
+
dest_path.delete if dest_path.exist?
|
|
38
|
+
zip_file.extract(entry, destination_directory: paths.src_dir)
|
|
31
39
|
end
|
|
32
40
|
end
|
|
33
41
|
|
|
@@ -36,8 +44,8 @@ module TRMNLP
|
|
|
36
44
|
tempfile.close
|
|
37
45
|
end
|
|
38
46
|
|
|
39
|
-
|
|
47
|
+
reporter.info "Downloaded plugin (#{size} bytes)"
|
|
40
48
|
end
|
|
41
49
|
end
|
|
42
50
|
end
|
|
43
|
-
end
|
|
51
|
+
end
|
data/lib/trmnlp/commands/push.rb
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'tmpdir'
|
|
1
5
|
require 'zip'
|
|
2
6
|
|
|
3
7
|
require_relative 'base'
|
|
@@ -6,62 +10,68 @@ require_relative '../api_client'
|
|
|
6
10
|
module TRMNLP
|
|
7
11
|
module Commands
|
|
8
12
|
class Push < Base
|
|
13
|
+
Options = Data.define(:dir, :quiet, :id, :force)
|
|
14
|
+
|
|
15
|
+
# Build the archive under the system temp dir, not the working
|
|
16
|
+
# directory — a relative path would litter the user's project (or the
|
|
17
|
+
# repo root, in specs) if a run is interrupted before the ensure-cleanup.
|
|
18
|
+
ZIP_PATH = File.join(Dir.tmpdir, "trmnlp-upload-#{Process.pid}.zip").freeze
|
|
19
|
+
|
|
9
20
|
def call
|
|
10
21
|
context.validate!
|
|
11
22
|
authenticate!
|
|
12
23
|
|
|
13
24
|
is_new = false
|
|
14
|
-
zip_path = 'upload.zip'
|
|
15
25
|
|
|
16
26
|
api = APIClient.new(config)
|
|
17
27
|
|
|
18
28
|
plugin_settings_id = options.id || config.plugin.id
|
|
19
29
|
if plugin_settings_id.nil?
|
|
20
|
-
|
|
30
|
+
reporter.info 'Creating a new plugin on the server...'
|
|
21
31
|
response = api.post_plugin_setting(name: 'New TRMNLP Plugin', plugin_id: 37) # hardcoded id for private_plugin
|
|
22
32
|
plugin_settings_id = response.dig('data', 'id')
|
|
23
33
|
is_new = true
|
|
24
34
|
end
|
|
25
35
|
|
|
26
36
|
unless is_new || options.force
|
|
27
|
-
answer = prompt(
|
|
28
|
-
raise
|
|
37
|
+
answer = prompt('Plugin settings on the server will be overwritten. Are you sure? (y/n) ').downcase
|
|
38
|
+
raise Aborted, 'aborting' unless %w[y yes].include?(answer)
|
|
29
39
|
end
|
|
30
40
|
|
|
31
|
-
Zip::File.open(
|
|
41
|
+
Zip::File.open(ZIP_PATH, create: true) do |zip_file|
|
|
32
42
|
paths.src_files.each do |file|
|
|
33
43
|
zip_file.add(File.basename(file), file)
|
|
34
44
|
end
|
|
35
45
|
end
|
|
36
|
-
|
|
37
|
-
response = api.post_plugin_setting_archive(plugin_settings_id,
|
|
46
|
+
|
|
47
|
+
response = api.post_plugin_setting_archive(plugin_settings_id, ZIP_PATH)
|
|
38
48
|
paths.plugin_config.write(response.dig('data', 'settings_yaml'))
|
|
39
49
|
|
|
40
|
-
size = File.size(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
size = File.size(ZIP_PATH)
|
|
51
|
+
|
|
52
|
+
reporter.info <<~HEREDOC
|
|
53
|
+
Uploaded plugin (#{size} bytes)
|
|
54
|
+
Dashboard: #{config.app.edit_plugin_settings_uri(plugin_settings_id)}
|
|
45
55
|
HEREDOC
|
|
46
56
|
|
|
47
57
|
if is_new
|
|
48
|
-
|
|
58
|
+
reporter.info <<~HEREDOC
|
|
49
59
|
|
|
50
|
-
|
|
60
|
+
IMPORTANT! Don't forget to add it to your device playlist!
|
|
51
61
|
|
|
52
|
-
|
|
62
|
+
#{config.app.playlists_uri}
|
|
53
63
|
HEREDOC
|
|
54
64
|
end
|
|
55
|
-
rescue
|
|
65
|
+
rescue StandardError
|
|
56
66
|
if is_new && plugin_settings_id
|
|
57
|
-
|
|
67
|
+
reporter.info 'Error during creation, cleaning up...'
|
|
58
68
|
api.delete_plugin_setting(plugin_settings_id)
|
|
59
69
|
end
|
|
60
70
|
|
|
61
71
|
raise
|
|
62
72
|
ensure
|
|
63
|
-
|
|
73
|
+
FileUtils.rm_f(ZIP_PATH)
|
|
64
74
|
end
|
|
65
75
|
end
|
|
66
76
|
end
|
|
67
|
-
end
|
|
77
|
+
end
|
|
@@ -1,25 +1,54 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'selenium-webdriver'
|
|
2
4
|
|
|
3
5
|
require_relative 'base'
|
|
4
6
|
require_relative '../api_client'
|
|
7
|
+
require_relative '../browser_pool'
|
|
5
8
|
|
|
6
9
|
module TRMNLP
|
|
7
10
|
module Commands
|
|
8
11
|
class Serve < Base
|
|
12
|
+
Options = Data.define(:dir, :quiet, :bind, :port)
|
|
13
|
+
|
|
9
14
|
def call
|
|
10
15
|
context.validate!
|
|
11
|
-
|
|
16
|
+
report_form_field_warnings
|
|
17
|
+
|
|
12
18
|
# Must come AFTER parsing options
|
|
13
19
|
require_relative '../app'
|
|
14
20
|
|
|
15
21
|
# Now we can configure things
|
|
16
22
|
App.set(:context, context)
|
|
23
|
+
App.set(:browser_pool, BrowserPool.new(driver_factory: method(:build_firefox_driver)))
|
|
17
24
|
App.set(:bind, options.bind)
|
|
18
25
|
App.set(:port, options.port)
|
|
26
|
+
permit_all_hosts if codespaces?
|
|
19
27
|
|
|
20
28
|
# Finally, start the app!
|
|
21
29
|
App.run!
|
|
22
30
|
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# Codespaces forwards the dev port through a proxy whose Host header Sinatra rejects.
|
|
35
|
+
def codespaces? = ENV['CODESPACES'] == 'true'
|
|
36
|
+
|
|
37
|
+
def permit_all_hosts
|
|
38
|
+
App.set(:host_authorization, { allow_if: ->(_env) { true } })
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def build_firefox_driver
|
|
42
|
+
options = Selenium::WebDriver::Firefox::Options.new
|
|
43
|
+
options.add_argument('--headless')
|
|
44
|
+
options.add_argument('--disable-web-security')
|
|
45
|
+
# Disable subpixel antialiasing — its colour fringing quantizes badly on 1-bit e-ink.
|
|
46
|
+
options.add_preference('gfx.text.disable-aa', true)
|
|
47
|
+
options.add_preference('gfx.text.subpixel-position.force-disabled', true)
|
|
48
|
+
Selenium::WebDriver.for(:firefox, options: options).tap do |driver|
|
|
49
|
+
driver.manage.window.maximize
|
|
50
|
+
end
|
|
51
|
+
end
|
|
23
52
|
end
|
|
24
53
|
end
|
|
25
|
-
end
|
|
54
|
+
end
|
data/lib/trmnlp/commands.rb
CHANGED
data/lib/trmnlp/config/app.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'yaml'
|
|
2
4
|
|
|
3
5
|
module TRMNLP
|
|
@@ -18,8 +20,9 @@ module TRMNLP
|
|
|
18
20
|
def logged_out? = !logged_in?
|
|
19
21
|
|
|
20
22
|
def api_key
|
|
21
|
-
env_api_key = ENV
|
|
23
|
+
env_api_key = ENV.fetch('TRMNL_API_KEY', nil)
|
|
22
24
|
return env_api_key if env_api_key && !env_api_key.empty?
|
|
25
|
+
|
|
23
26
|
@config['api_key']
|
|
24
27
|
end
|
|
25
28
|
|
|
@@ -33,7 +36,7 @@ module TRMNLP
|
|
|
33
36
|
|
|
34
37
|
def account_uri = URI.join(base_uri, '/account')
|
|
35
38
|
|
|
36
|
-
def edit_plugin_settings_uri(id) = URI.join(base_uri, "/plugin_settings/#{id
|
|
39
|
+
def edit_plugin_settings_uri(id) = URI.join(base_uri, "/plugin_settings/#{id}/edit")
|
|
37
40
|
|
|
38
41
|
def playlists_uri = URI.join(base_uri, '/playlists')
|
|
39
42
|
|
|
@@ -44,4 +47,4 @@ module TRMNLP
|
|
|
44
47
|
def read_config = paths.app_config.exist? ? YAML.safe_load(paths.app_config.read) : {}
|
|
45
48
|
end
|
|
46
49
|
end
|
|
47
|
-
end
|
|
50
|
+
end
|
data/lib/trmnlp/config/plugin.rb
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cgi'
|
|
1
4
|
require 'yaml'
|
|
2
5
|
|
|
6
|
+
require_relative '../errors'
|
|
7
|
+
require_relative '../framework_version'
|
|
8
|
+
|
|
3
9
|
module TRMNLP
|
|
4
10
|
class Config
|
|
5
11
|
class Plugin
|
|
@@ -10,13 +16,15 @@ module TRMNLP
|
|
|
10
16
|
end
|
|
11
17
|
|
|
12
18
|
def reload!
|
|
13
|
-
if paths.plugin_config.exist?
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
@config = if paths.plugin_config.exist?
|
|
20
|
+
YAML.safe_load_file(paths.plugin_config, permitted_classes: [Date, Time]) || {}
|
|
21
|
+
else
|
|
22
|
+
{}
|
|
23
|
+
end
|
|
24
|
+
rescue Psych::SyntaxError => e
|
|
25
|
+
raise InvalidConfig, "settings.yml is not valid YAML: #{e.message}"
|
|
18
26
|
end
|
|
19
|
-
|
|
27
|
+
|
|
20
28
|
def strategy = @config['strategy']
|
|
21
29
|
def polling? = strategy == 'polling'
|
|
22
30
|
def webhook? = strategy == 'webhook'
|
|
@@ -24,25 +32,32 @@ module TRMNLP
|
|
|
24
32
|
|
|
25
33
|
def polling_urls
|
|
26
34
|
# allow project-level config to override
|
|
27
|
-
urls = project_config.user_data_overrides.dig('trmnl', 'plugin_settings',
|
|
35
|
+
urls = project_config.user_data_overrides.dig('trmnl', 'plugin_settings',
|
|
36
|
+
'polling_url') || @config['polling_url']
|
|
28
37
|
|
|
29
38
|
return [] if urls.nil?
|
|
30
39
|
|
|
31
40
|
with_custom_fields(urls).strip.split("\n")
|
|
32
41
|
end
|
|
33
42
|
|
|
34
|
-
|
|
43
|
+
# for {{ trmnl }}
|
|
44
|
+
def polling_url_text = polling_urls.join("\r\n")
|
|
35
45
|
|
|
36
46
|
def polling_verb = @config['polling_verb'] || 'GET'
|
|
37
47
|
|
|
38
48
|
def polling_headers
|
|
39
|
-
|
|
49
|
+
# NOTE: render Liquid across the full headers string first so {% if %} blocks
|
|
50
|
+
# spanning multiple key=value pairs are preserved. Splitting on
|
|
51
|
+
# '&' or '=' before rendering would shatter tags into multiple values.
|
|
52
|
+
rendered = with_custom_fields(@config['polling_headers'] || '')
|
|
53
|
+
string_to_hash(rendered)
|
|
40
54
|
end
|
|
41
55
|
|
|
42
|
-
|
|
56
|
+
# for {{ trmnl }}
|
|
57
|
+
def polling_headers_encoded = polling_headers.map { |k, v| "#{k}=#{v}" }.join('&')
|
|
43
58
|
|
|
44
59
|
def polling_body = with_custom_fields(@config['polling_body'] || '')
|
|
45
|
-
|
|
60
|
+
|
|
46
61
|
def dark_mode = @config['dark_mode'] || 'no'
|
|
47
62
|
|
|
48
63
|
def no_screen_padding = @config['no_screen_padding'] || 'no'
|
|
@@ -52,16 +67,43 @@ module TRMNLP
|
|
|
52
67
|
def static_data
|
|
53
68
|
JSON.parse(@config['static_data'] || '{}')
|
|
54
69
|
rescue JSON::ParserError
|
|
55
|
-
raise
|
|
70
|
+
raise InvalidConfig, 'invalid JSON in static_data'
|
|
56
71
|
end
|
|
57
72
|
|
|
73
|
+
# Explicit language for transform.* code. If absent, the language
|
|
74
|
+
# is inferred from the file extension by Paths#transform_file.
|
|
75
|
+
# This one lives on the plugin (settings.yml) because production
|
|
76
|
+
# stores it on the plugin_setting record.
|
|
77
|
+
def serverless_language = @config['serverless_language']
|
|
78
|
+
|
|
79
|
+
# The TRMNL design-system version this plugin renders against.
|
|
80
|
+
# Lives on the plugin (settings.yml), like serverless_language,
|
|
81
|
+
# because production stores it on the plugin_setting record — so it
|
|
82
|
+
# round-trips through `trmnlp push` / `pull`. Accepts 'latest'
|
|
83
|
+
# (default), a pinned version, or nil (treated as latest). See
|
|
84
|
+
# db/data/framework_versions.yml for the supported set.
|
|
85
|
+
def framework_version
|
|
86
|
+
FrameworkVersion.new(@config['framework_version'], asset_host: project_config.asset_host)
|
|
87
|
+
rescue ArgumentError => e
|
|
88
|
+
raise InvalidConfig, e.message
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The custom-field *definitions* declared in settings.yml — the list
|
|
92
|
+
# of field hashes (keyname/name/field_type/...). Distinct from
|
|
93
|
+
# Config::Project#custom_fields, which holds the field *values*.
|
|
94
|
+
def custom_field_definitions = @config['custom_fields'] || []
|
|
95
|
+
|
|
96
|
+
# The raw parsed settings.yml hash. Most callers want the semantic
|
|
97
|
+
# readers above; `trmnlp lint` needs the uninterpreted values because
|
|
98
|
+
# it searches the raw {{ }} templates the semantic readers render away.
|
|
99
|
+
def settings = @config
|
|
100
|
+
|
|
58
101
|
private
|
|
59
102
|
|
|
60
103
|
attr_reader :paths, :project_config
|
|
61
104
|
|
|
62
105
|
def with_custom_fields(value) = project_config.with_custom_fields(value)
|
|
63
106
|
|
|
64
|
-
# copied from TRMNL core
|
|
65
107
|
def string_to_hash(str, delimiter: '=')
|
|
66
108
|
str.split('&').map do |k_v|
|
|
67
109
|
key, value = k_v.split(delimiter)
|
|
@@ -72,4 +114,4 @@ module TRMNLP
|
|
|
72
114
|
end
|
|
73
115
|
end
|
|
74
116
|
end
|
|
75
|
-
end
|
|
117
|
+
end
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'trmnl/liquid'
|
|
2
4
|
require 'yaml'
|
|
3
5
|
|
|
6
|
+
require_relative '../errors'
|
|
7
|
+
require_relative '../framework_version'
|
|
8
|
+
|
|
4
9
|
module TRMNLP
|
|
5
10
|
class Config
|
|
6
11
|
class Project
|
|
@@ -12,11 +17,13 @@ module TRMNLP
|
|
|
12
17
|
end
|
|
13
18
|
|
|
14
19
|
def reload!
|
|
15
|
-
if paths.trmnlp_config.exist?
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
@config = if paths.trmnlp_config.exist?
|
|
21
|
+
YAML.safe_load_file(paths.trmnlp_config, permitted_classes: [Date, Time]) || {}
|
|
22
|
+
else
|
|
23
|
+
{}
|
|
24
|
+
end
|
|
25
|
+
rescue Psych::SyntaxError => e
|
|
26
|
+
raise InvalidConfig, ".trmnlp.yml is not valid YAML: #{e.message}"
|
|
20
27
|
end
|
|
21
28
|
|
|
22
29
|
def user_filters = @config['custom_filters'] || []
|
|
@@ -27,7 +34,7 @@ module TRMNLP
|
|
|
27
34
|
(@config['watch'] || []).map { |watch_path| paths.expand(watch_path) }.uniq
|
|
28
35
|
end
|
|
29
36
|
|
|
30
|
-
def custom_fields = @config.fetch('custom_fields', {}).transform_values(
|
|
37
|
+
def custom_fields = @config.fetch('custom_fields', {}).transform_values { |v| stringify_field_value(v) }
|
|
31
38
|
|
|
32
39
|
def user_data_overrides = @config['variables'] || {}
|
|
33
40
|
|
|
@@ -39,15 +46,60 @@ module TRMNLP
|
|
|
39
46
|
|
|
40
47
|
def time_zone = @config['time_zone'] || 'UTC'
|
|
41
48
|
|
|
49
|
+
# Local override for the framework asset host (offline / mirrored
|
|
50
|
+
# dev). Trmnlp-specific (local dev only) — so it stays in
|
|
51
|
+
# .trmnlp.yml. Consumed by Config::Plugin#framework_version.
|
|
52
|
+
def asset_host = @config['framework_asset_host'] || FrameworkVersion::DEFAULT_ASSET_HOST
|
|
53
|
+
|
|
54
|
+
# Toggles serverless transform support. Enabled by default; set to
|
|
55
|
+
# 'disabled' in .trmnlp.yml to turn it off. A transform only runs
|
|
56
|
+
# when a src/transform.* file is also present, so the default is
|
|
57
|
+
# inert until the plugin actually ships one. Transforms run
|
|
58
|
+
# in-process via the bundled python/node/php/ruby interpreters; set
|
|
59
|
+
# serverless_daemon_url to route to a remote transform daemon
|
|
60
|
+
# instead. Lives in .trmnlp.yml because this is purely a
|
|
61
|
+
# local-dev decision.
|
|
62
|
+
def transform_runtime = @config['transform_runtime'] || 'enabled'
|
|
63
|
+
|
|
64
|
+
# Opt-in URL of a remote transform daemon. When set, transforms
|
|
65
|
+
# POST here instead of running locally — useful for
|
|
66
|
+
# production-fidelity testing or shared team daemons.
|
|
67
|
+
def serverless_daemon_url = @config['serverless_daemon_url']
|
|
68
|
+
|
|
69
|
+
# Bearer token for the remote transform daemon. Mirrors Config::App's
|
|
70
|
+
# ENV-first pattern so the secret stays out of version control —
|
|
71
|
+
# $TRMNL_SERVERLESS_DAEMON_API_KEY takes priority, falls through to
|
|
72
|
+
# the .trmnlp.yml key if no env var is set.
|
|
73
|
+
def serverless_daemon_api_key
|
|
74
|
+
env_key = ENV.fetch('TRMNL_SERVERLESS_DAEMON_API_KEY', nil)
|
|
75
|
+
return env_key if env_key && !env_key.empty?
|
|
76
|
+
|
|
77
|
+
@config['serverless_daemon_api_key']
|
|
78
|
+
end
|
|
79
|
+
|
|
42
80
|
private
|
|
43
81
|
|
|
82
|
+
# NOTE: arrays (multi-select fields) and hashes are preserved as-is;
|
|
83
|
+
# only their leaf values are stringified to match production
|
|
84
|
+
# behavior.
|
|
85
|
+
def stringify_field_value(value)
|
|
86
|
+
case value
|
|
87
|
+
when Array then value.map(&:to_s)
|
|
88
|
+
when Hash then value.transform_values { |v| stringify_field_value(v) }
|
|
89
|
+
else value.to_s
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
44
93
|
# for interpolating ENV vars into custom_fields
|
|
45
94
|
def with_env(value)
|
|
95
|
+
return value.map { |v| with_env(v) } if value.is_a?(Array)
|
|
96
|
+
return value.transform_values { |v| with_env(v) } if value.is_a?(Hash)
|
|
97
|
+
|
|
46
98
|
parse_liquid(value).render({ 'env' => ENV.to_h })
|
|
47
99
|
end
|
|
48
100
|
|
|
49
101
|
def parse_liquid(contents)
|
|
50
|
-
Liquid::Template.parse(contents, environment: TRMNL::Liquid.
|
|
102
|
+
Liquid::Template.parse(contents, environment: TRMNL::Liquid.new)
|
|
51
103
|
end
|
|
52
104
|
end
|
|
53
105
|
end
|
data/lib/trmnlp/config.rb
CHANGED