trek 2.0.1 → 2.1.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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.prettierignore +2 -0
  3. data/.rubocop.yml +11 -0
  4. data/AGENTS.md +5 -2
  5. data/CHANGELOG.md +34 -1
  6. data/Gemfile.lock +1 -1
  7. data/Makefile +13 -0
  8. data/app/components/trek/entries/item_component/item_component.css +1 -0
  9. data/app/components/trek/form/content_editor_component/content_editor_component.js +2 -0
  10. data/app/components/trek/form/content_editor_component.rb +4 -2
  11. data/app/components/trek/form/fieldset_component/fieldset_component.css +4 -3
  12. data/app/components/trek/form/group_component/group_component.css +5 -2
  13. data/app/components/trek/menu_component.rb +15 -2
  14. data/app/controllers/concerns/trek/model.rb +13 -6
  15. data/app/controllers/trek/resource_controller.rb +9 -1
  16. data/app/helpers/trek/i18n_helper.rb +1 -1
  17. data/docs/.vitepress/config.mjs +2 -1
  18. data/docs/deploy.md +57 -0
  19. data/docs/getting-started.md +1 -0
  20. data/docs/reference/controllers.md +23 -0
  21. data/docs/reference/generators/install.md +2 -0
  22. data/docs/reference/generators/scaffold.md +12 -0
  23. data/lib/generators/trek/install/config_generator.rb +9 -1
  24. data/lib/generators/trek/install/postmark_generator.rb +2 -0
  25. data/lib/generators/trek/install/prosopite_generator.rb +56 -0
  26. data/lib/generators/trek/install/scalingo_generator.rb +19 -7
  27. data/lib/generators/trek/install/yarn_generator.rb +1 -1
  28. data/lib/generators/trek/install_generator.rb +1 -0
  29. data/lib/generators/trek/scaffold_generator.rb +37 -5
  30. data/lib/generators/trek/templates/controllers/admin/scaffold_controller.rb.tt +2 -2
  31. data/lib/generators/trek/templates/locales/scaffold/model.en.yml.tt +4 -3
  32. data/lib/generators/trek/templates/spec/requests/admin/n_plus_one_spec.rb +28 -0
  33. data/lib/generators/trek/templates/views/admin/scaffold/show.html.slim.tt +2 -2
  34. data/lib/generators/trek/upgrade/v2_generator.rb +31 -0
  35. data/lib/trek/version.rb +1 -1
  36. data/package.json +1 -1
  37. data/vendor/tom_select/css/tom-select.custom.css +0 -1
  38. metadata +5 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36520ed5aaf6b1c69a1b34ffc8d2bf89bf225c0548edbb62909edf18fa54c16a
4
- data.tar.gz: '05296e71c1df00a73fe624ac48e5c0631f048480c643fe01a194c077405bed16'
3
+ metadata.gz: 5a2b7ab45c51c036cab94f7ab2f6119b84cfccf501d1133cd4f390e320d40aa6
4
+ data.tar.gz: '0803283b1a6283983d174c9797d5ea753f97ffc1ce458e0285ab2ac681257ecf'
5
5
  SHA512:
6
- metadata.gz: 796b4ecad9967d6d800ba0ffe37bd3b0e9eb51bbcbfe9d27c1c9e95a0ea6b7393561edf423dfdc2dcf16fda0402097bd83e9a30b2cacf3588133bdf66526502e
7
- data.tar.gz: 791afe1a759ffe45012cd98c6d49bf766d72233b65d8b51106eb77a9bd11a1fb45f3fe2a7a2d4247ebf5209807d46094494b3745d94d282a3f811553aac6a05a
6
+ metadata.gz: 10aea34299dba23456b3f687c6e450996d5473e8d34d9a12200373c2e3f394025e6e5fc2aab4d2dfe00628133113171354780ab3123c6628dc5480e75ed87987
7
+ data.tar.gz: 966a6004dc2aaa1b7ff21c4db7c87d00e3eb4054ea3dbaec780c62823b0d297f8736be2d93216dc438820375aa3487063882325fd55cb8f612ec831a9179c830
data/.prettierignore ADDED
@@ -0,0 +1,2 @@
1
+ # esbuild output (gitignored), present locally after `yarn build`
2
+ app/assets/builds/
data/.rubocop.yml CHANGED
@@ -33,6 +33,17 @@ Style/StringLiteralsInInterpolation:
33
33
  Layout/LineLength:
34
34
  Max: 120
35
35
 
36
+ # rubocop-rails-omakase disables both so it can indent methods under
37
+ # `private`; we use the normal style (methods at the class-body level,
38
+ # `private` included) and want `rubocop --autocorrect` to fix indentation,
39
+ # generated files included.
40
+ Layout/IndentationWidth:
41
+ Enabled: true
42
+
43
+ Layout/IndentationConsistency:
44
+ Enabled: true
45
+ EnforcedStyle: normal
46
+
36
47
  Gemspec/RequiredRubyVersion:
37
48
  Enabled: false
38
49
 
data/AGENTS.md CHANGED
@@ -21,11 +21,13 @@ bin/setup # bundle install + yarn install (dev setup)
21
21
  bin/console # IRB with trek loaded
22
22
  rake # default: spec + rubocop
23
23
  rake spec # run tests (or: bundle exec rspec)
24
+ make lint # all linters below
25
+ make fix # rubocop/eslint/stylelint/prettier autocorrect
24
26
  make release # bump version (version.rb + package.json), stamp CHANGELOG, tag, push gem + npm
25
27
  yarn docs:dev # VitePress docs locally
26
28
  ```
27
29
 
28
- Linters (also run automatically via lefthook on pre-commit):
30
+ Linters (`make lint`; also run automatically via lefthook on pre-commit):
29
31
 
30
32
  ```bash
31
33
  bundle exec rubocop
@@ -51,5 +53,6 @@ Security: `bundle exec brakeman` · `bundle exec bundle-audit check --update`
51
53
  - **Slim** linted via slim_lint; **JS/CSS** via ESLint + Stylelint + Prettier 2.8.8
52
54
  - **i18n**: keep English + French locales in sync (`config/locales/trek.*.yml`); DeepL auto-translation via `DEEPL_AUTH_KEY`
53
55
  - **Versioning**: version lives in both `lib/trek/version.rb` and `package.json` — keep in sync via `make release`
