trmnl_preview 0.13.2 → 0.14.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 905205771ecf23f22c55a3d48835e6ee75ea90d4d2b55638c6ebea0160173578
4
- data.tar.gz: 663c559a8b71f943023fae8e18ea2e494c8df04e2d5fe81ffd34ce19a6d4d736
3
+ metadata.gz: fbe879abcadc7c6f7efceb9e54c88606aafd090a9538c9942f8cd3afef9596da
4
+ data.tar.gz: d9a123366e021a55b8bdc5b55016e38f1f1b4107163bc570a5112d89c8084270
5
5
  SHA512:
6
- metadata.gz: d8fbb97e1eb0ccc38a54ab7820de2837e7ef27b1bc52faed278b912a1ad709aee63f5b58860cbe4dedbd9fac7a5b7b3bce5fb68f2a750a0c064bb489a4846bc5
7
- data.tar.gz: 0d3ffc25d6567a13c125bd332be0fd76ec2b018e4de2e144b3e36ac1e4dc99882b6c802c31bbfdcac86e104b00b3448e8d1d4d61733aad953325a67b9399ca0c
6
+ metadata.gz: 0cd3eb90f4a2231a28dfe8e08097a90bc0d70d93861f9decd1946708b77c0ddf4b5ba3e3dd1acbae0b1daac7238cb88ea74d9f504880adeb1ee7c6ac6402c774
7
+ data.tar.gz: fbd799614df7d3c978ca2a67fb1b7916b446994bfbd15ce268f764ad0dbd62fed7a34f6c8e63621926490a3eabece74d44b089f3175bd0ad305de7958cf622a1
data/CHANGELOG.md CHANGED
@@ -1,6 +1,17 @@
1
1
 
2
2
  # Changelog
3
3
 
