trmnl_preview 0.8.1 → 0.8.3

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: 0f80ac8d03e9b67e4ea6c7f18e8cd7745a76c910e9801f910662cc46b5661eb0
4
- data.tar.gz: 1022cfae134c303ba7d00201d2d6ca7acb7c23c927371585b9e63a00fa28bfa2
3
+ metadata.gz: c021076f55f8b5d150e65e2202c5bc978e8709684ee4d9bf27c0b7ec8e0b59b8
4
+ data.tar.gz: daa5e5734d9b1e3d755b789b0081916e53ce6c0a3cdd6d75e5276335ad2dae83
5
5
  SHA512:
6
- metadata.gz: 6667ad132d9b601073db2010c524f8322e6a7fd2b5425d248cf3a53398166d1f579171e8c1142da146247f7fcb96192c50c7fa0e8d9158797bc98d7bc9755eb2
7
- data.tar.gz: 189d830c8ee5b2ed2ccd2006ef1fda9c6dadd258488a74d4925ac4476762c581fd4df3961fb94f771ff0943f3ba57cd62d0294cff6e5e1c5f79558c763d534a1
6
+ metadata.gz: d422c11366ba16381be6765dfb36a25e2a662db4e9da3f3b08b780bbabbd94338d65f0f75bf6cbb82a0aa51fee8660886e5cd14203ccfbd2145539a617823559
7
+ data.tar.gz: 87b5bb57b023748ecc9412e13059801a755e6d0d03c82f27aaacc2077c119ed4386e28fba8a3a4f62ee47b783d4c92e49905d1a47fb29e2f8a93da7caddc563d
data/CHANGELOG.md CHANGED
@@ -1,6 +1,18 @@
1
1
 
2
2
  # Changelog
3
3
 
