trmnl_preview 0.8.2 → 0.8.4
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 +14 -0
- data/README.md +17 -3
- data/lib/trmnlp/app.rb +40 -7
- data/lib/trmnlp/cli.rb +2 -0
- data/lib/trmnlp/commands/clone.rb +2 -2
- data/lib/trmnlp/commands/init.rb +13 -2
- data/lib/trmnlp/commands/serve.rb +3 -0
- data/lib/trmnlp/config/plugin.rb +7 -2
- data/lib/trmnlp/version.rb +1 -1
- data/lib/trmnlp/watcher.rb +18 -0
- data/templates/init/.github/workflows/trmnl.yml +30 -0
- data/templates/init/.gitignore +2 -0
- data/templates/init/src/full.liquid +7 -1
- data/templates/init/src/half_horizontal.liquid +7 -1
- data/templates/init/src/half_vertical.liquid +7 -1
- data/templates/init/src/quadrant.liquid +7 -1
- data/trmnl_preview.gemspec +5 -2
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9fe003b113f4c1d832529b4d01cc931a8eb63259f385db8d6ff2dfca919a7cb9
|
|
4
|
+
data.tar.gz: 2254239e25186dab5f29bebf22ea8a87a180362521cf89a40900d86dda9bb9fc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d5e588f871e595f7c8dd637b6a2203cc1bde1696b91a5c397b1c176c4306f28e4f96ecc76a583f23da75ab3355a4288897b2ee823c7d8e1d658a2114c13d6f6d
|
|
7
|
+
data.tar.gz: 55efd439389300e0b18fe29025446632dd04412564a6ff36f8bea60d06f12e324bf18c97429b237d90df3eda94262a6009158cd82df8aab30b3b629dd6d1ff5e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
|
|
2
2
|
# Changelog
|
|
3
3
|
|
|
4
|
+
## 0.8.4
|
|
5
|
+
|
|
6
|
+
- Fixed `trmnlp serve` hanging after switching between browser tabs. Live reload now uses `rack.hijack` so SSE connections release their Puma worker thread immediately instead of holding it for the lifetime of the tab.
|
|
7
|
+
- Fixed Ctrl-C requiring three presses to stop the dev server. filewatcher 3.0.1 was clobbering Puma's signal handlers with its own `trap('INT') { exit }`.
|
|
8
|
+
- Fixed scaffolded plugins silently skipping their transform when `settings.yml` had `serverless_language: ''`. The empty string was treated as truthy in Ruby and short-circuited the file-extension fallback.
|
|
9
|
+
- Docker examples in the README now use `--pull always` (and `pull_policy: always` for Compose) so a new release is picked up on the next `docker run` without a manual pull.
|
|
10
|
+
|
|
11
|
+
## 0.8.3
|
|
12
|
+
|
|
13
|
+
- `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`
|
|
14
|
+
- Added `--skip-git` to `trmnlp init` and `trmnlp clone` for projects that manage Git themselves
|
|
15
|
+
- The Docker image now ships `git` so the `docker run trmnl/trmnlp clone` flow leaves a ready-to-push project on the host
|
|
16
|
+
- View templates now ship canonical `layout` + `title_bar` markup that passes `trmnlp lint`
|
|
17
|
+
|
|
4
18
|
## 0.8.2
|
|
5
19
|
|
|
6
20
|
- Fixed `framework_version: latest` rendering against the auto-upgrading `/latest/` asset path instead of the current concrete release, matching the hosted service (#99)
|
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 |
|
|
@@ -123,9 +129,12 @@ If an environment variable is more convenient (for example in a CI/CD pipeline),
|
|
|
123
129
|
|
|
124
130
|
## Continuous Integration
|
|
125
131
|
|
|
126
|
-
`trmnlp`
|
|
127
|
-
|
|
128
|
-
|
|
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:
|
|
129
138
|
|
|
130
139
|
```yaml
|
|
131
140
|
name: TRMNL
|
|
@@ -194,11 +203,14 @@ trmnlp serve
|
|
|
194
203
|
|
|
195
204
|
```sh
|
|
196
205
|
docker run \
|
|
206
|
+
--pull always \
|
|
197
207
|
--publish 4567:4567 \
|
|
198
208
|
--volume "$(pwd):/plugin" \
|
|
199
209
|
trmnl/trmnlp serve
|
|
200
210
|
```
|
|
201
211
|
|
|
212
|
+
`--pull always` checks the registry on every run and pulls a newer image if one exists, so you don't have to remember to `docker pull` after each release.
|
|
213
|
+
|
|
202
214
|
Inside a container, `serve` binds to `0.0.0.0` automatically (it detects `/.dockerenv`) so the preview is reachable from your host browser. Outside Docker it binds to `127.0.0.1`.
|
|
203
215
|
|
|
204
216
|
Swap `serve` for any other command (`lint`, `login`, `clone`, etc.) to run it in a one-off container.
|
|
@@ -209,6 +221,7 @@ For running multiple commands (login, clone, serve), you can start an interactiv
|
|
|
209
221
|
|
|
210
222
|
```sh
|
|
211
223
|
docker run -it \
|
|
224
|
+
--pull always \
|
|
212
225
|
--publish 4567:4567 \
|
|
213
226
|
--volume "$HOME/.config/trmnlp:/root/.config/trmnlp" \
|
|
214
227
|
--volume "$(pwd):/plugin" \
|
|
@@ -235,6 +248,7 @@ For a checked-in config — like [`examples/hn-stories/`](examples/hn-stories/)
|
|
|
235
248
|
services:
|
|
236
249
|
trmnlp:
|
|
237
250
|
image: trmnl/trmnlp
|
|
251
|
+
pull_policy: always
|
|
238
252
|
command: ["serve"]
|
|
239
253
|
ports:
|
|
240
254
|
- "4567:4567"
|
data/lib/trmnlp/app.rb
CHANGED
|
@@ -77,17 +77,40 @@ module TRMNLP
|
|
|
77
77
|
JSON.pretty_generate(@user_data_assembler.call(device:))
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
-
# Live reload
|
|
81
|
-
#
|
|
82
|
-
#
|
|
80
|
+
# Live reload uses rack.hijack so the Puma worker thread is released the
|
|
81
|
+
# instant we have the raw socket — broadcasting then runs on our own
|
|
82
|
+
# thread, never competing with HTTP request workers. Adapted from the
|
|
83
|
+
# Faye::EventSource pattern in faye-websocket (lib/faye/rack_stream.rb)
|
|
84
|
+
# but without the EventMachine dependency: where Faye uses EM.attach to
|
|
85
|
+
# get a reactor callback on socket close, we detect close synchronously
|
|
86
|
+
# via the IOError/EPIPE raised by the next heartbeat write.
|
|
87
|
+
HEARTBEAT_SECONDS = 5
|
|
88
|
+
|
|
83
89
|
get '/live_reload' do
|
|
84
|
-
|
|
90
|
+
hijack = env['rack.hijack']
|
|
91
|
+
halt 500, 'rack.hijack unavailable' unless hijack
|
|
92
|
+
hijack.call
|
|
93
|
+
io = env['rack.hijack_io']
|
|
94
|
+
|
|
85
95
|
queue = Thread::Queue.new
|
|
86
96
|
@live_reload_clients << queue
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
97
|
+
|
|
98
|
+
Thread.new do
|
|
99
|
+
io.write("HTTP/1.1 200 OK\r\n" \
|
|
100
|
+
"Content-Type: text/event-stream\r\n" \
|
|
101
|
+
"Cache-Control: no-cache\r\n" \
|
|
102
|
+
"Connection: close\r\n\r\n")
|
|
103
|
+
run_live_reload_loop(io, queue)
|
|
104
|
+
rescue IOError, Errno::EPIPE, Errno::ECONNRESET
|
|
105
|
+
# client disconnected mid-write — normal termination
|
|
106
|
+
ensure
|
|
107
|
+
@live_reload_clients.delete(queue)
|
|
108
|
+
io.close
|
|
90
109
|
end
|
|
110
|
+
|
|
111
|
+
# -1 status tells the server we've hijacked the response. The body
|
|
112
|
+
# is never iterated; the thread above owns the socket from here.
|
|
113
|
+
[-1, {}, []]
|
|
91
114
|
end
|
|
92
115
|
|
|
93
116
|
get '/poll' do
|
|
@@ -128,6 +151,16 @@ module TRMNLP
|
|
|
128
151
|
|
|
129
152
|
private
|
|
130
153
|
|
|
154
|
+
# On timeout (queue idle), a colon-prefixed SSE comment line both
|
|
155
|
+
# keeps proxies awake and surfaces a dead client via the next
|
|
156
|
+
# io.write — the route's outer rescue then cleans up.
|
|
157
|
+
def run_live_reload_loop(io, queue)
|
|
158
|
+
loop do
|
|
159
|
+
message = queue.pop(timeout: HEARTBEAT_SECONDS)
|
|
160
|
+
io.write(message || ": heartbeat\n\n")
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
131
164
|
# ScreenGenerator is request-scoped — it carries the per-request width,
|
|
132
165
|
# height, and color_depth — so it is built here rather than on the shared
|
|
133
166
|
# Context graph. Screenshots are a serve-only concern and would not belong
|
data/lib/trmnlp/cli.rb
CHANGED
|
@@ -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
|
|
data/lib/trmnlp/commands/init.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
@@ -22,6 +22,9 @@ module TRMNLP
|
|
|
22
22
|
App.set(:browser_pool, BrowserPool.new(driver_factory: FirefoxDriver.method(:build)))
|
|
23
23
|
App.set(:bind, options.bind)
|
|
24
24
|
App.set(:port, options.port)
|
|
25
|
+
# Each live-reload SSE connection holds a Puma thread for the tab's
|
|
26
|
+
# lifetime; the default 0:5 pool runs out fast with multiple tabs open.
|
|
27
|
+
App.set(:server_settings, Threads: '1:16')
|
|
25
28
|
permit_all_hosts if codespaces?
|
|
26
29
|
|
|
27
30
|
# Finally, start the app!
|
data/lib/trmnlp/config/plugin.rb
CHANGED
|
@@ -73,8 +73,13 @@ module TRMNLP
|
|
|
73
73
|
# Explicit language for transform.* code. If absent, the language
|
|
74
74
|
# is inferred from the file extension by Paths#transform_file.
|
|
75
75
|
# This one lives on the plugin (settings.yml) because production
|
|
76
|
-
# stores it on the plugin_setting record.
|
|
77
|
-
|
|
76
|
+
# stores it on the plugin_setting record. The scaffold emits
|
|
77
|
+
# `serverless_language: ''`, so empty strings collapse to nil here
|
|
78
|
+
# to let the `||` in the pipeline fall through to the inferred value.
|
|
79
|
+
def serverless_language
|
|
80
|
+
value = @config['serverless_language']
|
|
81
|
+
value unless value.to_s.empty?
|
|
82
|
+
end
|
|
78
83
|
|
|
79
84
|
# The TRMNL design-system version this plugin renders against.
|
|
80
85
|
# Lives on the plugin (settings.yml), like serverless_language,
|
data/lib/trmnlp/version.rb
CHANGED
data/lib/trmnlp/watcher.rb
CHANGED
|
@@ -4,6 +4,24 @@ require 'filewatcher'
|
|
|
4
4
|
|
|
5
5
|
require_relative 'reporter'
|
|
6
6
|
|
|
7
|
+
# filewatcher 3.0.1's #watch installs `trap('INT') { exit }` (and HUP/TERM)
|
|
8
|
+
# unconditionally, clobbering Puma's clean-shutdown handler — Ctrl-C then
|
|
9
|
+
# raises SystemExit instead of triggering Puma's graceful stop, which is
|
|
10
|
+
# why the container needs three SIGINTs to die. The gem offers no opt-out;
|
|
11
|
+
# its supported shutdown is Filewatcher#stop, which we're not using. We
|
|
12
|
+
# replace the trap-installing #watch with the rest of its body so signals
|
|
13
|
+
# fall through to whatever handler the host process installed (Puma).
|
|
14
|
+
Filewatcher.prepend(Module.new do
|
|
15
|
+
def watch(&on_update)
|
|
16
|
+
@on_update = on_update
|
|
17
|
+
@keep_watching = true
|
|
18
|
+
yield({ '' => '' }) if @immediate
|
|
19
|
+
main_cycle
|
|
20
|
+
@end_snapshot = current_snapshot
|
|
21
|
+
finalize(&on_update)
|
|
22
|
+
end
|
|
23
|
+
end)
|
|
24
|
+
|
|
7
25
|
module TRMNLP
|
|
8
26
|
class Watcher
|
|
9
27
|
def initialize(config:, user_data_assembler:, transform_pipeline:, reporter: Reporter.new)
|
|
@@ -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 }}
|
data/trmnl_preview.gemspec
CHANGED
|
@@ -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.
|
|
4
|
+
version: 0.8.4
|
|
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
|