4
+ ## 0.14.1
5
+
6
+ - `trmnlp lint` reports a `recipe_overview` under 100 words. Below that, TRMNL serves the public recipe page with `robots: noindex`, so the recipe never appears in Google or other search engines, and nothing said so: the page looked fine, and only its HTML gave it away. A blank overview still passes, since private plugins have none. (#144)
7
+
8
+ ## 0.14.0
9
+
10
+ - `trmnlp serve` and `build` print what the transform writes to stdout and stderr (`console.log`, `print`, `puts`), one `transform stdout:` / `transform stderr:` line each, so a transform can be debugged without returning its logs in the data. (#128)
11
+ - The OAuth token exchange and refresh honor `oauth_token_request_auth_method` the way the hosted service does: HTTP Basic only for `header`, otherwise the client credentials go in the request body. trmnlp used HTTP Basic whenever a client secret was set, so a provider that wants the credentials in the body rejected the callback with `invalid_client: client_id is required`. A plugin that relied on HTTP Basic locally sets `oauth_token_request_auth_method: header`, which the hosted service already needs. (#121)
12
+ - `polling_headers` are parsed the way the hosted service parses them: one header per line, `Name: Value` as well as `Name=Value`, a JSON object, and values that contain `=`. Before, trmnlp split only on `&`, so headers saved one per line made the whole poll fail with "cannot include CR/LF" and the markup and transform got no polled data (no `IDX_0`/`IDX_1`). (#127)
13
+ - `trmnlp lint` no longer reports a custom field as unused when only the serverless transform (`src/transform.{py,rb,php,js}`) reads it. Plugins that move their data handling into the transform, reading fields from `input["trmnl"]["plugin_settings"]["custom_fields_values"]`, got one "is not used in form fields or markup" warning per field. The check now searches the transform as well, and its message names it. (#136)
14
+
4
15
  ## 0.13.2
5
16
 
6
17
  - `google_photos_picker`, `json`, `modal_trigger` and `oauth_provider_select` are known form field types, so `trmnlp lint`, `build` and `serve` no longer warn "unknown field_type" for them. The hosted service renders all four. (#137)
data/README.md CHANGED
@@ -334,6 +334,7 @@ oauth_scopes: "read:user user:email"
334
334
  oauth_pkce_enabled: "true" # optional, default false
335
335
  # oauth_scope_separator: " " # optional; some providers use ","
336
336
  # oauth_refresh_url: https://... # optional; defaults to oauth_token_url
337
+ # oauth_token_request_auth_method: header # optional; "header" sends the client credentials as HTTP Basic, otherwise they go in the body
337
338
  ```
338
339
 
339
340
  Your OAuth app credentials stay local and are never synced, so set them in your environment:
@@ -433,7 +434,7 @@ The `settings.yml` file is part of the plugin definition, and is uploaded and do
433
434
 
434
435
  `description:` is an optional one-line summary of the plugin, up to 35 characters. `trmnlp lint` reports anything longer, and `trmnlp list` shows it next to each plugin name.
435
436
 
436
- `recipe_overview:` is optional free text for a published recipe, with no length limit. TRMNL shows it on the public recipe page with its line breaks kept, and its first 120 characters feed the page's link preview. An overview of at least 100 words lets the recipe appear in Google and other search engines. Put credits and links in an About This Plugin field instead. Leave the key out to keep the overview already on TRMNL; an empty value clears it.
437
+ `recipe_overview:` is optional free text for a published recipe, with no length limit. TRMNL shows it on the public recipe page with its line breaks kept, and its first 120 characters feed the page's link preview. An overview of at least 100 words lets the recipe appear in Google and other search engines; `trmnlp lint` reports one that is shorter. Put credits and links in an About This Plugin field instead. Leave the key out to keep the overview already on TRMNL; an empty value clears it.
437
438
 
438
439
  See [TRMNL documentation](https://help.trmnl.com/en/articles/10542599-importing-and-exporting-private-plugins#h_581fb988f0) for details on this file's contents.
439
440
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'cgi'
4
+ require 'json'
4
5
  require 'yaml'
5
6
 
6
7
  require_relative '../errors'
@@ -113,13 +114,34 @@ module TRMNLP
113
114
  project_config.with_custom_fields(value, extra_variables:)
114
115
  end
115
116
 
116
- def string_to_hash(str, delimiter: '=')
117
- str.split('&').map do |k_v|
118
- key, value = k_v.split(delimiter)
117
+ # Mirrors the hosted service's Plugins::Helpers::Transformer#string_to_hash.
118
+ def string_to_hash(str)
119
+ return json_to_hash(str) if str.strip.start_with?('{')
120
+
121
+ str.split(/[\n&]/).filter_map do |pair|
122
+ pair = pair.strip
123
+ key, value = pair.split(delimiter_for(pair), 2)
119
124
  next if value.nil?
120
125
 
121
- { key => CGI.unescape_uri_component(value) }
122
- end.compact.reduce({}, :merge)
126
+ [key.strip, CGI.unescape_uri_component(value.strip)]
127
+ end.to_h
128
+ end
129
+
130
+ # ": " (not ":") so an "https://" URL isn't mistaken for a "Name: Value" header.
131
+ def delimiter_for(pair)
132
+ equals = pair.index('=')
133
+ colon = pair.index(': ')
134
+ return '=' if colon.nil?
135
+ return ': ' if equals.nil?
136
+
137
+ colon < equals ? ': ' : '='
138
+ end
139
+
140
+ def json_to_hash(str)
141
+ parsed = JSON.parse(str)
142
+ parsed.is_a?(Hash) ? parsed.transform_values(&:to_s) : {}
143
+ rescue JSON::ParserError
144
+ {}
123
145
  end
124
146
  end
125
147
  end
@@ -6,13 +6,14 @@ module TRMNLP
6
6
  module Lint
7
7
  module Checks
8
8
  # Reports custom fields declared in .trmnlp.yml that never appear in the
9
- # polling settings or the markup — one finding per unused field.
9
+ # polling settings, the markup or the serverless transform — one finding
10
+ # per unused field.
10
11
  class CustomFieldsUsed < Check
11
12
  SETTINGS_KEYS = %w[polling_url polling_headers polling_body].freeze
12
13
 
13
14
  def issues
14
15
  source.custom_field_values.keys.reject { |keyname| used?(keyname) }.map do |keyname|
15
- { message: "Custom field '#{keyname}' is not used in form fields or markup." }
16
+ { message: "Custom field '#{keyname}' is not used in form fields, markup or the transform." }
16
17
  end
17
18
  end
18
19
 
@@ -20,7 +21,8 @@ module TRMNLP
20
21
 
21
22
  def used?(keyname)
22
23
  pattern = /#{Regexp.escape(keyname)}/
23
- searchable_settings.match?(pattern) || source.all_markup.match?(pattern)
24
+ searchable_settings.match?(pattern) || source.all_markup.match?(pattern) ||
25
+ source.transform_code.match?(pattern)
24
26
  end
25
27
 
26
28
  def searchable_settings
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../check'
4
+
5
+ module TRMNLP
6
+ module Lint
7
+ module Checks
8
+ # Below this many words the hosted service serves the public recipe page
9
+ # with `robots: noindex`, so search engines drop it. The overview is
10
+ # optional (private plugins never show one), so a blank value passes.
11
+ # Words are counted on whitespace; there is no upstream count to match.
12
+ class RecipeOverviewLength < Check
13
+ MIN_WORDS = 100
14
+ MESSAGE = "Recipe overview should be at least #{MIN_WORDS} words long " \
15
+ 'for the recipe page to appear in Google and other search engines.'.freeze
16
+
17
+ private
18
+
19
+ def pass?
20
+ words = source.recipe_overview.split.size
21
+ words.zero? || words >= MIN_WORDS
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -17,6 +17,7 @@ module TRMNLP
17
17
 
18
18
  def plugin_name = settings['name'].to_s
19
19
  def plugin_description = settings['description'].to_s
20
+ def recipe_overview = settings['recipe_overview'].to_s
20
21
  def settings = config.plugin.settings
21
22
  def custom_field_values = config.project.custom_fields
22
23
  def custom_field_definitions = config.plugin.custom_field_definitions
@@ -33,6 +34,15 @@ module TRMNLP
33
34
  @all_markup ||= view_markup.values.join + shared_markup
34
35
  end
35
36
 
37
+ # The serverless transform (src/transform.{py,rb,php,js}), or '' without one.
38
+ # It reads custom fields from its input, so a field may be used only there.
39
+ def transform_code
40
+ @transform_code ||= begin
41
+ path, = paths.transform_file
42
+ path ? read(path) : ''
43
+ end
44
+ end
45
+
36
46
  private
37
47
 
38
48
  attr_reader :config, :paths
data/lib/trmnlp/lint.rb CHANGED
@@ -5,6 +5,7 @@ require_relative 'lint/source'
5
5
  require_relative 'lint/checks/title_casing'
6
6
  require_relative 'lint/checks/title_length'
7
7
  require_relative 'lint/checks/description_length'
8
+ require_relative 'lint/checks/recipe_overview_length'
8
9
  require_relative 'lint/checks/layouts_have_content'
9
10
  require_relative 'lint/checks/no_async_functions'
10
11
  require_relative 'lint/checks/waits_for_dom_load'
@@ -25,6 +26,7 @@ module TRMNLP
25
26
  Checks::TitleCasing,
26
27
  Checks::TitleLength,
27
28
  Checks::DescriptionLength,
29
+ Checks::RecipeOverviewLength,
28
30
  Checks::LayoutsHaveContent,
29
31
  Checks::NoAsyncFunctions,
30
32
  Checks::WaitsForDomLoad,
@@ -39,9 +39,8 @@ module TRMNLP
39
39
  )
40
40
  end
41
41
 
42
- # A PKCE public client has no secret, so its client_id must travel in the
43
- # request body; a confidential client authenticates with HTTP Basic.
44
- def auth_scheme = provider.client_secret ? :basic_auth : :request_body
42
+ # Same rule as the hosted service: HTTP Basic only when asked for, otherwise the body.
43
+ def auth_scheme = provider.token_request_auth_method == 'header' ? :basic_auth : :request_body
45
44
 
46
45
  # oauth2 posts tokens through a connection built from the site, so give it
47
46
  # the endpoint's origin even though the full URL is absolute.
@@ -19,6 +19,7 @@ module TRMNLP
19
19
  def refresh_url = settings['oauth_refresh_url'] || token_url
20
20
  def scopes = settings['oauth_scopes']
21
21
  def scope_separator = settings['oauth_scope_separator'] || ' '
22
+ def token_request_auth_method = settings['oauth_token_request_auth_method']
22
23
  def pkce? = truthy?(settings['oauth_pkce_enabled'])
23
24
  def enabled? = truthy?(settings['oauth_enabled'])
24
25
  def client_id = env_first(ENV_CLIENT_ID, settings['oauth_client_id'])
@@ -40,11 +40,19 @@ module TRMNLP
40
40
  def run(path, inferred_language, data)
41
41
  language = config.plugin.serverless_language || inferred_language
42
42
  result = client.execute(code: path.read, stdin: JSON.generate(data), language:)
43
+ report_printed_output(result)
43
44
  return record_failure(result, data) unless result.success?
44
45
 
45
46
  parse_output(result.output, data)
46
47
  end
47
48
 
49
+ # The transform's return value travels separately, so stdout holds only what it printed.
50
+ def report_printed_output(result)
51
+ { 'stdout' => result.stdout, 'stderr' => result.stderr }.each do |stream, text|
52
+ reporter.info("transform #{stream}: #{text.strip}") unless text.strip.empty?
53
+ end
54
+ end
55
+
48
56
  def record_failure(result, fallback)
49
57
  @error = result.error || "transform exited #{result.exit_code}: #{result.stderr.strip}"
50
58
  reporter.info("transform failed: #{@error}")
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TRMNLP
4
- VERSION = '0.13.2'
4
+ VERSION = '0.14.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trmnl_preview
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.2
4
+ version: 0.14.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rockwell Schrock
@@ -315,6 +315,7 @@ files:
315
315
  - lib/trmnlp/lint/checks/no_async_functions.rb
316
316
  - lib/trmnlp/lint/checks/no_opacity.rb
317
317
  - lib/trmnlp/lint/checks/no_size_classes.rb
318
+ - lib/trmnlp/lint/checks/recipe_overview_length.rb
318
319
  - lib/trmnlp/lint/checks/title_casing.rb
319
320
  - lib/trmnlp/lint/checks/title_length.rb
320
321
  - lib/trmnlp/lint/checks/waits_for_dom_load.rb