4
+ ## 0.8.3
5
+
6
+ - `trmnlp init` and `trmnlp clone` now scaffold a `.github/workflows/trmnl.yml` CI workflow and a `.gitignore`, and run `git init -b main`, so a cloned plugin is ready to push to GitHub and deploy on every commit to `main`
7
+ - Added `--skip-git` to `trmnlp init` and `trmnlp clone` for projects that manage Git themselves
8
+ - The Docker image now ships `git` so the `docker run trmnl/trmnlp clone` flow leaves a ready-to-push project on the host
9
+ - View templates now ship canonical `layout` + `title_bar` markup that passes `trmnlp lint`
10
+
11
+ ## 0.8.2
12
+
13
+ - Fixed `framework_version: latest` rendering against the auto-upgrading `/latest/` asset path instead of the current concrete release, matching the hosted service (#99)
14
+ - Cleanup and minor improvements
15
+
4
16
  ## 0.8.1
5
17
 
6
18
  ### Added
data/README.md CHANGED
@@ -28,6 +28,10 @@ This is the structure of a plugin project:
28
28
 
29
29
  ```
30
30
  .
31
+ ├── .github
32
+ │ └── workflows
33
+ │ └── trmnl.yml
34
+ ├── .gitignore
31
35
  ├── .trmnlp.yml
32
36
  ├── bin
33
37
  │ └── trmnlp
@@ -42,6 +46,8 @@ This is the structure of a plugin project:
42
46
 
43
47
  | File | Purpose |
44
48
  |---|---|
49
+ | `.github/workflows/trmnl.yml` | GitHub Actions workflow — lints every PR, deploys to TRMNL on `main` |
50
+ | `.gitignore` | Keeps `trmnlp build` output out of version control |
45
51
  | `.trmnlp.yml` | Local dev-server config — not uploaded to TRMNL |
46
52
  | `src/full.liquid` | Markup for the full screen |
47
53
  | `src/half_horizontal.liquid` | Top or bottom half of a stacked mashup |
@@ -111,7 +117,7 @@ trmnlp build --png --color-depth 2
111
117
  | `--png` | Render a PNG per view alongside the HTML |
112
118
  | `--width` | PNG width in pixels (default 800) |
113
119
  | `--height` | PNG height in pixels (default 480) |
114
- | `--color-depth` | PNG bit depth — 1, 2, or 4 — overriding the markup |
120
+ | `--color-depth` | PNG bit depth — 1-8 — overriding the markup |
115
121
 
116
122
  `--width`, `--height`, and `--color-depth` apply only with `--png`. PNG rendering needs Firefox and ImageMagick installed; plain `trmnlp build` needs neither.
117
123
 
@@ -121,6 +127,56 @@ The `trmnlp login` command saves your API key to `~/.config/trmnlp/config.yml`.
121
127
 
122
128
  If an environment variable is more convenient (for example in a CI/CD pipeline), you can set `$TRMNL_API_KEY` instead.
123
129
 
130
+ ## Continuous Integration
131
+
132
+ `trmnlp init` and `trmnlp clone` scaffold a `.github/workflows/trmnl.yml`
133
+ workflow and initialize a Git repository, so a fresh project is ready to push
134
+ to GitHub. The workflow runs in GitHub Actions without `trmnlp login` — set the
135
+ `TRMNL_API_KEY` environment variable and it's used in place of the saved
136
+ config. Add it as a repository secret to activate the workflow; it looks like
137
+ this:
138
+
139
+ ```yaml
140
+ name: TRMNL
141
+ on:
142
+ pull_request:
143
+ push:
144
+ branches: [main]
145
+
146
+ jobs:
147
+ lint:
148
+ runs-on: ubuntu-latest
149
+ steps:
150
+ - uses: actions/checkout@v6
151
+ - uses: ruby/setup-ruby@v1
152
+ with:
153
+ ruby-version: "4.0"
154
+ - run: gem install trmnl_preview
155
+ - run: trmnlp lint
156
+
157
+ push:
158
+ needs: lint
159
+ if: github.ref == 'refs/heads/main'
160
+ runs-on: ubuntu-latest
161
+ steps:
162
+ - uses: actions/checkout@v6
163
+ - uses: ruby/setup-ruby@v1
164
+ with:
165
+ ruby-version: "4.0"
166
+ - run: gem install trmnl_preview
167
+ - run: trmnlp push --force
168
+ env:
169
+ TRMNL_API_KEY: ${{ secrets.TRMNL_API_KEY }}
170
+ ```
171
+
172
+ The `lint` job gates every pull request — `trmnlp lint` exits non-zero on
173
+ issues, so a failing check blocks the merge. The `push` job uploads to TRMNL
174
+ only on `main`.
175
+
176
+ > **Make sure `src/settings.yml` has an `id`.** `trmnlp push` updates the
177
+ > plugin with that id; without one it creates a *new* plugin on every run.
178
+ > Projects made with `trmnlp clone` or `trmnlp pull` already have it.
179
+
124
180
  ## Running trmnlp
125
181
 
126
182
  The `bin/trmnlp` script is provided as a convenience. It will use the local Ruby gem if available, falling back to the `trmnl/trmnlp` Docker image.
@@ -336,6 +392,21 @@ bin/rake
336
392
 
337
393
  Specs run under SimpleCov; a coverage report is written to `coverage/`.
338
394
 
395
+ ## Releasing
396
+
397
+ Releases are automated. The [`Release` workflow](.github/workflows/release.yaml)
398
+ fires whenever `lib/trmnlp/version.rb` changes on `main`, then tags the commit,
399
+ publishes the gem to RubyGems, and pushes the multi-arch Docker image. Each step
400
+ is idempotent, so the workflow is safe to re-run after a partial failure.
401
+
402
+ To cut a release:
403
+
404
+ 1. Bump the version in `lib/trmnlp/version.rb`.
405
+ 2. Run `bundle install` so `Gemfile.lock` picks up the new version.
406
+ 3. Commit and merge to `main` — the workflow does the rest.
407
+
408
+ By convention, add a matching `CHANGELOG.md` entry in the same change.
409
+
339
410
  ## Contributing
340
411
 
341
412
  Bug reports and pull requests are welcome on GitHub at https://github.com/usetrmnl/trmnlp.
data/lib/trmnlp/cli.rb CHANGED
@@ -22,7 +22,7 @@ module TRMNLP
22
22
  method_option :png, type: :boolean, default: false, desc: 'Also render a PNG per view'
23
23
  method_option :width, type: :numeric, desc: 'PNG width in pixels (with --png)'
24
24
  method_option :height, type: :numeric, desc: 'PNG height in pixels (with --png)'
25
- method_option :color_depth, type: :numeric, desc: 'PNG bit depth: 1, 2, or 4 (with --png)'
25
+ method_option :color_depth, type: :numeric, desc: 'PNG bit depth: 1-8 (with --png)'
26
26
  def build
27
27
  Commands::Build.run(options)
28
28
  end
@@ -34,11 +34,13 @@ module TRMNLP
34
34
 
35
35
  desc 'init NAME', 'Start a new plugin project'
36
36
  method_option :skip_liquid, type: :boolean, default: false, desc: 'Skip generating liquid templates'
37
+ method_option :skip_git, type: :boolean, default: false, desc: 'Skip initializing a git repository'
37
38
  def init(name)
38
39
  Commands::Init.run(options, name)
39
40
  end
40
41
 
41
42
  desc 'clone NAME ID', 'Copy a plugin project from TRMNL server'
43
+ method_option :skip_git, type: :boolean, default: false, desc: 'Skip initializing a git repository'
42
44
  def clone(name, id)
43
45
  Commands::Clone.run(options, name, id)
44
46
  end
@@ -7,7 +7,7 @@ require_relative 'pull'
7
7
  module TRMNLP
8
8
  module Commands
9
9
  class Clone < Base
10
- Options = Data.define(:dir, :quiet)
10
+ Options = Data.define(:dir, :quiet, :skip_git)
11
11
 
12
12
  def call(directory_name, id)
13
13
  authenticate!
@@ -15,7 +15,7 @@ module TRMNLP
15
15
  destination_path = Pathname.new(options.dir).join(directory_name)
16
16
  raise DirectoryExists, "directory #{destination_path} already exists, aborting" if destination_path.exist?
17
17
 
18
- Init.run({ dir: options.dir, skip_liquid: true, quiet: true }, directory_name)
18
+ Init.run({ dir: options.dir, skip_liquid: true, quiet: true, skip_git: options.skip_git }, directory_name)
19
19
 
20
20
  Pull.run({ dir: destination_path.to_s, force: true, id: id })
21
21
 
@@ -7,7 +7,7 @@ require_relative 'base'
7
7
  module TRMNLP
8
8
  module Commands
9
9
  class Init < Base
10
- Options = Data.define(:dir, :quiet, :skip_liquid)
10
+ Options = Data.define(:dir, :quiet, :skip_liquid, :skip_git)
11
11
 
12
12
  def call(name)
13
13
  destination_dir = Pathname.new(options.dir).join(name)
@@ -17,7 +17,9 @@ module TRMNLP
17
17
  destination_dir.mkpath
18
18
  end
19
19
 
20
- template_dir.glob('**/{*,.*}').each do |source_pathname|
20
+ # NOTE: FNM_DOTMATCH so the glob descends into hidden template
21
+ # directories (e.g. .github/); without it those files are skipped.
22
+ template_dir.glob('**/{*,.*}', File::FNM_DOTMATCH).each do |source_pathname|
21
23
  next if source_pathname.directory?
22
24
  next if options.skip_liquid && source_pathname.extname == '.liquid'
23
25
 
@@ -41,6 +43,8 @@ module TRMNLP
41
43
  destination_pathname.chmod(destination_pathname.stat.mode | 0o200)
42
44
  end
43
45
 
46
+ init_git_repo(destination_dir) unless options.skip_git
47
+
44
48
  reporter.info <<~HEREDOC
45
49
 
46
50
  To start the local server:
@@ -58,6 +62,13 @@ module TRMNLP
58
62
  private
59
63
 
60
64
  def template_dir = paths.templates_dir.join('init')
65
+
66
+ # Make the scaffold a Git repository on `main` so it's ready to push
67
+ # to GitHub (the workflow's `branches: [main]` trigger requires it,
68
+ # regardless of the host's init.defaultBranch).
69
+ # Does nothing when git is unavailable — `system` returns nil rather
70
+ # than raising, so the scaffold itself still succeeds.
71
+ def init_git_repo(dir) = system('git', 'init', '-q', '-b', 'main', dir.to_s)
61
72
  end
62
73
  end
63
74
  end
@@ -44,9 +44,13 @@ module TRMNLP
44
44
 
45
45
  def pinned? = @pinned
46
46
 
47
- def css_url = "#{@asset_host}/css/#{path_segment}/plugins.css"
47
+ # Both a pinned and an unpinned ("latest") version resolve to a
48
+ # concrete release here — #number is never the literal "latest" — so a
49
+ # local preview renders the same bundle as the hosted service instead
50
+ # of drifting onto a new release the moment one ships.
51
+ def css_url = "#{@asset_host}/css/#{number}/plugins.css"
48
52
 
49
- def js_url = "#{@asset_host}/js/#{path_segment}/plugins.js"
53
+ def js_url = "#{@asset_host}/js/#{number}/plugins.js"
50
54
 
51
55
  def ==(other) = other.is_a?(self.class) && number == other.number
52
56
 
@@ -59,11 +63,5 @@ module TRMNLP
59
63
  def as_json(*) = number
60
64
 
61
65
  def to_s = number
62
-
63
- private
64
-
65
- # When pinned, requests assets at /css/<version>/plugins.css to lock
66
- # behavior; otherwise hit /css/latest/ for live updates.
67
- def path_segment = pinned? ? @number : 'latest'
68
66
  end
69
67
  end
@@ -24,8 +24,8 @@ module TRMNLP
24
24
  def pass? = static_image_urls.all? { |url| reachable?(url) }
25
25
 
26
26
  def static_image_urls
27
- source.view_markup.values
28
- .flat_map { |html| html.scan(/<img[^>]+src\s*=\s*["']([^"']+)["']/i).flatten }
27
+ source.all_markup
28
+ .scan(/<img[^>]+src\s*=\s*["']([^"']+)["']/i).flatten
29
29
  .map(&:strip)
30
30
  .reject { |src| src.empty? || src.include?('{{') || src.start_with?('data:') }
31
31
  end
@@ -6,7 +6,8 @@ module TRMNLP
6
6
  module Lint
7
7
  module Checks
8
8
  class WaitsForDomLoad < Check
9
- MESSAGE = 'JavaScript should listen for the DOMContentLoaded event, not window.onLoad()'
9
+ MESSAGE = 'JavaScript should listen for the DOMContentLoaded event, ' \
10
+ 'not window.onload or window.addEventListener("load")'
10
11
  LEARN_MORE = 'https://help.trmnl.com/en/articles/9510536-private-plugins#h_db7030f8b8'
11
12
  FORBIDDEN = ['window.onload', 'window.addeventlistener("load")',
12
13
  "window.addeventlistener('load')"].freeze
@@ -72,7 +72,7 @@ module TRMNLP
72
72
  end
73
73
  end
74
74
 
75
- # bindings must match the `GET /render/{view}.html` route in app.rb
75
+ # ivars must match the @-references in web/views/render_html.erb
76
76
  class TemplateBinding
77
77
  def initialize(renderer, view, params)
78
78
  @view = view
@@ -59,9 +59,7 @@ module TRMNLP
59
59
  TransformClient::Result.new(
60
60
  stdout: parsed['stdout'] || '',
61
61
  stderr: parsed['stderr'] || '',
62
- # Fall back to stdout for daemons that haven't been upgraded to
63
- # the separate `output` channel yet.
64
- output: (parsed['output'].to_s.empty? ? parsed['stdout'] : parsed['output']).to_s,
62
+ output: parsed['output'].to_s,
65
63
  exit_code: parsed['exit_code'] || 0,
66
64
  duration_ms: parsed['duration_ms'] || 0,
67
65
  error: parsed['error']
@@ -9,8 +9,10 @@ module TRMNLP
9
9
  # snippet supplied by the caller. Subprocess writes to a tempfile
10
10
  # path, Http writes to FD 3 for the production daemon to capture.
11
11
  #
12
- # Mirrors the hosted serverless runtime's code-wrapping behavior
13
- # verbatim except for the configurable sink.
12
+ # Mirrors the hosted serverless runtime's code-wrapping behavior,
13
+ # parameterized on the per-backend output sink. PHP additionally
14
+ # carries a leading `<?php` tag the file-based Subprocess path
15
+ # needs; the daemon tolerates it, so parity holds.
14
16
  #
15
17
  # NOTE: `output_sink` is spliced verbatim into the generated script as
16
18
  # executable code. It MUST be trmnlp-generated (see Subprocess#sink_for
@@ -17,9 +17,9 @@ module TRMNLP
17
17
 
18
18
  # Assembles the merged data hash. The trmnl namespace is built first,
19
19
  # layered with static_data / cached polled data / user_data_overrides,
20
- # then piped through the transform. The trmnl namespace is re-applied
21
- # after the transform so device, user, and plugin_settings survive
22
- # even when the transform doesn't pass them through.
20
+ # then piped through the transform. The whole trmnl namespace is
21
+ # re-applied after the transform so it survives even when the
22
+ # transform doesn't pass it through.
23
23
  def call(device: {})
24
24
  namespace = base_trmnl_data(device:)
25
25
  merged = assemble(namespace)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TRMNLP
4
- VERSION = '0.8.1'
4
+ VERSION = '0.8.3'
5
5
  end
@@ -0,0 +1,30 @@
1
+ name: TRMNL
2
+ on:
3
+ pull_request:
4
+ push:
5
+ branches: [main]
6
+
7
+ jobs:
8
+ lint:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v6
12
+ - uses: ruby/setup-ruby@v1
13
+ with:
14
+ ruby-version: "4.0"
15
+ - run: gem install trmnl_preview
16
+ - run: trmnlp lint
17
+
18
+ push:
19
+ needs: lint
20
+ if: github.ref == 'refs/heads/main'
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - uses: actions/checkout@v6
24
+ - uses: ruby/setup-ruby@v1
25
+ with:
26
+ ruby-version: "4.0"
27
+ - run: gem install trmnl_preview
28
+ - run: trmnlp push --force
29
+ env:
30
+ TRMNL_API_KEY: ${{ secrets.TRMNL_API_KEY }}
@@ -0,0 +1,2 @@
1
+ # trmnlp build output
2
+ _build/
@@ -1 +1,7 @@
1
- full!
1
+ <div class="layout layout--col layout--center">
2
+ <span class="title title--large">Hello, TRMNL!</span>
3
+ </div>
4
+
5
+ <div class="title_bar">
6
+ <span class="title">My Plugin</span>
7
+ </div>
@@ -1 +1,7 @@
1
- half horizontal!
1
+ <div class="layout layout--col layout--center">
2
+ <span class="title title--large">Half horizontal</span>
3
+ </div>
4
+
5
+ <div class="title_bar">
6
+ <span class="title">My Plugin</span>
7
+ </div>
@@ -1 +1,7 @@
1
- half vertical!
1
+ <div class="layout layout--col layout--center">
2
+ <span class="title title--large">Half vertical</span>
3
+ </div>
4
+
5
+ <div class="title_bar">
6
+ <span class="title">My Plugin</span>
7
+ </div>
@@ -1 +1,7 @@
1
- quadrant!
1
+ <div class="layout layout--col layout--center">
2
+ <span class="title title--large">Quadrant</span>
3
+ </div>
4
+
5
+ <div class="title_bar">
6
+ <span class="title">My Plugin</span>
7
+ </div>
@@ -21,17 +21,20 @@ Gem::Specification.new do |spec|
21
21
  spec.metadata['rubygems_mfa_required'] = 'true'
22
22
 
23
23
  spec.files = Dir.chdir(__dir__) do
24
- [
24
+ files = [
25
25
  'bin/**/*',
26
26
  'db/**/*',
27
27
  'lib/**/*',
28
- 'templates/**/{*,.*}',
29
28
  'web/**/*',
30
29
  'CHANGELOG.md',
31
30
  'LICENSE.txt',
32
31
  'README.md',
33
32
  'trmnl_preview.gemspec'
34
33
  ].flat_map { |glob| Dir[glob] }
34
+
35
+ # FNM_DOTMATCH so the glob descends into the templates' hidden directories
36
+ # (e.g. .github/) — a plain Dir[] skips them and drops the file from the gem.
37
+ files + Dir.glob('templates/**/{*,.*}', File::FNM_DOTMATCH)
35
38
  end
36
39
  spec.bindir = 'bin'
37
40
  spec.executables = ['trmnlp']
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.8.1
4
+ version: 0.8.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rockwell Schrock
@@ -319,6 +319,8 @@ files:
319
319
  - lib/trmnlp/user_data_assembler.rb
320
320
  - lib/trmnlp/version.rb
321
321
  - lib/trmnlp/watcher.rb
322
+ - templates/init/.github/workflows/trmnl.yml
323
+ - templates/init/.gitignore
322
324
  - templates/init/.trmnlp.yml
323
325
  - templates/init/bin/trmnlp
324
326
  - templates/init/src/full.liquid