postnhost 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -0
- data/README.md +22 -9
- data/Rakefile +28 -0
- data/app/assets/builds/postnhost/application.css +1 -1
- data/app/assets/stylesheets/application.tailwind.css +5 -1
- data/app/controllers/postnhost/api/base_controller.rb +29 -0
- data/app/controllers/postnhost/api/v1/commands_controller.rb +27 -0
- data/app/controllers/postnhost/api/v1/mcp_controller.rb +49 -0
- data/app/controllers/postnhost/api_tokens_controller.rb +34 -0
- data/app/controllers/postnhost/articles/translations_controller.rb +2 -9
- data/app/controllers/postnhost/categories/translations_controller.rb +2 -9
- data/app/controllers/postnhost/management_preview_controller.rb +17 -0
- data/app/controllers/postnhost/pages/translations_controller.rb +2 -9
- data/app/helpers/postnhost/application_helper.rb +0 -11
- data/app/models/postnhost/api_token.rb +26 -0
- data/app/models/postnhost/user.rb +2 -0
- data/app/services/postnhost/api/v1/openapi.rb +27 -0
- data/app/services/postnhost/management/catalog.rb +151 -0
- data/app/services/postnhost/management/command.rb +174 -0
- data/app/services/postnhost/management/content_actions.rb +75 -0
- data/app/services/postnhost/management/input.rb +49 -0
- data/app/services/postnhost/management/mcp.rb +54 -0
- data/app/services/postnhost/management/preview.rb +25 -0
- data/app/services/postnhost/management/resources.rb +30 -0
- data/app/services/postnhost/management/schema_fields.rb +25 -0
- data/app/services/postnhost/management/serializer.rb +40 -0
- data/app/services/postnhost/management/settings_actions.rb +64 -0
- data/app/services/postnhost/management/upload.rb +18 -0
- data/app/services/postnhost/translation/enqueue.rb +59 -0
- data/app/views/layouts/postnhost/_sidebar.html.erb +7 -0
- data/app/views/layouts/postnhost/application.html.erb +2 -2
- data/app/views/layouts/postnhost/articles.html.erb +3 -3
- data/app/views/layouts/postnhost/product.html.erb +3 -3
- data/app/views/layouts/postnhost/public/templates/default.html.erb +2 -2
- data/app/views/layouts/postnhost/public/templates/swiss-editorial.html.erb +2 -2
- data/app/views/layouts/postnhost/public/templates/workspace-journal.html.erb +2 -2
- data/app/views/postnhost/api_tokens/index.html.erb +92 -0
- data/app/views/postnhost/onboarding/_post_install_checklist.html.erb +8 -8
- data/app/views/postnhost/pages/index.html.erb +1 -1
- data/app/views/postnhost/templates/edit.html.erb +1 -1
- data/config/routes.rb +12 -0
- data/db/migrate/20261008120000_create_api_tokens.rb +12 -0
- data/lib/generators/postnhost/install/install_generator.rb +11 -12
- data/lib/generators/postnhost/locale/locale_generator.rb +1 -1
- data/lib/generators/postnhost/static_pages/static_pages_generator.rb +1 -1
- data/lib/generators/postnhost/tailwindcss/install_generator.rb +28 -25
- data/lib/generators/postnhost/tailwindcss/templates/host.tailwind.css +0 -3
- data/lib/generators/postnhost/user/user_generator.rb +1 -1
- data/lib/generators/postnhost/views/views_generator.rb +5 -7
- data/lib/postnhost/css_build.rb +68 -0
- data/lib/postnhost/css_scope.rb +77 -0
- data/lib/postnhost/sample_data.rb +1 -1
- data/lib/postnhost/tailwind_compiler.rb +75 -0
- data/lib/postnhost/tailwind_input.rb +32 -0
- data/lib/postnhost/version.rb +1 -1
- data/lib/tasks/postnhost_tasks.rake +40 -65
- metadata +40 -2
- data/lib/generators/postnhost/tailwindcss/templates/tailwind.config.js +0 -8
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
<p class="text-sm leading-6 text-gray-600">If your changes add new Tailwind classes, install and run the optional host Tailwind pipeline:</p>
|
|
56
56
|
<div class="space-y-1">
|
|
57
57
|
<code class="block overflow-x-auto rounded-lg bg-gray-900 px-3 py-2 text-xs leading-5 text-white">bin/rails g postnhost:tailwindcss:install</code>
|
|
58
|
-
<code class="block overflow-x-auto rounded-lg bg-gray-900 px-3 py-2 text-xs leading-5 text-white">
|
|
58
|
+
<code class="block overflow-x-auto rounded-lg bg-gray-900 px-3 py-2 text-xs leading-5 text-white">bin/rails postnhost:tailwindcss:watch</code>
|
|
59
59
|
</div>
|
|
60
60
|
</div>
|
|
61
61
|
</div>
|
data/config/routes.rb
CHANGED
|
@@ -33,6 +33,7 @@ Postnhost::Engine.routes.draw do
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
# Admin routes
|
|
36
|
+
resources :api_tokens, only: %i[index create destroy]
|
|
36
37
|
resource :onboarding, only: %i[show update], controller: "onboarding"
|
|
37
38
|
resource :session, only: %i[new create destroy], controller: "sessions"
|
|
38
39
|
resources :users, only: %i[index new create edit update destroy] do
|
|
@@ -106,6 +107,17 @@ Postnhost::Engine.routes.draw do
|
|
|
106
107
|
resources :site_scripts, only: %i[create update destroy], controller: "settings/site_scripts"
|
|
107
108
|
end
|
|
108
109
|
|
|
110
|
+
# Management API and MCP
|
|
111
|
+
namespace :api do
|
|
112
|
+
namespace :v1 do
|
|
113
|
+
get "openapi", to: "commands#schema", as: :openapi
|
|
114
|
+
get "commands", to: "commands#index", as: :commands
|
|
115
|
+
post "commands/:command", to: "commands#create", as: :command
|
|
116
|
+
post "mcp", to: "mcp#create", as: :mcp
|
|
117
|
+
match "mcp", to: "mcp#unsupported", via: %i[get delete]
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
109
121
|
# Public static pages from host app views:
|
|
110
122
|
# app/views/postnhost/static_pages/*.html.erb
|
|
111
123
|
get "/:slug", to: "public/static_pages#show", as: :public_static_page
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class CreateApiTokens < ActiveRecord::Migration[7.2]
|
|
2
|
+
def change
|
|
3
|
+
create_table :postnhost_api_tokens do |t|
|
|
4
|
+
t.references :user, null: false, foreign_key: { to_table: :postnhost_users }
|
|
5
|
+
t.string :name, null: false
|
|
6
|
+
t.string :token_digest, null: false
|
|
7
|
+
t.datetime :revoked_at
|
|
8
|
+
t.timestamps
|
|
9
|
+
end
|
|
10
|
+
add_index :postnhost_api_tokens, :token_digest, unique: true
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -5,7 +5,7 @@ module Postnhost
|
|
|
5
5
|
|
|
6
6
|
source_root File.expand_path("templates", __dir__)
|
|
7
7
|
|
|
8
|
-
desc "Creates
|
|
8
|
+
desc "Creates the PostnHost initializer and mounts the engine"
|
|
9
9
|
|
|
10
10
|
def create_initializer
|
|
11
11
|
template "postnhost.rb", "config/initializers/postnhost.rb"
|
|
@@ -24,7 +24,7 @@ module Postnhost
|
|
|
24
24
|
def show_post_install
|
|
25
25
|
say ""
|
|
26
26
|
say "=" * 60
|
|
27
|
-
say "
|
|
27
|
+
say "PostnHost has been installed!", :green
|
|
28
28
|
say "=" * 60
|
|
29
29
|
say ""
|
|
30
30
|
say "Next steps:"
|
|
@@ -32,23 +32,22 @@ module Postnhost
|
|
|
32
32
|
say " bin/rails postnhost:install:migrations"
|
|
33
33
|
say " bin/rails db:migrate"
|
|
34
34
|
say ""
|
|
35
|
-
say " 2. Open first-time setup in
|
|
36
|
-
say "
|
|
35
|
+
say " 2. Open first-time setup in your app:"
|
|
36
|
+
say " #{DEFAULT_MOUNT_PATH}/onboarding"
|
|
37
37
|
say " Or create a CMS user interactively: bin/rails g postnhost:user"
|
|
38
38
|
say ""
|
|
39
|
-
say " 3.
|
|
39
|
+
say " 3. Review config/initializers/postnhost.rb for optional defaults, production uploads, and AI translations."
|
|
40
40
|
say ""
|
|
41
41
|
say " 4. (Optional) Customize public views:"
|
|
42
|
-
say " rails g postnhost:views --views-scope=minimal"
|
|
43
|
-
say "
|
|
44
|
-
say " rails g postnhost:views --views-scope=full"
|
|
42
|
+
say " bin/rails g postnhost:views --views-scope=minimal"
|
|
43
|
+
say " or"
|
|
44
|
+
say " bin/rails g postnhost:views --views-scope=full"
|
|
45
45
|
say ""
|
|
46
|
-
say " 5. (Optional) Enable host Tailwind
|
|
47
|
-
say " rails g postnhost:tailwindcss:install"
|
|
48
|
-
say " bundle exec rails postnhost:tailwindcss:watch"
|
|
46
|
+
say " 5. (Optional) Enable host Tailwind support for custom PostnHost classes:"
|
|
47
|
+
say " bin/rails g postnhost:tailwindcss:install"
|
|
49
48
|
say ""
|
|
50
49
|
say " 6. (Optional) Add a new public locale (copy English strings into your app):"
|
|
51
|
-
say " rails g postnhost:locale sv"
|
|
50
|
+
say " bin/rails g postnhost:locale sv"
|
|
52
51
|
say ""
|
|
53
52
|
end
|
|
54
53
|
end
|
|
@@ -5,7 +5,7 @@ module Postnhost
|
|
|
5
5
|
class LocaleGenerator < Rails::Generators::Base
|
|
6
6
|
argument :locale, type: :string, desc: "Locale code to match Language#html_lang (e.g. sv, pt-BR)"
|
|
7
7
|
|
|
8
|
-
desc "Copies
|
|
8
|
+
desc "Copies the PostnHost English locale (en.yml) to config/locales/<locale>.yml in your app for translation."
|
|
9
9
|
|
|
10
10
|
def copy_postnhost_locale
|
|
11
11
|
code = locale.to_s.strip
|
|
@@ -7,7 +7,7 @@ module Postnhost
|
|
|
7
7
|
|
|
8
8
|
def create_static_pages
|
|
9
9
|
if slugs.empty?
|
|
10
|
-
say("Please provide at least one page slug, e.g. rails g postnhost:static_pages terms privacy", :red)
|
|
10
|
+
say("Please provide at least one page slug, e.g. bin/rails g postnhost:static_pages terms privacy", :red)
|
|
11
11
|
return
|
|
12
12
|
end
|
|
13
13
|
|
|
@@ -7,19 +7,12 @@ module Postnhost
|
|
|
7
7
|
source_root File.expand_path("templates", __dir__)
|
|
8
8
|
|
|
9
9
|
namespace "postnhost:tailwindcss:install"
|
|
10
|
-
desc "Install host Tailwind
|
|
10
|
+
desc "Install host Tailwind support for PostnHost view overrides."
|
|
11
11
|
|
|
12
12
|
def create_stylesheet_directory
|
|
13
13
|
empty_directory "app/assets/stylesheets/postnhost"
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def create_tailwind_config
|
|
17
|
-
path = Rails.root.join("postnhost.tailwind.config.js")
|
|
18
|
-
return if path.exist?
|
|
19
|
-
|
|
20
|
-
copy_file "tailwind.config.js", path
|
|
21
|
-
end
|
|
22
|
-
|
|
23
16
|
def create_host_stylesheet
|
|
24
17
|
path = Rails.root.join("app/assets/stylesheets/postnhost/host.tailwind.css")
|
|
25
18
|
return if path.exist?
|
|
@@ -31,12 +24,14 @@ module Postnhost
|
|
|
31
24
|
path = Rails.root.join("package.json")
|
|
32
25
|
return say_package_json_instructions unless path.exist?
|
|
33
26
|
|
|
27
|
+
@package_json_present = true
|
|
28
|
+
|
|
34
29
|
json_data = JSON.parse(File.read(path))
|
|
35
30
|
json_data["scripts"] ||= {}
|
|
36
31
|
json_data["devDependencies"] ||= {}
|
|
37
32
|
|
|
38
|
-
json_data["scripts"]["postnhost:tailwindcss"] =
|
|
39
|
-
json_data["scripts"]["postnhost:tailwindcss:watch"] = "
|
|
33
|
+
json_data["scripts"]["postnhost:tailwindcss"] = "bundle exec rails postnhost:tailwindcss:build"
|
|
34
|
+
json_data["scripts"]["postnhost:tailwindcss:watch"] = "bundle exec rails postnhost:tailwindcss:watch"
|
|
40
35
|
|
|
41
36
|
ensure_dependency(json_data, "tailwindcss", "^4.1.12")
|
|
42
37
|
ensure_dependency(json_data, "@tailwindcss/cli", "^4.1.12")
|
|
@@ -44,8 +39,7 @@ module Postnhost
|
|
|
44
39
|
|
|
45
40
|
File.write(path, "#{JSON.pretty_generate(json_data)}\n")
|
|
46
41
|
|
|
47
|
-
say "Updated package.json
|
|
48
|
-
say " yarn postnhost:tailwindcss:watch"
|
|
42
|
+
say "Updated package.json with PostnHost Tailwind scripts and dependencies.", :green
|
|
49
43
|
end
|
|
50
44
|
|
|
51
45
|
def wire_procfile_dev
|
|
@@ -54,32 +48,41 @@ module Postnhost
|
|
|
54
48
|
|
|
55
49
|
if path.exist?
|
|
56
50
|
if File.read(path).include?("postnhost:tailwindcss:watch")
|
|
57
|
-
say "Procfile.dev already includes
|
|
51
|
+
say "Procfile.dev already includes the PostnHost watcher.", :green
|
|
58
52
|
else
|
|
59
53
|
append_to_file path, "\n#{entry}\n"
|
|
60
|
-
say "Added
|
|
54
|
+
say "Added the PostnHost watcher to Procfile.dev.", :green
|
|
61
55
|
end
|
|
62
56
|
else
|
|
63
57
|
create_file "Procfile.dev", "#{entry}\n"
|
|
64
|
-
say "Created Procfile.dev with
|
|
58
|
+
say "Created Procfile.dev with the PostnHost watcher.", :green
|
|
65
59
|
end
|
|
66
60
|
end
|
|
67
61
|
|
|
68
62
|
def print_next_steps
|
|
69
63
|
say ""
|
|
70
|
-
say "
|
|
64
|
+
say "PostnHost host Tailwind support installed.", :green
|
|
71
65
|
say "Next steps:"
|
|
72
|
-
|
|
73
|
-
|
|
66
|
+
step = 1
|
|
67
|
+
|
|
68
|
+
if @package_json_present
|
|
69
|
+
say " #{step}. Install dependencies with the JavaScript package manager used by this app."
|
|
70
|
+
step += 1
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if Rails.root.join("bin/dev").exist?
|
|
74
|
+
say " #{step}. Run bin/dev; the PostnHost watcher is wired in Procfile.dev."
|
|
75
|
+
else
|
|
76
|
+
say " #{step}. Run bin/rails postnhost:tailwindcss:watch alongside your Rails server."
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
say " #{step + 1}. Use normal Tailwind classes in copied PostnHost views."
|
|
80
|
+
say " #{step + 2}. Keep the default layout tags; the combined build overrides postnhost/application automatically."
|
|
74
81
|
say ""
|
|
75
82
|
end
|
|
76
83
|
|
|
77
84
|
private
|
|
78
85
|
|
|
79
|
-
def tailwind_build_command
|
|
80
|
-
"./node_modules/.bin/tailwindcss -i ./app/assets/stylesheets/postnhost/host.tailwind.css -o ./app/assets/builds/postnhost/host.css -c ./postnhost.tailwind.config.js --minify"
|
|
81
|
-
end
|
|
82
|
-
|
|
83
86
|
def ensure_dependency(json_data, package_name, version)
|
|
84
87
|
return if json_data["dependencies"]&.key?(package_name) || json_data["devDependencies"].key?(package_name)
|
|
85
88
|
|
|
@@ -87,9 +90,9 @@ module Postnhost
|
|
|
87
90
|
end
|
|
88
91
|
|
|
89
92
|
def say_package_json_instructions
|
|
90
|
-
say "package.json not found. Add
|
|
91
|
-
say
|
|
92
|
-
say "
|
|
93
|
+
say "package.json not found. Add a Tailwind CSS 4 compiler to the host Gemfile:", :yellow
|
|
94
|
+
say ' gem "tailwindcss-ruby", "~> 4.0"'
|
|
95
|
+
say "Then run bundle install."
|
|
93
96
|
end
|
|
94
97
|
end
|
|
95
98
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module Postnhost
|
|
2
2
|
module Generators
|
|
3
3
|
class ViewsGenerator < Rails::Generators::Base
|
|
4
|
-
desc "Copies public
|
|
4
|
+
desc "Copies public PostnHost views to your application for customization"
|
|
5
5
|
class_option :views_scope,
|
|
6
6
|
type: :string,
|
|
7
7
|
required: true,
|
|
@@ -28,7 +28,7 @@ module Postnhost
|
|
|
28
28
|
copy_file "layouts/postnhost/public_header/_dashboard_sign_in.html.erb", "app/views/layouts/postnhost/public_header/_dashboard_sign_in.html.erb"
|
|
29
29
|
say ""
|
|
30
30
|
warn_if_host_tailwind_not_installed
|
|
31
|
-
say "
|
|
31
|
+
say views_scope == "full" ? "All public templates and layouts were copied." : "The minimal public view set was copied.", :green
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def engine_views_path
|
|
@@ -123,15 +123,13 @@ module Postnhost
|
|
|
123
123
|
def warn_if_host_tailwind_not_installed
|
|
124
124
|
return if host_tailwind_enabled?
|
|
125
125
|
|
|
126
|
-
say "Tip: if you add new Tailwind classes in copied views, install host
|
|
127
|
-
say " rails g postnhost:tailwindcss:install"
|
|
128
|
-
say " bundle exec rails postnhost:tailwindcss:watch"
|
|
126
|
+
say "Tip: if you add new Tailwind classes in copied views, install host Tailwind support:", :yellow
|
|
127
|
+
say " bin/rails g postnhost:tailwindcss:install"
|
|
129
128
|
say ""
|
|
130
129
|
end
|
|
131
130
|
|
|
132
131
|
def host_tailwind_enabled?
|
|
133
|
-
Rails.root.join("postnhost.tailwind.
|
|
134
|
-
Rails.root.join("app/assets/stylesheets/postnhost/host.tailwind.css").exist?
|
|
132
|
+
Rails.root.join("app/assets/stylesheets/postnhost/host.tailwind.css").exist?
|
|
135
133
|
end
|
|
136
134
|
|
|
137
135
|
def source_paths
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
require "English"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
|
|
4
|
+
require_relative "css_scope"
|
|
5
|
+
|
|
6
|
+
module Postnhost
|
|
7
|
+
class CssBuild
|
|
8
|
+
POLL_INTERVAL = 0.1
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def run(command:, unscoped_path:, output_path:, watch: false)
|
|
12
|
+
FileUtils.rm_f(unscoped_path)
|
|
13
|
+
|
|
14
|
+
if watch
|
|
15
|
+
watch(command:, unscoped_path:, output_path:)
|
|
16
|
+
else
|
|
17
|
+
system(*command, exception: true)
|
|
18
|
+
CssScope.scope_file(unscoped_path, output_path)
|
|
19
|
+
end
|
|
20
|
+
ensure
|
|
21
|
+
FileUtils.rm_f(unscoped_path)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def watch(command:, unscoped_path:, output_path:)
|
|
27
|
+
process_id = Process.spawn(*command)
|
|
28
|
+
applied_signature = nil
|
|
29
|
+
pending_signature = nil
|
|
30
|
+
|
|
31
|
+
loop do
|
|
32
|
+
process_status = Process.waitpid(process_id, Process::WNOHANG)
|
|
33
|
+
signature = file_signature(unscoped_path)
|
|
34
|
+
|
|
35
|
+
if signature && signature == pending_signature && signature != applied_signature
|
|
36
|
+
CssScope.scope_file(unscoped_path, output_path)
|
|
37
|
+
applied_signature = signature
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
pending_signature = signature
|
|
41
|
+
break if process_status
|
|
42
|
+
|
|
43
|
+
sleep POLL_INTERVAL
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
raise "Tailwind CSS watcher exited unsuccessfully" unless $CHILD_STATUS.success?
|
|
47
|
+
ensure
|
|
48
|
+
terminate(process_id)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def file_signature(path)
|
|
52
|
+
return unless File.exist?(path)
|
|
53
|
+
|
|
54
|
+
stat = File.stat(path)
|
|
55
|
+
[stat.mtime.to_f, stat.size]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def terminate(process_id)
|
|
59
|
+
return unless process_id
|
|
60
|
+
|
|
61
|
+
Process.kill("TERM", process_id)
|
|
62
|
+
Process.wait(process_id)
|
|
63
|
+
rescue Errno::ESRCH, Errno::ECHILD
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
require "crass"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
|
|
4
|
+
module Postnhost
|
|
5
|
+
class CssScope
|
|
6
|
+
ROOT_SELECTOR = "html[data-postnhost]"
|
|
7
|
+
ROOT_OR_DESCENDANT_SELECTOR = ":is(#{ROOT_SELECTOR}, #{ROOT_SELECTOR} *)"
|
|
8
|
+
GROUP_AT_RULES = %w[container layer media starting-style supports].freeze
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def call(css)
|
|
12
|
+
rules = Crass::Parser.parse_stylesheet(css, preserve_comments: true)
|
|
13
|
+
scope_rules(rules)
|
|
14
|
+
Crass::Parser.stringify(rules)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def scope_file(input_path, output_path = input_path)
|
|
18
|
+
output_path = File.expand_path(output_path)
|
|
19
|
+
FileUtils.mkdir_p(File.dirname(output_path))
|
|
20
|
+
temporary_path = "#{output_path}.tmp"
|
|
21
|
+
File.write(temporary_path, call(File.read(input_path)))
|
|
22
|
+
FileUtils.mv(temporary_path, output_path)
|
|
23
|
+
ensure
|
|
24
|
+
FileUtils.rm_f(temporary_path) if temporary_path
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def scope_rules(rules)
|
|
30
|
+
rules.each do |rule|
|
|
31
|
+
case rule[:node]
|
|
32
|
+
when :style_rule
|
|
33
|
+
scope_style_rule(rule)
|
|
34
|
+
when :at_rule
|
|
35
|
+
scope_at_rule(rule)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def scope_at_rule(rule)
|
|
41
|
+
return unless rule[:block] && GROUP_AT_RULES.include?(rule[:name])
|
|
42
|
+
|
|
43
|
+
nested_rules = Crass::Parser.parse_rules(rule[:block], preserve_comments: true)
|
|
44
|
+
scope_rules(nested_rules)
|
|
45
|
+
rule[:block] = nested_rules
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def scope_style_rule(rule)
|
|
49
|
+
selectors = split_selectors(rule.dig(:selector, :tokens)).map { |selector| scope_selector(selector) }.uniq
|
|
50
|
+
scoped_rule = Crass::Parser.parse_rules("#{selectors.join(',')}{}").first
|
|
51
|
+
rule[:selector] = scoped_rule[:selector]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def split_selectors(tokens)
|
|
55
|
+
selectors = tokens.each_with_object([[]]) do |token, groups|
|
|
56
|
+
if token[:node] == :comma
|
|
57
|
+
groups << []
|
|
58
|
+
else
|
|
59
|
+
groups.last << token
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
selectors.map { |selector_tokens| Crass::Parser.stringify(selector_tokens).strip }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def scope_selector(selector)
|
|
67
|
+
return ROOT_SELECTOR if selector.match?(/\A(?::root|:host)\z/)
|
|
68
|
+
return selector.sub(/\A:root/, ROOT_SELECTOR) if selector.match?(/\A:root(?=$|[\s.#\[:>+~])/)
|
|
69
|
+
return selector.sub(/\Ahtml/, ROOT_SELECTOR) if selector.match?(/\Ahtml(?=$|[\s.#\[:>+~])/)
|
|
70
|
+
return selector.sub(/\A\*/, ROOT_OR_DESCENDANT_SELECTOR) if selector.start_with?("*")
|
|
71
|
+
return "#{ROOT_OR_DESCENDANT_SELECTOR}#{selector}" if selector.match?(/\A[.#\[:]/)
|
|
72
|
+
|
|
73
|
+
"#{ROOT_SELECTOR} #{selector}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -785,7 +785,7 @@ module Postnhost
|
|
|
785
785
|
<li>⏰ <strong>Post scheduling</strong> — Schedule publication through your application’s Active Job adapter</li>
|
|
786
786
|
<li>🌐 <strong>Localized SEO</strong> — Localized routes, language switcher, sitemap, and hreflang tags</li>
|
|
787
787
|
<li>⚙️ <strong>Settings</strong> — Update key copy and assets from the CMS</li>
|
|
788
|
-
<li>🎨 <strong>Customizable views</strong> — Override public templates in the host app (<code>rails g postnhost:views</code>)</li>
|
|
788
|
+
<li>🎨 <strong>Customizable views</strong> — Override public templates in the host app (<code>bin/rails g postnhost:views --views-scope=minimal</code>)</li>
|
|
789
789
|
<li>⚡ <strong>Hotwire UI</strong> — Turbo Frames/Streams and Stimulus</li>
|
|
790
790
|
<li>🔐 <strong>Session-based auth</strong> — Built-in admin authentication</li>
|
|
791
791
|
</ul>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "pathname"
|
|
3
|
+
require "rubygems/version"
|
|
4
|
+
|
|
5
|
+
module Postnhost
|
|
6
|
+
class TailwindCompiler
|
|
7
|
+
REQUIRED_MAJOR = 4
|
|
8
|
+
|
|
9
|
+
class MissingCompiler < StandardError; end
|
|
10
|
+
class IncompatibleVersion < StandardError; end
|
|
11
|
+
|
|
12
|
+
def initialize(root:, loaded_specs: Gem.loaded_specs)
|
|
13
|
+
@root = Pathname.new(root)
|
|
14
|
+
@loaded_specs = loaded_specs
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def command(input_path:, output_path:, watch: false)
|
|
18
|
+
command = npm_command(input_path:, output_path:) || ruby_command(input_path:, output_path:)
|
|
19
|
+
raise MissingCompiler, missing_compiler_message unless command
|
|
20
|
+
|
|
21
|
+
command << "--watch" if watch
|
|
22
|
+
command
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
attr_reader :root, :loaded_specs
|
|
28
|
+
|
|
29
|
+
def ruby_command(input_path:, output_path:)
|
|
30
|
+
spec = loaded_specs["tailwindcss-ruby"]
|
|
31
|
+
return unless spec
|
|
32
|
+
|
|
33
|
+
validate_version!(spec.version, "tailwindcss-ruby")
|
|
34
|
+
require "tailwindcss/ruby"
|
|
35
|
+
executable = Tailwindcss::Ruby.executable
|
|
36
|
+
return if executable.to_s.empty?
|
|
37
|
+
|
|
38
|
+
[executable, "-i", input_path.to_s, "-o", output_path.to_s, "--minify"]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def npm_command(input_path:, output_path:)
|
|
42
|
+
executable = root.join("node_modules/.bin/tailwindcss")
|
|
43
|
+
return unless executable.file?
|
|
44
|
+
|
|
45
|
+
validate_package_version!("tailwindcss")
|
|
46
|
+
validate_package_version!("@tailwindcss/cli")
|
|
47
|
+
|
|
48
|
+
[executable.to_s, "-i", input_path.to_s, "-o", output_path.to_s, "--minify"]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def validate_package_version!(package_name)
|
|
52
|
+
package_path = root.join("node_modules", package_name, "package.json")
|
|
53
|
+
raise MissingCompiler, "[postnhost] Missing #{package_name}. Install Tailwind CSS 4 and @tailwindcss/cli 4." unless package_path.file?
|
|
54
|
+
|
|
55
|
+
version = JSON.parse(package_path.read).fetch("version")
|
|
56
|
+
validate_version!(version, package_name)
|
|
57
|
+
rescue JSON::ParserError, KeyError
|
|
58
|
+
raise MissingCompiler, "[postnhost] Cannot read the installed #{package_name} version."
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def validate_version!(version, source)
|
|
62
|
+
return if Gem::Version.new(version.to_s).segments.first == REQUIRED_MAJOR
|
|
63
|
+
|
|
64
|
+
raise IncompatibleVersion,
|
|
65
|
+
"[postnhost] Host CSS requires Tailwind CSS 4; found #{source} #{version}."
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def missing_compiler_message
|
|
69
|
+
<<~MESSAGE.strip
|
|
70
|
+
[postnhost] No Tailwind CSS 4 compiler found for host customization.
|
|
71
|
+
Add tailwindcss-ruby 4, or install tailwindcss 4 and @tailwindcss/cli 4.
|
|
72
|
+
MESSAGE
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require "pathname"
|
|
2
|
+
|
|
3
|
+
module Postnhost
|
|
4
|
+
class TailwindInput
|
|
5
|
+
SOURCE_PATTERN = /@source\s+"([^"]+)";/
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def build(engine_source:, host_source:)
|
|
9
|
+
[
|
|
10
|
+
expand_sources(engine_source),
|
|
11
|
+
expand_sources(host_source)
|
|
12
|
+
].join("\n\n")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def expand_sources(path)
|
|
18
|
+
source_path = Pathname.new(path)
|
|
19
|
+
|
|
20
|
+
source_path.read.gsub(SOURCE_PATTERN) do
|
|
21
|
+
relative_source = Regexp.last_match(1)
|
|
22
|
+
absolute_source = source_path.dirname.join(relative_source).expand_path
|
|
23
|
+
%(@source "#{css_path(absolute_source)}";)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def css_path(path)
|
|
28
|
+
Pathname.new(path).expand_path.to_s.tr("\\", "/").gsub('"', '\\"')
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/postnhost/version.rb
CHANGED