54
- - **CHANGELOG.md**: Keep-a-Changelog format. **Every MR must add an `[Unreleased]` entry.** When the change requires action in the host app on upgrade, add an "Upgrade note" describing what to do. When the change lets host apps remove or simplify existing patches or custom implementations, mention that too (e.g. "you can now drop your custom …").
56
+ - **CHANGELOG.md**: Keep-a-Changelog format. **Every MR must add an `[Unreleased]` entry.** When the change requires action in the host app on upgrade, end the entry with a bold `**Upgrade note:**` label followed by what to do. Use the same label when the change lets host apps remove or simplify existing patches or custom implementations (e.g. "**Upgrade note:** you can now drop your custom …") — never as an unlabelled trailing sentence.
57
+ - **Public code**: Trek ships publicly (gem, npm, docs), so nothing committed may mention a client or project name, nor a client's domain models or business logic — not in code, comments, specs, templates, docs, CHANGELOG or commit messages. Use neutral examples (`Page`, `Shop::Product`, an engine's `Blog::Post`). Client context belongs in GitLab issues and MR descriptions only.
55
58
  - **CI**: GitLab CI (`.gitlab-ci.yml`), stages build → lint → security → test → deploy; docs deploy to Netlify; deps via Renovate
data/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [2.1.0] - 2026-08-25
4
+
5
+ ### Added
6
+
7
+ - Docs: a [Deploying to Scalingo](https://trek.etamin.studio/deploy) guide covering the trek-buildpack setup, the environment to configure, and how to migrate an app from the stock buildpacks.
8
+ - `trek:install:prosopite` generator (wired into `trek:install`) sets up [Prosopite](https://github.com/charkost/prosopite) N+1 query detection (#211): adds `prosopite` and `pg_query` to the `development`/`test` Gemfile group and a `config/initializers/prosopite.rb` that mounts the Rack middleware — so every request is scanned, Trek's engine controllers included — raising `Prosopite::NPlusOneQueriesError` in test and logging in development. It also generates `spec/requests/admin/n_plus_one_spec.rb`, a smoke request spec rendering the dashboard, pages index and fragments index as an admin (extend it with the app's other admin routes). Documented under [Controllers › N+1 detection](https://trek.etamin.studio/reference/controllers#n-1-detection) with the `Prosopite.pause` escape hatch. **Upgrade note:** existing apps can run `rails g trek:install:prosopite`; apps that already wired Prosopite by hand can keep their setup (the generator skips the gems when `prosopite` is already in the Gemfile and won't overwrite an existing initializer).
9
+ - `make lint` and `make fix` targets in Trek's own Makefile, mirroring the ones generated for host apps (RuboCop, slim-lint, ESLint, Stylelint, Prettier — the `fix` variant autocorrects all but slim-lint). A `.prettierignore` excludes the gitignored esbuild output in `app/assets/builds/`, which made the Prettier check fail on a local checkout that had run `yarn build`.
10
+ - `Trek::MenuComponent` items accept an `active:` option for menu entries whose pages live under other controllers: a `String` is matched as a `controller_path` prefix, a `Regexp` against the whole path, and a boolean forces (or clears) the state — e.g. `s.with_item(text: …, href: [:admin, Shop], active: "admin/shop")` keeps _Shop_ highlighted while editing its products. Without it, the item stays active when its href matches the current controller, as before.
11
+ - `content_editor` accepts an `autofocus:` option (default `false`): when true, the editor grabs focus on mount with the cursor at the end of the content — useful when the editor is the primary field of a side panel.
12
+
13
+ ### Changed
14
+
15
+ - The shared `.rubocop.yml` (Trek's own, and the one `trek:install:rubocop` copies into host apps) re-enables `Layout/IndentationWidth` and `Layout/IndentationConsistency` with `EnforcedStyle: normal`. rubocop-rails-omakase disables both to allow its house style of indenting methods under `private`; Trek's style keeps methods at the class-body level, and a plain `rubocop --autocorrect` now fixes indentation — in generated files too. **Upgrade note:** copy the two cop blocks from the template (`rails g trek:install:rubocop` will prompt to overwrite) into your app's `.rubocop.yml`, then run `bundle exec rubocop --autocorrect` once; methods indented under `private` and mis-indented generated files get fixed.
16
+ - `trek:install:scalingo` now targets the dedicated [trek-buildpack](https://github.com/etaminstudio/trek-buildpack) (#193) instead of the stock apt + ruby buildpack pair: it no longer generates `.buildpacks` (and removes an existing one) and prints the `env-set` commands to run (`BUILDPACK_URL` pinned to the buildpack's release tag, and the `DATABASE_URL=$SCALINGO_POSTGRESQL_URL` mapping). The buildpack pins Ruby/Node/Yarn from `.ruby-version`, the `.node-version` that `rails new` already generates, and `package.json`'s `packageManager`, runs `yarn install` once, and ships a slimmed image (~40% smaller, measured deploys down from ~3min to ~1min35) — see the buildpack README for details. **Upgrade note:** on each Scalingo app, set `BUILDPACK_URL=https://github.com/etaminstudio/trek-buildpack#v0.1.0`, delete `.buildpacks`, make sure `.node-version` exists (apps generated with `rails new -j esbuild` have it), and check that `config/database.yml` reads `DATABASE_URL` in production (mapped with `scalingo env-set 'DATABASE_URL=$SCALINGO_POSTGRESQL_URL'`).
17
+ - `Trek::Form::FieldsetComponent`: the fieldset title (`.c-trek--form--fieldset-title`) now shares the form group label style (`--slate-12`, semibold, `1.2rem` bottom margin) so a titled fieldset reads like a labelled group; the `section` modifier only adds its larger font size on top. Hosts that restyled the plain fieldset title themselves can drop that override.
18
+ - `trek:install:yarn` now installs **Yarn 4.18.0** (was 4.17.0). **Upgrade note:** existing apps can upgrade with `yarn set version 4.18.0` followed by `yarn install`.
19
+
20
+ ### Fixed
21
+
22
+ - `trek:scaffold` now references the model top-level qualified (`::Shop::Product`) in the generated controller's `model` and in the `Admin::Dashboard::MODELS_IN_MENU` entry: once the scaffold creates the `Admin::Shop` namespace, a bare `Shop` under `module Admin` resolves to that module instead of the model — the scaffolded controller raised `NameError`, and previously generated code referencing a `Shop` model silently got the module. The generator also warns when a namespace segment matches an existing model file that `rails g model` would replace with a namespace module. (#201) **Upgrade note:** if a scaffold introduced a namespace named like an existing model, qualify that model as `::Shop` in the existing `Admin::…Controller#model` and `MODELS_IN_MENU` entries.
23
+ - `Trek::Entries::ItemComponent`: the details list (`.c-trek--entries--item-details`) now wraps (`flex-wrap: wrap`) instead of overflowing or squeezing its items when an entry has many details or a narrow container.
24
+ - `trek:install:postmark` is idempotent again: re-running it on an app where it had already run appended a second `config.action_mailer.delivery_method = :postmark` / `postmark_settings` block to `config/environments/production.rb` (the gem, credentials and mailer steps were already guarded). The config injection is now skipped when the file already sets `delivery_method = :postmark`. (#210)
25
+ - Namespaced models (e.g. `Shop::Product`, routed under `namespace :shop`) now work out of the box with `Trek::ResourceController` (#212): `object_params` requires `model_name.param_key` (`shop_product`) instead of the demodulized element, so create/update no longer 400; `model_element_route` no longer prefixes the namespace when the route key already carries it (`shop_product`), so the delete button and list item links build `admin_shop_product_path` instead of the double-namespaced `admin_shop_shop_product_path` — models from isolated engines with relative naming (e.g. an engine's `Blog::Post`, route key `post`) keep the `[:blog, :post]` form; the controller defines `implicit_authorization_target` (`@object || model`) so `authorized(params)` and `Trek::Reorderable#reorder`'s bare `authorize!` find the policy; and `i18n_model_gender` reads `activerecord.models.<i18n_key>.gender` (`shop/product`), so `i18n_resource_label` picks the declared gender instead of the neutral fallback. **Upgrade note:** hosts that worked around this with a controller concern overriding `implicit_authorization_target`/`object_params`/`model_element_route`, or a prepend on `Trek::I18nHelper#i18n_model_gender`, can drop them.
26
+ - `Trek::Form::GroupComponent` now styles `input[type="time"]` like `input[type="date"]` (field frame, 16rem max width, lighter picker colour); `time_field`s used to render as bare browser inputs.
27
+ - Removed the dangling `sourceMappingURL` comment from the vendored `tom_select/css/tom-select.custom.css`: the referenced `.map` file is not shipped, so every host-app esbuild build (including `assets:precompile` on deploy) logged a `missing-source-map` debug warning. (#209)
28
+ - `trek:scaffold` now re-indents the generated controller and policy: for a namespaced model the nested `module` wrappers were emitted without indenting the class body, tripping the host app's linters. The generator runs a targeted `rubocop --autocorrect --only Layout/IndentationWidth,Layout/IndentationConsistency` on both files (forced with `--only` since rubocop-rails-omakase leaves those cops disabled). (#199)
29
+ - The `config/initializers/host.rb` generated by `trek:install:config` no longer references `ActionMailer::Base` during the initializer phase: doing so fires Action Mailer's load hook before the app's autoloader can resolve app classes, so an app configuring a custom `config.action_mailer.delivery_job` (e.g. `"MailDeliveryJob"`) crashed with a `NameError` on every production boot — breaking `assets:precompile` on deploy. The assignment is now deferred with `ActiveSupport.on_load(:action_mailer)`. (#208) **Upgrade note:** apply the same change to your app's `config/initializers/host.rb` — replace the `ActionMailer::Base.default_url_options = … if Rails.env.production?` line with the deferred block (see the generator template).
30
+ - `trek:scaffold` now generates the right controller class for a namespaced model (e.g. `rails g trek:scaffold Shop::Product`): the template used the still-namespaced `controller_class_name` inside the per-namespace `module` wrappers, defining `Admin::Shop::Shop::ProductsController` while the generated route expected `Admin::Shop::ProductsController` — every request 404ed with an uninitialized constant. The class name is now demodulized, like the policy template already did. (#200)
31
+ - The `show.html.slim` generated by `trek:scaffold` no longer ends with a trailing blank line: the attribute loop emitted its separator after every item instead of between items, so slim-lint failed in host apps with `TrailingBlankLines: Multiple empty lines in the end of file`. (#205)
32
+ - `trek:upgrade:v2` now upgrades Yarn to 4.18.0 first (applying the 0.1.26 upgrade note: `yarn set version` + dropping the `interactive-tools` plugin) when the app still runs Yarn 3. Previously the generator wrote `npmPreapprovedPackages` — a Yarn 4-only setting — unconditionally, which under Yarn 3 is a hard error that breaks every yarn command, including the generator's own `yarn add` step.
33
+ - Clarified the 0.1.26 changelog upgrade note about the Yarn minimal-age-gate exemption: it only applied to the pre-2.0.0 private GitLab distribution — since 2.0.0 the (Yarn 4-only) `npmPreapprovedPackages` entry, set up automatically by `trek:install` and `trek:upgrade:v2`, replaces it.
34
+ - Fixed whitespace in `model.en.yml.tt` which caused prettier to fail.
35
+
3
36
  ## [2.0.1] - 2026-08-18
4
37
 
5
38
  ### Fixed
@@ -41,7 +74,7 @@
41
74
  ### Fixed
42
75
 
43
76
  - Added the missing French translations that fell back to English: `admin.dialog_component.{title,text}` (the confirmation dialog shown on every destroy action) and `admin.{pages,fragments}.{show.intro,reorder.success,search.alert}`. Also added the English `admin.section`/`admin.sections` flash messages that previously existed only in French. (#194)
44
- - `trek:install:yarn` now exempts the `@etaminstudio` scope from Yarn's minimal-age gate (`npmMinimalAgeGate: 0` under `npmScopes.etaminstudio` in the generated `.yarnrc.yml`), so a freshly published `@etaminstudio/trek` release installs without being quarantined; the gate still applies to public packages. **Upgrade note:** existing apps can add `npmMinimalAgeGate: 0` under `npmScopes.etaminstudio` in their `.yarnrc.yml`.
77
+ - `trek:install:yarn` now exempts the `@etaminstudio` scope from Yarn's minimal-age gate (`npmMinimalAgeGate: 0` under `npmScopes.etaminstudio` in the generated `.yarnrc.yml`), so a freshly published `@etaminstudio/trek` release installs without being quarantined; the gate still applies to public packages. **Upgrade note:** existing apps can add `npmMinimalAgeGate: 0` under `npmScopes.etaminstudio` in their `.yarnrc.yml`. _(Only relevant to the private GitLab distribution used before 2.0.0: the `@etaminstudio` scope is retired since then, so this scope exemption does nothing for the public unscoped `trek` package. From 2.0.0 the equivalent is `trek` listed under `npmPreapprovedPackages` — a Yarn 4-only setting, hence the Yarn upgrade above — which `trek:install` generates and `trek:upgrade:v2` adds during migration, so no manual step is needed.)_
45
78
  - `trek:install:locales` no longer generates an invalid `config/initializers/locales.rb` (`I18n.available_locales = [:]` / `I18n.default_locale = :`) when passed no or blank locales. A blank option/env now falls back to the default (`en, fr`), blank entries are dropped, and the list can never end up empty. (#169)
46
79
 
47
80
  ## [0.1.25] - 2026-07-01
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- trek (2.0.1)
4
+ trek (2.1.0)
5
5
  action_policy (~> 0.6)
6
6
  actioncable
7
7
  acts_as_list (~> 1.1)
data/Makefile CHANGED
@@ -1,5 +1,18 @@
1
1
  .PHONY: *
2
2
 
3
+ lint:
4
+ bundle exec rubocop
5
+ bundle exec slim-lint "app/**/*.slim"
6
+ yarn eslint "app/**/*.js"
7
+ yarn stylelint "app/**/*.css"
8
+ yarn prettier --check "{app,config,lib,spec}/**/*.{css,js,json,md,yml}"
9
+
10
+ fix:
11
+ bundle exec rubocop --autocorrect
12
+ yarn eslint "app/**/*.js" --fix
13
+ yarn stylelint "app/**/*.css" --fix
14
+ yarn prettier --write "{app,config,lib,spec}/**/*.{css,js,json,md,yml}"
15
+
3
16
  release:
4
17
  @read -p "New version (current: $$(sed -n 's/.*VERSION = "\(.*\)"/\1/p' lib/trek/version.rb)): " version; \
5
18
  if [ -z "$$version" ]; then echo "Version cannot be empty"; exit 1; fi; \
@@ -92,6 +92,7 @@ table .root {
92
92
  .details {
93
93
  color: var(--slate-9);
94
94
  display: flex;
95
+ flex-wrap: wrap;
95
96
  font-size: 1.3rem;
96
97
  line-height: 1.2;
97
98
  list-style: none;
@@ -23,6 +23,7 @@ export class Controller extends BaseController {
23
23
  ];
24
24
  static values = {
25
25
  placeholder: String,
26
+ autofocus: Boolean,
26
27
  };
27
28
 
28
29
  connect() {
@@ -55,6 +56,7 @@ export class Controller extends BaseController {
55
56
  PromptsBlock,
56
57
  ],
57
58
  content: this.contentJson,
59
+ autofocus: this.autofocusValue ? "end" : false,
58
60
  });
59
61
 
60
62
  this.editor.on("update", () => this.updateContent());
@@ -8,7 +8,7 @@ module Trek
8
8
  include Turbo::FramesHelper
9
9
  include ViewComponent::Translatable
10
10
 
11
- attr_reader :nodes, :blocks, :floating
11
+ attr_reader :nodes, :blocks, :floating, :autofocus
12
12
 
13
13
  def initialize(form, object_name, method_name, options = {})
14
14
  @nodes = options.delete(:nodes)
@@ -17,6 +17,7 @@ module Trek
17
17
  @blocks = true if @blocks.nil?
18
18
  @floating = options.delete(:floating)
19
19
  @floating = true if @floating.nil?
20
+ @autofocus = options.delete(:autofocus) || false
20
21
 
21
22
  super
22
23
  end
@@ -29,7 +30,8 @@ module Trek
29
30
  }.merge(
30
31
  stimulus_class_hash("active", class_for("is-active")),
31
32
  stimulus_class_hash("empty", class_for("is-empty")),
32
- stimulus_value_hash("placeholder", t(".placeholder"))
33
+ stimulus_value_hash("placeholder", t(".placeholder")),
34
+ stimulus_value_hash("autofocus", autofocus)
33
35
  )
34
36
  end
35
37
 
@@ -10,6 +10,10 @@
10
10
  }
11
11
 
12
12
  .title {
13
+ color: var(--slate-12);
14
+ display: block;
15
+ font-weight: var(--semibold);
16
+ margin-bottom: 1.2rem;
13
17
  margin-left: -1rem;
14
18
  padding: 0 1rem;
15
19
  }
@@ -35,8 +39,5 @@
35
39
  }
36
40
 
37
41
  .root.section .title {
38
- color: var(--slate-12);
39
42
  font-size: 1.2em;
40
- font-weight: var(--semibold);
41
- margin-bottom: 1.2rem;
42
43
  }
@@ -32,6 +32,7 @@
32
32
  .root input[type="text"],
33
33
  .root input[type="url"],
34
34
  .root input[type="date"],
35
+ .root input[type="time"],
35
36
  .root select,
36
37
  .root textarea {
37
38
  background-color: var(--slate-2);
@@ -55,7 +56,8 @@
55
56
 
56
57
  .root input[type="number"],
57
58
  .root input[name*="color"],
58
- .root input[type="date"] {
59
+ .root input[type="date"],
60
+ .root input[type="time"] {
59
61
  max-width: 16rem;
60
62
  }
61
63
 
@@ -63,7 +65,8 @@
63
65
  max-width: 19rem;
64
66
  }
65
67
 
66
- .root input[type="date"] {
68
+ .root input[type="date"],
69
+ .root input[type="time"] {
67
70
  color: var(--slate-11);
68
71
  font-weight: var(--light);
69
72
  }
@@ -57,6 +57,11 @@ module Trek
57
57
  option :href
58
58
  option :text
59
59
  option :allowed_to, default: -> { true }
60
+ # Overrides the active state for a section whose sub-resources live under other
61
+ # controllers: a String is a controller_path prefix ("admin/shop"), a Regexp is matched
62
+ # against it, a boolean forces the state. By default the item is active when its href
63
+ # matches the current controller.
64
+ option :active, optional: true
60
65
 
61
66
  def call
62
67
  tag.li(
@@ -79,10 +84,18 @@ module Trek
79
84
  private
80
85
 
81
86
  def active?
82
- href_path = Array.wrap(href).map do |part|
87
+ case active
88
+ when nil then href_path == controller_path
89
+ when String then controller_path.start_with?(active)
90
+ when Regexp then active.match?(controller_path)
91
+ else active
92
+ end
93
+ end
94
+
95
+ def href_path
96
+ Array.wrap(href).map do |part|
83
97
  part.respond_to?(:model_name) ? part.model_name.collection : part
84
98
  end.join("/").sub("root", "dashboards")
85
- href_path == controller_path
86
99
  end
87
100
  end
88
101
  end
@@ -21,7 +21,7 @@ module Trek
21
21
  end
22
22
 
23
23
  # Returns model collection as an array of symbols for route building.
24
- # For namespaced models like Missive::List, returns [:missive, :lists].
24
+ # For namespaced models like Shop::Product, returns [:shop, :products].
25
25
  # For non-namespaced models like Page, returns [:pages].
26
26
  def model_collection_route
27
27
  model_name.collection.split("/").map(&:to_sym)
@@ -32,13 +32,20 @@ module Trek
32
32
  end
33
33
 
34
34
  # Returns model element as an array of symbols for route building.
35
- # For namespaced models like Missive::List, returns [:missive, :list].
36
35
  # For non-namespaced models like Page, returns [:page].
37
- # Derived from collection (which preserves namespace) rather than element
38
- # (which demodulizes).
36
+ # For namespaced models, it depends on the route key: callers build member
37
+ # routes as `[:admin, *model_element_route[0...-1], record]`, and the
38
+ # record's route key must not carry the namespace twice.
39
+ # - Shop::Product (plain app namespace, route key `shop_product`)
40
+ # returns [:shop_product] — a [:shop, :product] prefix would yield
41
+ # `admin_shop_shop_product_path`.
42
+ # - Blog::Post from an isolated engine (`isolate_namespace` → relative
43
+ # model naming, route key `post`) returns [:blog, :post], since the
44
+ # namespace segment is needed for `admin_blog_post_path`.
39
45
  def model_element_route
40
- parts = model_name.collection.split("/")
41
- (parts[0...-1] + [parts.last.singularize]).map(&:to_sym)
46
+ element = model_name.singular_route_key
47
+ namespace = element == model_name.element ? model_name.collection.split("/")[0...-1] : []
48
+ (namespace + [element]).map(&:to_sym)
42
49
  end
43
50
 
44
51
  def model_intro(action: nil)
@@ -79,8 +79,16 @@ module Trek
79
79
  authorize! @object
80
80
  end
81
81
 
82
+ # The form posts under model_name.param_key, which keeps the namespace
83
+ # (shop_product for Shop::Product) unlike model_element (product).
82
84
  def object_params
83
- authorized(params.require(model_element))
85
+ authorized(params.require(model_name.param_key))
86
+ end
87
+
88
+ # action_policy infers the target from controller_name.classify, which
89
+ # doesn't exist for a namespaced model (Product for Shop::Product).
90
+ def implicit_authorization_target
91
+ @object || model
84
92
  end
85
93
  end
86
94
  end
@@ -11,7 +11,7 @@ module Trek
11
11
  end
12
12
 
13
13
  def i18n_model_gender(model_name)
14
- t("activerecord.models.#{model_name.element}.gender", default: "n")
14
+ t("activerecord.models.#{model_name.i18n_key}.gender", default: "n")
15
15
  end
16
16
 
17
17
  def boolean_collection(key: "boolean_labels")
@@ -33,7 +33,8 @@ export default defineConfig({
33
33
  {
34
34
  text: 'Guides',
35
35
  items: [
36
- { text: 'How to customize Trek', link: '/customize' }
36
+ { text: 'How to customize Trek', link: '/customize' },
37
+ { text: 'Deploying to Scalingo', link: '/deploy' }
37
38
  ]
38
39
  },
39
40
  {
data/docs/deploy.md ADDED
@@ -0,0 +1,57 @@
1
+ # Deploying to Scalingo
2
+
3
+ Trek apps deploy to [Scalingo](https://scalingo.com) with the dedicated [trek-buildpack](https://github.com/etaminstudio/trek-buildpack), a single buildpack that replaces the usual `apt` + `ruby` buildpack pair and knows how a Trek app is built.
4
+
5
+ ## What the buildpack does
6
+
7
+ - installs the APT packages listed in `Aptfile` (image processing libraries, jemalloc), skipping `apt-get update` and package resolution entirely when the `Aptfile` is unchanged
8
+ - installs Ruby with [rv](https://rv.dev) — precompiled, in seconds — using the version pinned in `.ruby-version`
9
+ - installs Node.js pinned by `.node-version` (generated by `rails new`), and the exact Yarn release pinned by `package.json`'s `packageManager` field — no unpinned defaults
10
+ - runs `yarn install` exactly once, with a persistent cache, before `assets:precompile`
11
+ - slims the shipped image: `node_modules`, Yarn caches and Node.js itself are dropped after the build, roughly halving the image size of a typical Trek app — faster deploys, restarts and scaling
12
+
13
+ Compared with the stock buildpacks, deploys typically go from around 3 minutes to about 1 minute 30 with a warm cache.
14
+
15
+ ## Set up the app
16
+
17
+ Generate the Scalingo files (an `Aptfile` and a `Procfile` with the web process and post-deploy migrations):
18
+
19
+ ```sh
20
+ rails g trek:install:scalingo
21
+ ```
22
+
23
+ Then configure the Scalingo app's environment, pinning the buildpack to a release tag:
24
+
25
+ ```sh
26
+ scalingo --app <app> env-set BUILDPACK_URL=https://github.com/etaminstudio/trek-buildpack#v0.1.0
27
+ scalingo --app <app> env-set 'DATABASE_URL=$SCALINGO_POSTGRESQL_URL'
28
+ scalingo --app <app> env-set RAILS_MASTER_KEY=$(cat config/master.key)
29
+ ```
30
+
31
+ Make sure `config/database.yml` reads `DATABASE_URL` in production — the section `rails new` generates targets a local socket with a password variable instead:
32
+
33
+ ```yml
34
+ production:
35
+ primary: &primary_production
36
+ <<: *default
37
+ url: <%= ENV["DATABASE_URL"] %>
38
+ ```
39
+
40
+ Provision a PostgreSQL addon (`scalingo --app <app> addons-add postgresql <plan>`), push, and the app boots with `RAILS_ENV`, `RAILS_LOG_TO_STDOUT` and `RAILS_SERVE_STATIC_FILES` already set by the buildpack.
41
+
42
+ ## Options
43
+
44
+ | Variable | Effect |
45
+ | --- | --- |
46
+ | `TREK_BUILDPACK_RUNTIME_NODE=1` | Ship Node.js in the image (only if the app runs JavaScript at runtime, e.g. execjs) |
47
+ | `TREK_BUILDPACK_PRUNE_SOURCEMAPS=1` | Also remove `.map` files from the compiled assets |
48
+
49
+ ## Migrating an existing app
50
+
51
+ Apps deployed with the stock `.buildpacks` (apt + ruby) migrate in three steps:
52
+
53
+ 1. delete `.buildpacks` (re-running `rails g trek:install:scalingo` does it) and check that `.node-version` exists — apps generated with `rails new -j esbuild` have it
54
+ 2. set `BUILDPACK_URL` as above, and map `DATABASE_URL` if the app doesn't read it yet
55
+ 3. deploy — the first build fills the caches, the next ones benefit from them
56
+
57
+ The buildpack's [README](https://github.com/etaminstudio/trek-buildpack#readme) details the cache layout and the `Aptfile` syntax.
@@ -132,3 +132,4 @@ Next steps:
132
132
 
133
133
  - [Customize Trek](/customize) — branding, favicon, components
134
134
  - [Reference manual](/reference/) — generators, models, components and more
135
+ - [Deploying to Scalingo](/deploy) — `trek:install:scalingo`, a few environment variables, and the dedicated trek-buildpack handles the rest
@@ -93,3 +93,26 @@ end
93
93
  ```
94
94
 
95
95
  Like `Trek::Filters`, forgetting to call `apply_scopes!` raises `Trek::Scopes::NotAppliedError` outside production.
96
+
97
+ ## N+1 detection
98
+
99
+ `trek:install:prosopite` (part of `trek:install`) sets up [Prosopite](https://github.com/charkost/prosopite)
100
+ in development and test: `config/initializers/prosopite.rb` mounts its Rack middleware, so **every request** —
101
+ including Trek's own engine controllers — is scanned. A detected N+1 raises `Prosopite::NPlusOneQueriesError`
102
+ in test (so request specs fail) and is logged in development.
103
+
104
+ The generator also adds `spec/requests/admin/n_plus_one_spec.rb`, a smoke request spec that renders the
105
+ dashboard, the pages index and the fragments index as an admin — extend its route table with the app's other
106
+ admin pages (scaffolded indexes and edit pages in particular), with a few records created beforehand, since an
107
+ empty list can't reveal an N+1.
108
+
109
+ Legitimate repeated queries (e.g. one `COUNT` per row) can be exempted with the escape hatch:
110
+
111
+ ```ruby
112
+ Prosopite.pause do
113
+ scopes.index_with { |scope| relation.public_send(scope).count }
114
+ end
115
+ ```
116
+
117
+ Trek's own concerns already do this where needed (`Trek::Scopes#scope_counts`). Guard with
118
+ `if defined?(Prosopite)` in code that must also run in production, where the gem isn't loaded.
@@ -32,7 +32,9 @@ The installer orchestrates dedicated sub-generators, all under the `trek:install
32
32
  | Tooling | esbuild, PostCSS, EditorConfig, Procfiles, Makefile, health check endpoint |
33
33
  | Linting | Standard, ESLint, Stylelint, Prettier, Slim Lint, Lefthook git hooks |
34
34
  | Security | Brakeman, bundler-audit, ruby_audit |
35
+ | N+1 detection | Prosopite (development/test), scanning every request — see [Controllers › N+1 detection](/reference/controllers#n-1-detection) |
35
36
  | CI | GitLab CI configuration |
37
+ | Deployment | Scalingo files (`trek:install:scalingo`, run on demand) — see [Deploying to Scalingo](/deploy) |
36
38
 
37
39
  ## ENV variables read by the installer
38
40
 
@@ -18,6 +18,18 @@ Akin to Rails', Trek's scaffold generates, with a single command:
18
18
 
19
19
  Standard Rails attribute types are supported (`string`, `text`, `integer`, `float`, `decimal`, `boolean`, `date`, `datetime`, `references`…) and mapped to the matching [form helpers](/reference/forms): `text` renders the content editor, `boolean` a switch box, `references` a collection select, and so on.
20
20
 
21
+ ## Namespaced models
22
+
23
+ ```sh
24
+ rails g trek:scaffold Shop::Product name price:decimal
25
+ ```
26
+
27
+ The controller and policy are nested accordingly (`Admin::Shop::ProductsController`, `Admin::Shop::ProductPolicy`), the routes go under `namespace :shop`, and everything else follows the namespace.
28
+
29
+ The generated code references the model as `::Shop::Product`, top-level qualified, and the dashboard menu entry is `::Shop::Product` too. This is deliberate: once `Admin::Shop` exists, a bare `Shop` written anywhere under `module Admin` resolves to that namespace instead of the `Shop` model. Existing admin code that references such a model — an `Admin::ShopsController#model` returning `Shop`, or a `Shop` entry in `Admin::Dashboard::MODELS_IN_MENU` — must be updated to `::Shop` once a scaffold introduces a namespace with the same name (the menu would otherwise silently list the module).
30
+
31
+ Also note that `rails g model Shop::Product` (which the scaffold runs) writes `app/models/shop.rb` as a `module Shop` namespace file; if a `Shop` model already lives there, the generator warns and Rails asks before overwriting it — move the model or answer `n`.
32
+
21
33
  ## Opt-in concerns
22
34
 
23
35
  The generated model ships with commented-out hooks for Trek's most useful [concerns](/reference/), ready to be enabled when needed:
@@ -25,7 +25,15 @@ module Trek
25
25
 
26
26
  # Mailers run outside a request, so in production they need the same host for
27
27
  # link helpers. Development and test keep the host from their environment files.
28
- ActionMailer::Base.default_url_options = Rails.application.routes.default_url_options if Rails.env.production?
28
+ # Deferred with on_load: referencing ActionMailer::Base here would load it
29
+ # before the app's autoloader can resolve app classes, and its load hook
30
+ # constantizes config.action_mailer.delivery_job — NameError at boot when
31
+ # the app configures a custom delivery job.
32
+ if Rails.env.production?
33
+ ActiveSupport.on_load(:action_mailer) do
34
+ self.default_url_options = Rails.application.routes.default_url_options
35
+ end
36
+ end
29
37
  RUBY
30
38
  end
31
39
  end
@@ -28,6 +28,8 @@ module Trek
28
28
  end
29
29
 
30
30
  def inject_postmark_to_config
31
+ return if File.read("config/environments/production.rb").include?("delivery_method = :postmark")
32
+
31
33
  inject_into_file "config/environments/production.rb",
32
34
  config_injection,
33
35
  after: %r{# Specify outgoing SMTP server\. Remember to add smtp/\* credentials via (?:bin/)?rails credentials:edit\.\n}
@@ -0,0 +1,56 @@
1
+ require "rails/generators/actions"
2
+ require "generators/trek/helpers"
3
+
4
+ module Trek
5
+ module Generators
6
+ module Install
7
+ class ProsopiteGenerator < Rails::Generators::Base
8
+ include Trek::Generators::Helpers
9
+
10
+ desc "Install Prosopite (N+1 query detection)."
11
+
12
+ source_root File.expand_path("../templates", __dir__)
13
+
14
+ def add_gems
15
+ return if in_gemfile?("prosopite")
16
+
17
+ gem "prosopite", group: [:development, :test],
18
+ comment: "N+1 query detection (raises in test, logs in development) [https://github.com/charkost/prosopite]"
19
+ gem "pg_query", group: [:development, :test]
20
+ bundle_install
21
+ end
22
+
23
+ def create_initializer
24
+ return if File.exist?("config/initializers/prosopite.rb")
25
+
26
+ create_file "config/initializers/prosopite.rb" do
27
+ <<~RUBY
28
+ # N+1 query detection with Prosopite, in development and test only (the gem is not loaded in production).
29
+ # Every request is scanned by the Rack middleware, which also covers Trek's engine controllers.
30
+ # Detected N+1s raise Prosopite::NPlusOneQueriesError in test and are logged in development.
31
+ # Legitimate repeated queries can be wrapped in `Prosopite.pause { … }`.
32
+ if defined?(Prosopite)
33
+ require "prosopite/middleware/rack"
34
+
35
+ Rails.application.configure do
36
+ config.middleware.use(Prosopite::Middleware::Rack)
37
+ end
38
+
39
+ Rails.application.config.after_initialize do
40
+ Prosopite.rails_logger = true
41
+ Prosopite.raise = Rails.env.test?
42
+ end
43
+ end
44
+ RUBY
45
+ end
46
+ end
47
+
48
+ def create_smoke_spec
49
+ return if File.exist?("spec/requests/admin/n_plus_one_spec.rb")
50
+
51
+ copy_file "spec/requests/admin/n_plus_one_spec.rb", "spec/requests/admin/n_plus_one_spec.rb"
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -5,17 +5,16 @@ module Trek
5
5
  module Generators
6
6
  module Install
7
7
  class ScalingoGenerator < Rails::Generators::Base
8
+ BUILDPACK_URL = "https://github.com/etaminstudio/trek-buildpack#v0.1.0".freeze
9
+
8
10
  include Trek::Generators::Helpers
9
11
 
10
12
  desc "Create Scalingo files."
11
13
 
12
- def create_buildpacks
13
- create_file ".buildpacks" do
14
- <<~TEXT
15
- https://github.com/Scalingo/apt-buildpack.git
16
- https://github.com/Scalingo/ruby-buildpack.git
17
- TEXT
18
- end
14
+ # The trek-buildpack replaces the apt + ruby buildpack combination. It
15
+ # pins Node from the .node-version that `rails new` already generates.
16
+ def remove_buildpacks
17
+ remove_file ".buildpacks"
19
18
  end
20
19
 
21
20
  def create_aptfile
@@ -37,6 +36,19 @@ module Trek
37
36
  TEXT
38
37
  end
39
38
  end
39
+
40
+ def final_instructions
41
+ say ""
42
+ say "Configure the Scalingo app environment:"
43
+ say ""
44
+ say " scalingo --app <app> env-set BUILDPACK_URL=#{BUILDPACK_URL}"
45
+ say " scalingo --app <app> env-set 'DATABASE_URL=$SCALINGO_POSTGRESQL_URL'"
46
+ say ""
47
+ say "and make sure config/database.yml reads DATABASE_URL in production."
48
+ say ""
49
+ say "See https://trek.etamin.studio/deploy for details."
50
+ say ""
51
+ end
40
52
  end
41
53
  end
42
54
  end
@@ -5,7 +5,7 @@ module Trek
5
5
  module Generators
6
6
  module Install
7
7
  class YarnGenerator < Rails::Generators::Base
8
- YARN_VERSION = "4.17.0"
8
+ YARN_VERSION = "4.18.0"
9
9
 
10
10
  include Trek::Generators::Helpers
11
11
 
@@ -25,6 +25,7 @@ module Trek
25
25
  generate "trek:install:postcss"
26
26
  generate "trek:install:prettier"
27
27
  generate "trek:install:procfiles"
28
+ generate "trek:install:prosopite"
28
29
  generate "trek:install:rubocop"
29
30
  generate "trek:install:slim_lint"
30
31
  generate "trek:install:stylelint"
@@ -15,6 +15,18 @@ module Trek
15
15
 
16
16
  source_root File.expand_path("templates", __dir__)
17
17
 
18
+ # `rails g model Shop::Product` writes app/models/shop.rb as a `module Shop`
19
+ # namespace file; if a `Shop` model already lives there, it gets replaced.
20
+ def warn_about_shadowed_models
21
+ model_namespaces.each do |ns|
22
+ path = "app/models/#{ns}.rb"
23
+ next unless File.exist?(path) && File.read(path).match?(/^class #{ns.camelize}\b/)
24
+
25
+ say_status :warning, "#{path} defines the #{ns.camelize} model, which the #{ns.camelize} namespace " \
26
+ "would replace — answer 'n' below to keep it, or move the model first", :red
27
+ end
28
+ end
29
+
18
30
  def create_model
19
31
  generate :model, class_name, attributes.map { |a| "#{a.name}:#{a.type}" }
20
32
  end
@@ -32,7 +44,8 @@ module Trek
32
44
 
33
45
  def create_controller
34
46
  template "controllers/admin/scaffold_controller.rb",
35
- "app/controllers/admin/#{namespaced_plural}_controller.rb"
47
+ controller_file_path
48
+ reindent controller_file_path
36
49
  end
37
50
 
38
51
  def create_views
@@ -42,9 +55,11 @@ module Trek
42
55
  end
43
56
  end
44
57
 
58
+ # Top-level qualified: once an Admin::Shop namespace exists (scaffolded
59
+ # controller/policy), a bare `Shop` under `module Admin` resolves to it.
45
60
  def add_models_to_menu
46
61
  inject_into_file dashboard_file_path,
47
- "\n#{class_name},",
62
+ "\n::#{class_name},",
48
63
  after: "MODELS_IN_MENU = ["
49
64
  run "rubocop --autocorrect #{dashboard_file_path}", abort_on_failure: true
50
65
  end
@@ -67,7 +82,8 @@ module Trek
67
82
 
68
83
  def add_policy
69
84
  template "policies/admin/scaffold_policy.rb",
70
- "app/policies/admin/#{file_path}_policy.rb"
85
+ policy_file_path
86
+ reindent policy_file_path
71
87
  end
72
88
 
73
89
  def add_default_locale_files
@@ -114,6 +130,22 @@ module Trek
114
130
  "app/models/admin/dashboard.rb"
115
131
  end
116
132
 
133
+ # The templates wrap the class in one `module` per namespace segment
134
+ # without re-indenting the body; the indentation cops are forced with
135
+ # --only because rubocop-rails-omakase leaves them disabled.
136
+ def reindent(path)
137
+ run "rubocop --autocorrect --only Layout/IndentationWidth,Layout/IndentationConsistency #{path}",
138
+ abort_on_failure: true
139
+ end
140
+
141
+ def controller_file_path
142
+ "app/controllers/admin/#{namespaced_plural}_controller.rb"
143
+ end
144
+
145
+ def policy_file_path
146
+ "app/policies/admin/#{file_path}_policy.rb"
147
+ end
148
+
117
149
  def model_injection
118
150
  <<~RUBY
119
151
  # TODO: uncomment to format some attributes, or remove if not needed
@@ -160,12 +192,12 @@ module Trek
160
192
  "config/locales/admin/#{file_path}.#{locale}.yml"
161
193
  end
162
194
 
163
- # Returns namespace segments from file_path (e.g., ["missive"] for "missive/list")
195
+ # Returns namespace segments from file_path (e.g., ["shop"] for "shop/product")
164
196
  def model_namespaces
165
197
  file_path.split("/")[0...-1]
166
198
  end
167
199
 
168
- # Returns the pluralized path preserving namespaces (e.g., "missive/lists" for "missive/list")
200
+ # Returns the pluralized path preserving namespaces (e.g., "shop/products" for "shop/product")
169
201
  def namespaced_plural
170
202
  parts = file_path.split("/")
171
203
  parts[-1] = parts[-1].pluralize
@@ -2,13 +2,13 @@ module Admin
2
2
  <% model_namespaces.each do |ns| -%>
3
3
  module <%= ns.camelize %>
4
4
  <% end -%>
5
- class <%= controller_class_name %>Controller < Trek::ResourceController
5
+ class <%= controller_class_name.demodulize %>Controller < Trek::ResourceController
6
6
  # include Trek::Pagination
7
7
 
8
8
  private
9
9
 
10
10
  def model
11
- <%= class_name %>
11
+ ::<%= class_name %>
12
12
  end
13
13
  end
14
14
  <% model_namespaces.each do |_ns| -%>
@@ -2,10 +2,11 @@ en:
2
2
  activerecord:
3
3
  models:
4
4
  <%= file_path %>:
5
- gender: 'n'
5
+ gender: "n"
6
6
  one: <%= human_name %>
7
7
  other: <%= plural_name.humanize %>
8
8
  attributes:
9
- <%= file_path %>:<%- attributes.each do |attr| %>
9
+ <%= file_path %>:
10
+ <%- attributes.each do |attr| -%>
10
11
  <%= attr.name %>: <%= attr.name.humanize -%>
11
- <% end %>
12
+ <%- end -%>
@@ -0,0 +1,28 @@
1
+ require "rails_helper"
2
+
3
+ # Smoke test for Prosopite (config/initializers/prosopite.rb): each admin page below is rendered
4
+ # as an admin, and any N+1 query raises Prosopite::NPlusOneQueriesError.
5
+ #
6
+ # TODO: add the other admin routes (index and edit pages of scaffolded models in particular), and
7
+ # create a few records first so the per-row queries have something to loop over — an empty list
8
+ # can't reveal an N+1.
9
+ RSpec.describe "Admin pages N+1 queries", type: :request do
10
+ let(:password) { "secret-password-123" }
11
+ let(:admin) { User.create!(email: "admin@example.com", password:, role: "admin") }
12
+
13
+ before do
14
+ post admin_user_session_path, params: { email: admin.email, password: }
15
+ end
16
+
17
+ {
18
+ "dashboard" => -> { admin_root_path },
19
+ "pages index" => -> { admin_pages_path },
20
+ "fragments index" => -> { admin_fragments_path }
21
+ }.each do |page, path|
22
+ it "renders the #{page} without N+1 queries" do
23
+ get instance_exec(&path)
24
+
25
+ expect(response).to have_http_status(:ok)
26
+ end
27
+ end
28
+ end
@@ -3,8 +3,8 @@
3
3
  = render Trek::TabsComponent.new do |c|
4
4
  - c.with_item(id: :content, label: t("admin.content"))
5
5
  = render Trek::PropertiesComponent.new(model:) do |p|
6
- <% attributes.each do |attribute| -%>
6
+ <% attributes.each_with_index do |attribute, i| -%>
7
+ <%= "\n" unless i.zero? -%>
7
8
  = p.with_item :<%= attribute.column_name %>
8
9
  = @object.<%= attribute.column_name %>
9
-
10
10
  <% end -%>
@@ -1,5 +1,6 @@
1
1
  require "rails/generators/actions"
2
2
  require "generators/trek/helpers"
3
+ require "generators/trek/install/yarn_generator"
3
4
  require "yaml"
4
5
 
5
6
  module Trek
@@ -66,10 +67,28 @@ module Trek
66
67
  "and the etaminstudio scope manually.", :yellow
67
68
  end
68
69
 
70
+ def upgrade_yarn
71
+ return if yarn_major_version >= 4
72
+
73
+ # The npmPreapprovedPackages setting below requires Yarn 4 (Yarn 3
74
+ # hard-errors on it), so apply the 0.1.26 upgrade note as part of the
75
+ # migration: bump Yarn and drop the interactive-tools plugin, whose
76
+ # upgrade-interactive command is built into Yarn 4.
77
+ if File.exist?(".yarnrc.yml") && File.read(".yarnrc.yml").include?("plugin-interactive-tools")
78
+ run "yarn plugin remove @yarnpkg/plugin-interactive-tools"
79
+ end
80
+ run "yarn set version #{Install::YarnGenerator::YARN_VERSION}", abort_on_failure: true
81
+ @yarn_version = nil
82
+ end
83
+
69
84
  def preapprove_npm_package
70
85
  return unless File.exist?(".yarnrc.yml")
71
86
  return if File.read(".yarnrc.yml").include?("npmPreapprovedPackages")
72
87
 
88
+ # Defensive: never write the (Yarn 4-only) setting under Yarn 3,
89
+ # where it is a hard error that breaks every yarn command.
90
+ return if yarn_major_version < 4
91
+
73
92
  append_to_file ".yarnrc.yml", <<~YAML
74
93
 
75
94
  # Trek is our own package — exempt it from Yarn's package gates
@@ -98,6 +117,18 @@ module Trek
98
117
  * Revoke this project's Trek deploy token on GitLab.
99
118
  SUMMARY
100
119
  end
120
+
121
+ private
122
+
123
+ def yarn_version
124
+ @yarn_version ||= `yarn --version`.strip
125
+ rescue Errno::ENOENT
126
+ @yarn_version = "0"
127
+ end
128
+
129
+ def yarn_major_version
130
+ yarn_version.to_i
131
+ end
101
132
  end
102
133
  end
103
134
  end
data/lib/trek/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Trek
4
- VERSION = "2.0.1"
4
+ VERSION = "2.1.0"
5
5
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trek",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "A modern content management & back-office system for Ruby on Rails",
5
5
  "homepage": "https://trek.etamin.studio",
6
6
  "license": "MIT",
@@ -459,4 +459,3 @@
459
459
  .ts-dropdown .optgroup:first-child {
460
460
  border-top: 0 none;
461
461
  }
462
- /*# sourceMappingURL=tom-select.default.css.map */
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trek
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mohamed Bengrich
@@ -434,6 +434,7 @@ files:
434
434
  - ".eslintrc.json"
435
435
  - ".node-version"
436
436
  - ".npmignore"
437
+ - ".prettierignore"
437
438
  - ".rspec"
438
439
  - ".rubocop.yml"
439
440
  - ".slim-lint.yml"
@@ -737,6 +738,7 @@ files:
737
738
  - docs/.vitepress/theme/index.js
738
739
  - docs/.vitepress/theme/style.css
739
740
  - docs/customize.md
741
+ - docs/deploy.md
740
742
  - docs/getting-started.md
741
743
  - docs/index.md
742
744
  - docs/public/logo.svg
@@ -821,6 +823,7 @@ files:
821
823
  - lib/generators/trek/install/postmark_generator.rb
822
824
  - lib/generators/trek/install/prettier_generator.rb
823
825
  - lib/generators/trek/install/procfiles_generator.rb
826
+ - lib/generators/trek/install/prosopite_generator.rb
824
827
  - lib/generators/trek/install/readme_generator.rb
825
828
  - lib/generators/trek/install/rubocop_generator.rb
826
829
  - lib/generators/trek/install/ruby_generator.rb
@@ -902,6 +905,7 @@ files:
902
905
  - lib/generators/trek/templates/policies/page.rb
903
906
  - lib/generators/trek/templates/policies/preview.rb
904
907
  - lib/generators/trek/templates/policies/user.rb
908
+ - lib/generators/trek/templates/spec/requests/admin/n_plus_one_spec.rb
905
909
  - lib/generators/trek/templates/views/admin/dashboards/index.html.slim
906
910
  - lib/generators/trek/templates/views/admin/fragments/_form.html.slim
907
911
  - lib/generators/trek/templates/views/admin/fragments/_fragment.json.jb