diff --git a/.agents/bin/README.md b/.agents/bin/README.md index 0885878..1bc277e 100644 --- a/.agents/bin/README.md +++ b/.agents/bin/README.md @@ -11,7 +11,7 @@ means that capability is n/a here. | `validate` | Pre-push gate | JavaScript and Tailwind CSS builds, production Shakapacker compilation, then `bin/rails test` | | `test` | Run tests | `bin/rails test` | | `lint` | Lint / format | n/a | -| `build` | Build / type-check | `bin/yarn build`, `bin/yarn build:css`, then `RAILS_ENV=production bin/shakapacker` | +| `build` | Build / type-check | JavaScript and Tailwind builds, React on Rails pack generation, then production Shakapacker compilation | | `docs` | Validate documentation | n/a | | `ci-detect` | Detect CI impact | n/a | diff --git a/.agents/bin/build b/.agents/bin/build index 463aa91..4d89345 100755 --- a/.agents/bin/build +++ b/.agents/bin/build @@ -3,5 +3,6 @@ set -euo pipefail cd "$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)" bin/yarn build -bin/yarn build:css +bin/rails tailwindcss:build +bin/shakapacker-precompile-hook exec env RAILS_ENV=production bin/shakapacker "$@" diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b86c026 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +.git +.github +.agents +.bundle +.env +.env.* +config/master.key +config/credentials/*.key +db/*.sqlite3 +log/* +storage/* +tmp/* +node_modules +vendor/bundle +app/assets/builds/* +public/assets +public/packs +public/packs-test +coverage diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..1cf8253 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.4.6 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d72bad3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,97 @@ +ARG RUBY_VERSION=3.4.6 + +# Node and Ruby base images share the same Debian release so that glibc, libssl, and +# other system libraries line up between stages. +FROM docker.io/library/node:22-bookworm-slim AS node +FROM ruby:$RUBY_VERSION-slim-bookworm + +# Yarn Classic / pnpm fallbacks for projects that haven't adopted corepack via +# `packageManager` in package.json. Override at build time (`--build-arg=YARN_CLASSIC_VERSION=...`) +# rather than editing this file so regenerating the Dockerfile keeps your pin. +ARG YARN_CLASSIC_VERSION=1.22.22 +ARG PNPM_FALLBACK_VERSION=9.12.3 + +WORKDIR /app + +# Keep Node.js available both for asset compilation and for SSR runtimes that +# rely on ExecJS in production. Narrowed to just what the node stage actually +# ships under /usr/local so we don't drag in unused Debian libs from that image. +COPY --from=node /usr/local/bin/node /usr/local/bin/node +COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules +COPY --from=node /usr/local/include/node /usr/local/include/node + +RUN ln -sf ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && \ + ln -sf ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx && \ + ln -sf ../lib/node_modules/corepack/dist/corepack.js /usr/local/bin/corepack && \ + chmod +x /usr/local/lib/node_modules/npm/bin/npm-cli.js \ + /usr/local/lib/node_modules/npm/bin/npx-cli.js \ + /usr/local/lib/node_modules/corepack/dist/corepack.js && \ + node --version && npm --version && corepack --version + +# Expose Corepack-managed shims so later build steps can call yarn/pnpm +# directly during asset precompilation hooks. +RUN printf '%s\n' '#!/bin/sh' 'exec corepack yarn "$@"' > /usr/bin/yarn && \ + chmod +x /usr/bin/yarn && \ + printf '%s\n' '#!/bin/sh' 'exec corepack pnpm "$@"' > /usr/bin/pnpm && \ + chmod +x /usr/bin/pnpm + +# install ruby gems +COPY Gemfile* ./ + +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential && \ + bundle config set without 'development test' && \ + bundle config set with 'production' && \ + bundle install --jobs=3 --retry=3 && \ + apt-get purge -y --auto-remove build-essential && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +COPY . ./ + +# Install JavaScript dependencies only when the project actually has them. +# Pin `packageManager` in package.json to take the corepack path and avoid the +# YARN_CLASSIC_VERSION / PNPM_FALLBACK_VERSION fallbacks below; the fallbacks exist for +# projects that haven't adopted corepack and should be reviewed periodically as they go stale. +RUN if [ -f package.json ]; then \ + package_manager="$(node -p "require('./package.json').packageManager || ''")"; \ + if [ -f yarn.lock ]; then \ + if printf '%s' "$package_manager" | grep -q '^yarn@'; then \ + corepack prepare "$package_manager" --activate && \ + (corepack yarn install --immutable || corepack yarn install --frozen-lockfile); \ + else \ + npm install -g "yarn@${YARN_CLASSIC_VERSION}" && \ + (yarn install --immutable || yarn install --frozen-lockfile); \ + fi; \ + elif [ -f pnpm-lock.yaml ]; then \ + if printf '%s' "$package_manager" | grep -q '^pnpm@'; then \ + corepack prepare "$package_manager" --activate && \ + corepack pnpm install --frozen-lockfile; \ + else \ + corepack prepare "pnpm@${PNPM_FALLBACK_VERSION}" --activate && \ + corepack pnpm install --frozen-lockfile; \ + fi; \ + elif [ -f package-lock.json ]; then \ + npm ci; \ + else \ + npm install; \ + fi; \ + fi + +ENV RAILS_ENV=production +ENV RAILS_SERVE_STATIC_FILES=true + +# Compiling assets requires any non-blank value, but the placeholder must not +# remain in the final image environment. +RUN SECRET_KEY_BASE=NOT_USED_NON_BLANK bin/shakapacker-precompile-hook + + +RUN SECRET_KEY_BASE=NOT_USED_NON_BLANK rails assets:precompile + +# add entrypoint +COPY bin/docker-entrypoint /app/entrypoint.sh +RUN chmod +x /app/entrypoint.sh +ARG GIT_COMMIT=unknown +ENV GIT_COMMIT=$GIT_COMMIT +ENTRYPOINT ["/app/entrypoint.sh"] + +CMD ["rails", "s"] diff --git a/Gemfile b/Gemfile index ed96902..64ded9b 100644 --- a/Gemfile +++ b/Gemfile @@ -13,6 +13,8 @@ gem "mutex_m" # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] gem "sprockets-rails" +# Sprockets 4.2 still passes JSON options removed in JSON 3. +gem "json", "< 3" # Use sqlite3 as the database for Active Record gem "sqlite3", "~> 2.9" @@ -27,7 +29,7 @@ gem "jbuilder" gem "mini_racer", "~> 0.22.1" # The library which provides react helpers and more in rails -gem "react_on_rails", "17.2.0.rc.0" +gem "react_on_rails", "17.2.0" # Modern javscript compressor gem "terser" diff --git a/Gemfile.lock b/Gemfile.lock index 8f434aa..4671667 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -122,7 +122,7 @@ GEM sorbet-runtime jsbundling-rails (1.3.1) railties (>= 6.0.0) - json (3.0.2) + json (2.21.2) libv8-node (24.12.0.1-arm64-darwin) libv8-node (24.12.0.1-x86_64-linux) logger (1.7.0) @@ -226,7 +226,7 @@ GEM prism (>= 1.6.0) rbs (>= 4.0.0) tsort - react_on_rails (17.2.0.rc.0) + react_on_rails (17.2.0) addressable connection_pool execjs (~> 2.5) @@ -309,13 +309,14 @@ DEPENDENCIES jbuilder js-routes jsbundling-rails (~> 1.3) + json (< 3) mini_racer (~> 0.22.1) minitest (< 7) mutex_m pry-remote puma (~> 8.0) rails (~> 8.1.4) - react_on_rails (= 17.2.0.rc.0) + react_on_rails (= 17.2.0) selenium-webdriver shakapacker (= 10.3.2) sprockets-rails diff --git a/README.md b/README.md index 01d2fdf..ea259af 100644 --- a/README.md +++ b/README.md @@ -23,3 +23,18 @@ This repository is a migrated version of [ganchdev/react-rails-example](https:// ## Notes - This app is intentionally minimal and focused on migration outcomes. - Generated React on Rails files are kept in-repo so users can inspect exact changes after migration. + +## Temporary Verification + +Run `bin/verify-container` to build the production image and check server-rendered +HTML on `/`, `/posts/0`, and `/hello_world`. Docker must be running. The command +uses a random signing key, an ephemeral SQLite database, and a random localhost +port. It removes its container and image tag on success, failure, or interruption. +No cloud app, persistent volume, registry upload, or permanent endpoint is created. + +The image prepares SQLite automatically only for direct `rails server` / `rails s` +commands, including `bin/rails`. Pass runtime overrides with Docker's `--env` +flags. Custom wrapped commands must prepare their database explicitly. + +For interactive browser checks, use `bin/dev` and stop it afterward. This repository +has no deployment workflows, automatic deployments, or permanent staging app. diff --git a/app/javascript/src/Navbar/ror_components/Navbar.client.jsx b/app/javascript/src/Navbar/ror_components/Navbar.client.jsx new file mode 100644 index 0000000..c7eb39e --- /dev/null +++ b/app/javascript/src/Navbar/ror_components/Navbar.client.jsx @@ -0,0 +1,3 @@ +import Navbar from '../../components/Navbar'; + +export default Navbar; diff --git a/app/javascript/src/Navbar/ror_components/Navbar.server.jsx b/app/javascript/src/Navbar/ror_components/Navbar.server.jsx new file mode 100644 index 0000000..f91e1a2 --- /dev/null +++ b/app/javascript/src/Navbar/ror_components/Navbar.server.jsx @@ -0,0 +1,3 @@ +import Navbar from './Navbar.client'; + +export default Navbar; diff --git a/app/javascript/src/PostsIndex/ror_components/PostsIndex.client.jsx b/app/javascript/src/PostsIndex/ror_components/PostsIndex.client.jsx new file mode 100644 index 0000000..3e9ce9a --- /dev/null +++ b/app/javascript/src/PostsIndex/ror_components/PostsIndex.client.jsx @@ -0,0 +1,3 @@ +import PostsIndex from '../../pages/PostsIndex'; + +export default PostsIndex; diff --git a/app/javascript/src/PostsIndex/ror_components/PostsIndex.server.jsx b/app/javascript/src/PostsIndex/ror_components/PostsIndex.server.jsx new file mode 100644 index 0000000..729577b --- /dev/null +++ b/app/javascript/src/PostsIndex/ror_components/PostsIndex.server.jsx @@ -0,0 +1,3 @@ +import PostsIndex from './PostsIndex.client'; + +export default PostsIndex; diff --git a/app/javascript/src/PostsShow/ror_components/PostsShow.client.jsx b/app/javascript/src/PostsShow/ror_components/PostsShow.client.jsx new file mode 100644 index 0000000..164bb24 --- /dev/null +++ b/app/javascript/src/PostsShow/ror_components/PostsShow.client.jsx @@ -0,0 +1,3 @@ +import PostsShow from '../../pages/PostsShow'; + +export default PostsShow; diff --git a/app/javascript/src/PostsShow/ror_components/PostsShow.server.jsx b/app/javascript/src/PostsShow/ror_components/PostsShow.server.jsx new file mode 100644 index 0000000..f342d72 --- /dev/null +++ b/app/javascript/src/PostsShow/ror_components/PostsShow.server.jsx @@ -0,0 +1,3 @@ +import PostsShow from './PostsShow.client'; + +export default PostsShow; diff --git a/app/javascript/src/components/Navbar.jsx b/app/javascript/src/components/Navbar.jsx index 8615b56..19241b9 100644 --- a/app/javascript/src/components/Navbar.jsx +++ b/app/javascript/src/components/Navbar.jsx @@ -6,9 +6,10 @@ import Card from "./Card"; /** * @param {String} menus * @param {String} homeUrl + * @param {Number} avatarSeed * @return {JSX.Element} */ -function Navbar({ menus, homeUrl }) { +function Navbar({ menus, homeUrl, avatarSeed = 0 }) { return (
@@ -23,7 +24,7 @@ function Navbar({ menus, homeUrl }) {
    @@ -42,9 +43,10 @@ function Navbar({ menus, homeUrl }) { ); } -Card.propTypes = { +Navbar.propTypes = { menus: PropTypes.string, homeUrl: PropTypes.string, -} + avatarSeed: PropTypes.number, +}; export default Navbar; diff --git a/app/javascript/src/pages/PostsShow.jsx b/app/javascript/src/pages/PostsShow.jsx index b284691..2d082a0 100644 --- a/app/javascript/src/pages/PostsShow.jsx +++ b/app/javascript/src/pages/PostsShow.jsx @@ -8,7 +8,7 @@ import PropTypes from "prop-types"; function PostsShow({ post }) { const parsedPost = JSON.parse(post); - return( + return (

    {parsedPost.title}

    {parsedPost.content}

    @@ -18,6 +18,6 @@ function PostsShow({ post }) { PostsShow.propTypes = { post: PropTypes.string -} +}; export default PostsShow; diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 988f871..cfef32b 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -6,18 +6,17 @@ <%= csrf_meta_tags %> <%= csp_meta_tag %> - <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %> + <%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %> <%= stylesheet_link_tag "application" %> <%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %> - <%= javascript_pack_tag "application" %> <%= react_component( "Navbar", - { - menus: JSON.generate([{name: "Posts", href: root_path}]), + props: { + menus: JSON.generate([{ name: "Posts", href: root_path }]), homeUrl: root_path }, prerender: true) @@ -26,5 +25,6 @@
    <%= yield %>
    + <%= javascript_pack_tag %> diff --git a/app/views/posts/index.html.erb b/app/views/posts/index.html.erb index 64c3c87..6132b7e 100644 --- a/app/views/posts/index.html.erb +++ b/app/views/posts/index.html.erb @@ -1,3 +1,3 @@ <%= - react_component("PostsIndex", { posts: @posts }, prerender: true) + react_component("PostsIndex", props: { posts: @posts }, prerender: true) %> diff --git a/app/views/posts/show.html.erb b/app/views/posts/show.html.erb index 06b4410..d47efcf 100644 --- a/app/views/posts/show.html.erb +++ b/app/views/posts/show.html.erb @@ -1,3 +1,3 @@ <%= - react_component("PostsShow", {post: @post}, prerender: true) + react_component("PostsShow", props: { post: @post }, prerender: true) %> diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint new file mode 100755 index 0000000..1b6e87a --- /dev/null +++ b/bin/docker-entrypoint @@ -0,0 +1,13 @@ +#!/bin/sh +set -e + +# Only direct Rails server commands prepare the temporary database automatically. +# Pass runtime overrides with docker run --env; wrapped commands prepare explicitly. +case "${1:-}:${2:-}" in + rails:s|rails:server|bin/rails:s|bin/rails:server|./bin/rails:s|./bin/rails:server) + echo " -- Preparing database" + ./bin/rails db:prepare + ;; +esac + +exec "$@" diff --git a/bin/verify-container b/bin/verify-container new file mode 100755 index 0000000..ea87859 --- /dev/null +++ b/bin/verify-container @@ -0,0 +1,64 @@ +#!/usr/bin/env ruby +# Build and exercise a production image without provisioning a hosted app. +require 'json' +require 'net/http' +require 'open3' +require 'securerandom' + +Dir.chdir(File.expand_path('..', __dir__)) +Signal.trap('TERM') { raise Interrupt } +name = "rormig-verification-#{Process.pid}-#{SecureRandom.hex(4)}" +image = "#{name}:local" + +def run!(*command) + abort "Command failed: #{command.first(2).join(' ')}" unless system(*command) +end + +def response_for(url) + uri = URI(url) + Net::HTTP.start(uri.host, uri.port, open_timeout: 2, read_timeout: 5) do |http| + http.get(uri.request_uri) + end +end + +begin + head, status = Open3.capture2('git', 'rev-parse', 'HEAD') + abort 'Cannot determine the verification revision' unless status.success? + run!('docker', 'build', '--platform', 'linux/amd64', '--build-arg', "GIT_COMMIT=#{head.strip}", '--tag', image, '.') + env = { 'SECRET_KEY_BASE' => SecureRandom.hex(64) } + output, error, status = Open3.capture3(env, 'docker', 'run', '--detach', '--rm', '--name', name, + '--publish', '127.0.0.1::3000', '--env', 'SECRET_KEY_BASE', + '--env', 'DATABASE_PATH=/tmp/verification.sqlite3', image, 'bin/rails', 'server', '-b', '0.0.0.0') + abort "Container startup failed: #{error}" unless status.success? + port, status = Open3.capture2('docker', 'port', name, '3000/tcp') + abort 'Cannot determine the localhost port' unless status.success? + base = "http://#{port.strip}" + deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 120 + loop do + begin + response = response_for("#{base}/hello_world") + break if response.code == '200' + rescue IOError, SystemCallError, Net::OpenTimeout, Net::ReadTimeout + # Rails may still be preparing SQLite and starting Puma. + end + abort 'Container did not become healthy within 120 seconds' if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline + sleep 1 + end + checked_assets = [] + { '/' => 'Tail beef', '/posts/0' => 'Tail beef', '/hello_world' => 'Hello, Stranger!' }.each do |path, heading| + response = response_for("#{base}#{path}") + headings = response.body.scan(/]*>(.*?)<\/h[1-3]>/m).flatten.map { |html| html.gsub(/<[^>]*>/, '') } + abort "SSR check failed for #{path}" unless response.code == '200' && headings.any? { |text| text.include?(heading) } && response.body.include?('data-component-name=') + response.body.scan(/<(?:script|link)\b[^>]*(?:src|href)="([^"]+)"/i).flatten.uniq.each do |asset| + next unless asset.start_with?('/') && !asset.start_with?('//') + next if checked_assets.include?(asset) + abort "Asset check failed for #{asset}" unless response_for("#{base}#{asset}").code == '200' + checked_assets << asset + end + puts "PASS #{path}: HTTP 200, server HTML and reachable assets" + end + puts "Verified working-tree image (base revision #{head.strip}); localhost-only container #{name}" +ensure + system('docker', 'rm', '--force', name, out: File::NULL, err: File::NULL) + system('docker', 'image', 'rm', image, out: File::NULL, err: File::NULL) +end diff --git a/config/database.yml b/config/database.yml index fcba57f..a10ab4a 100644 --- a/config/database.yml +++ b/config/database.yml @@ -22,4 +22,4 @@ test: production: <<: *default - database: db/production.sqlite3 + database: <%= ENV.fetch("DATABASE_PATH", "db/production.sqlite3") %> diff --git a/config/tailwind.config.js b/config/tailwind.config.js new file mode 100644 index 0000000..2d459e9 --- /dev/null +++ b/config/tailwind.config.js @@ -0,0 +1,18 @@ +module.exports = { + content: [ + "./app/helpers/**/*.rb", + "./app/javascript/**/*.{js,jsx,ts,tsx}", + "./app/views/**/*.{erb,haml,html,slim}", + "./config/initializers/**/*.rb" + ], + theme: { + extend: {} + }, + plugins: [ + require("@tailwindcss/aspect-ratio"), + require("@tailwindcss/container-queries"), + require("@tailwindcss/forms"), + require("@tailwindcss/typography"), + require("daisyui") + ] +}; diff --git a/config/webpack/serverWebpackConfig.js b/config/webpack/serverWebpackConfig.js index 77aa114..f780b90 100644 --- a/config/webpack/serverWebpackConfig.js +++ b/config/webpack/serverWebpackConfig.js @@ -53,6 +53,38 @@ const configureServer = () => { minimize: false, }; serverWebpackConfig.plugins.unshift(new bundler.optimize.LimitChunkCountPlugin({ maxChunks: 1 })); + serverWebpackConfig.plugins.unshift( + new bundler.BannerPlugin({ + banner: ` +if (typeof globalThis.TextEncoder === 'undefined') { + globalThis.TextEncoder = class TextEncoder { + encode(input = '') { + // TextEncoder replaces unpaired UTF-16 surrogates instead of throwing. + const encoded = unescape(encodeURIComponent(String(input).toWellFormed())); + return Uint8Array.from(encoded, (character) => character.charCodeAt(0)); + } + }; +} + +if (typeof globalThis.MessageChannel === 'undefined') { + globalThis.MessageChannel = class MessageChannel { + constructor() { + const port1 = { onmessage: null }; + const port2 = { + postMessage: (data) => { + Promise.resolve().then(() => port1.onmessage?.({ data })); + }, + }; + this.port1 = port1; + this.port2 = port2; + } + }; +} +`, + entryOnly: true, + raw: true, + }), + ); // Custom output for the server-bundle // Using Shakapacker 9.0+ privateOutputPath for automatic sync with shakapacker.yml @@ -140,11 +172,6 @@ const configureServer = () => { // The default of cheap-module-source-map is slow and provides poor info. serverWebpackConfig.devtool = 'eval'; - // If using the default 'web', then libraries like Emotion and loadable-components - // break with SSR. The fix is to use a node renderer and change the target. - // If using the React on Rails Pro node server renderer, uncomment the next line - // serverWebpackConfig.target = 'node' - return serverWebpackConfig; }; diff --git a/package.json b/package.json index f5c9c31..e6736e0 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "react": "19.3.0", "react-dom": "19.3.0", "react-jazzicon": "^1.0.4", - "react-on-rails": "17.2.0-rc.0", + "react-on-rails": "17.2.0", "react_ujs": "^3.3.1", "sass-loader": "^17.0.1", "shakapacker": "10.3.2", diff --git a/test/integration/rendering_test.rb b/test/integration/rendering_test.rb new file mode 100644 index 0000000..37c1172 --- /dev/null +++ b/test/integration/rendering_test.rb @@ -0,0 +1,41 @@ +require "test_helper" + +class RenderingTest < ActionDispatch::IntegrationTest + test "posts index renders its navigation and cards on the server" do + get root_path + + assert_response :success + assert_select "a", text: "Home" + assert_select "h2", text: "Tail beef" + assert_select 'script[data-component-name="Navbar"]' + assert_select 'script[data-component-name="PostsIndex"]' + assert_select 'script[src*="generated/Navbar"]' + assert_select 'script[src*="generated/PostsIndex"]' + end + + test "post details render on the server" do + get post_path(0) + + assert_response :success + assert_select "h1", text: "Tail beef" + assert_select 'script[data-component-name="PostsShow"]' + assert_select 'script[src*="generated/Navbar"]' + assert_select 'script[src*="generated/PostsShow"]' + end + + test "hello world renders with the MiniRacer runtime" do + get hello_world_path + + assert_response :success + assert_select "h3", text: "Hello, Stranger!" + assert_select 'input#name[value="Stranger"]' + assert_select 'script[data-component-name="HelloWorld"]' + end + + test "server text encoding replaces lone surrogates and retains Unicode" do + context = ReactOnRails::ServerRenderingPool.pool.create_js_context + + assert_equal [239, 191, 189], context.eval(%q{Array.from(new TextEncoder().encode("\uD83D"))}) + assert_equal [195, 169, 240, 159, 152, 128], context.eval(%q{Array.from(new TextEncoder().encode("é😀"))}) + end +end diff --git a/test/lib/docker_entrypoint_test.rb b/test/lib/docker_entrypoint_test.rb new file mode 100644 index 0000000..d37398b --- /dev/null +++ b/test/lib/docker_entrypoint_test.rb @@ -0,0 +1,43 @@ +require "test_helper" +require "tmpdir" +require "fileutils" +require "open3" + +class DockerEntrypointTest < ActiveSupport::TestCase + test "direct server commands prepare with the inherited runtime environment" do + assert_equal ["custom.sqlite3|production||db:prepare", "custom.sqlite3|production||server"], + probe("bin/rails", "server") + end + + test "wrapped commands keep native env behavior and own their preparation" do + assert_equal ["override.sqlite3|production|1|server"], + probe("env", "DATABASE_PATH=override.sqlite3", "SOME.FLAG=1", "bin/rails", "server") + end + + test "non-server commands skip database preparation" do + assert_equal ["custom.sqlite3|production||runner"], probe("bin/rails", "runner") + end + + private + + def probe(*command) + Dir.mktmpdir("rormig-entrypoint-") do |directory| + FileUtils.mkdir_p(File.join(directory, "bin")) + rails = File.join(directory, "bin/rails") + log = File.join(directory, "commands.log") + File.write(rails, <<~'SCRIPT') + #!/usr/bin/env ruby + File.open(ENV.fetch("ENTRYPOINT_PROBE"), "a") do |file| + file.puts [ENV["DATABASE_PATH"], ENV["RAILS_ENV"], ENV["SOME.FLAG"], ARGV.join(" ")].join("|") + end + SCRIPT + FileUtils.chmod(0o755, rails) + _output, error, status = Open3.capture3( + { "DATABASE_PATH" => "custom.sqlite3", "RAILS_ENV" => "production", "ENTRYPOINT_PROBE" => log }, + Rails.root.join("bin/docker-entrypoint").to_s, *command, chdir: directory + ) + assert status.success?, error + File.readlines(log, chomp: true) + end + end +end diff --git a/yarn.lock b/yarn.lock index 607e8b6..feddcda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2786,10 +2786,10 @@ react-jazzicon@^1.0.4: dependencies: mersenne-twister "^1.1.0" -react-on-rails@17.2.0-rc.0: - version "17.2.0-rc.0" - resolved "https://registry.npmjs.org/react-on-rails/-/react-on-rails-17.2.0-rc.0.tgz#32ab8f2c521a02dffe162d9c1957969da3bd2d21" - integrity sha512-Ew810LOL2gRiwZEaCjwvslShJX+/Ec7fZvGGX8L+VO0UMxCcoXy6sfAy8GfGoAjyHS7x/2lt59eCIYjPc2l53w== +react-on-rails@17.2.0: + version "17.2.0" + resolved "https://registry.npmjs.org/react-on-rails/-/react-on-rails-17.2.0.tgz#6dfb1f9b9eb2c8912c5b5d2d06230b484d80cd3a" + integrity sha512-t6vOTT7fnyIB4VtW3chl3b1DcmQVR01sNhg1nevTol0hySweWCdigAOCfFyTDyGgSiXYD2ZGrZUSKGz2oVOm3Q== react-refresh@0.19.0: version "0.19.0"