diff --git a/.controlplane/Dockerfile b/.controlplane/Dockerfile index 9a955122..c18eb985 100644 --- a/.controlplane/Dockerfile +++ b/.controlplane/Dockerfile @@ -72,7 +72,8 @@ RUN SECRET_KEY_BASE=precompile_placeholder bin/rails react_on_rails:locale # and /app/client/app are the client assets that are bundled, so not needed once built # Helps to have smaller images b/c of smaller Docker Layer Caches and smaller final images # SECRET_KEY_BASE is required for asset precompilation but is not persisted in the image -RUN SECRET_KEY_BASE=precompile_placeholder yarn res:build && \ +RUN SECRET_KEY_BASE=precompile_placeholder bundle exec rake react_on_rails:generate_packs && \ + SECRET_KEY_BASE=precompile_placeholder yarn res:build && \ SECRET_KEY_BASE=precompile_placeholder bin/rails assets:precompile && \ rm -rf /app/lib/bs /app/client/app diff --git a/.controlplane/templates/org.yml b/.controlplane/templates/org.yml index 4be7ee03..0f3dd187 100644 --- a/.controlplane/templates/org.yml +++ b/.controlplane/templates/org.yml @@ -1,8 +1,10 @@ -# Org level secrets are used to store sensitive information that is -# shared across multiple apps in the same organization. This is -# useful for storing things like API keys, database credentials, and -# other sensitive information that is shared across multiple apps -# in the same organization. +# App secret dictionaries store sensitive information for apps in the +# organization. This template keeps the cpflow app-secret placeholders +# {{APP_SECRETS}} and {{APP_SECRETS_POLICY}}. +# +# cpflow 5.1.1 shared_secret_grants are only for a separate shared +# org-level dictionary referenced from app/workload templates with +# {{SHARED_SECRET_}}. # The qa-* dictionary is bootstrapped via this template for review apps. # Review apps run pull request code, so values in this dictionary must be @@ -29,7 +31,7 @@ data: --- -# Policy is needed to allow identities to access secrets +# App secret policy grants app identities reveal access to this dictionary. kind: policy name: {{APP_SECRETS_POLICY}} targetKind: secret diff --git a/.github/actions/cpflow-build-docker-image/action.yml b/.github/actions/cpflow-build-docker-image/action.yml new file mode 100644 index 00000000..5b62a554 --- /dev/null +++ b/.github/actions/cpflow-build-docker-image/action.yml @@ -0,0 +1,99 @@ +name: Build Docker Image +description: Builds and pushes the app image for a Control Plane workload + +inputs: + app_name: + description: Name of the application + required: true + org: + description: Control Plane organization name + required: true + commit: + description: Commit SHA to tag the image with + required: true + pr_number: + description: Pull request number for status messaging + required: false + docker_build_extra_args: + description: Optional newline-delimited extra docker build tokens. Use key=value forms like --build-arg=FOO=bar. + required: false + docker_build_ssh_key: + description: Optional private SSH key used for Docker builds that fetch private dependencies with RUN --mount=type=ssh + required: false + docker_build_ssh_known_hosts: + description: Optional SSH known_hosts entries used with docker_build_ssh_key. Defaults to pinned GitHub.com host keys. + required: false + +outputs: + image_tag: + description: Fully qualified image tag + value: ${{ steps.build.outputs.image_tag }} + +runs: + using: composite + steps: + - name: Build Docker image + id: build + shell: bash + env: + APP_NAME: ${{ inputs.app_name }} + COMMIT: ${{ inputs.commit }} + DOCKER_BUILD_EXTRA_ARGS: ${{ inputs.docker_build_extra_args }} + DOCKER_BUILD_SSH_KEY: ${{ inputs.docker_build_ssh_key }} + DOCKER_BUILD_SSH_KNOWN_HOSTS: ${{ inputs.docker_build_ssh_known_hosts }} + ORG: ${{ inputs.org }} + PR_NUMBER: ${{ inputs.pr_number }} + run: | + set -euo pipefail + + PR_INFO="" + docker_build_args=() + + if [[ -n "$PR_NUMBER" ]]; then + PR_INFO=" for PR #${PR_NUMBER}" + fi + + if [[ -n "$DOCKER_BUILD_EXTRA_ARGS" ]]; then + while IFS= read -r arg; do + arg="${arg%$'\r'}" + [[ -n "${arg}" ]] || continue + + if [[ "${arg}" =~ [[:space:]] ]]; then + echo "docker_build_extra_args entries must be single docker-build tokens. " \ + "Use key=value forms like --build-arg=FOO=bar." >&2 + exit 1 + fi + + docker_build_args+=("${arg}") + done <<< "$DOCKER_BUILD_EXTRA_ARGS" + fi + + if [[ -n "$DOCKER_BUILD_SSH_KEY" ]]; then + mkdir -p ~/.ssh + chmod 700 ~/.ssh + + if [[ -n "$DOCKER_BUILD_SSH_KNOWN_HOSTS" ]]; then + printf '%s\n' "$DOCKER_BUILD_SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts + else + cat <<'EOF' > ~/.ssh/known_hosts + github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl + github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg= + github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk= + EOF + fi + + chmod 600 ~/.ssh/known_hosts + + eval "$(ssh-agent -s)" + trap 'ssh-agent -k >/dev/null' EXIT + ssh-add - <<< "$DOCKER_BUILD_SSH_KEY" + unset DOCKER_BUILD_SSH_KEY + docker_build_args+=("--ssh=default") + fi + + echo "🏗️ Building Docker image${PR_INFO} (commit ${COMMIT})..." + cpflow build-image -a "$APP_NAME" --commit="$COMMIT" --org="$ORG" "${docker_build_args[@]}" + + image_tag="${ORG}/${APP_NAME}:${COMMIT}" + echo "image_tag=${image_tag}" >> "$GITHUB_OUTPUT" + echo "✅ Docker image build successful${PR_INFO} (commit ${COMMIT})" diff --git a/.github/actions/cpflow-delete-control-plane-app/action.yml b/.github/actions/cpflow-delete-control-plane-app/action.yml new file mode 100644 index 00000000..63981dd5 --- /dev/null +++ b/.github/actions/cpflow-delete-control-plane-app/action.yml @@ -0,0 +1,24 @@ +name: Delete Control Plane App +description: Deletes a Control Plane app and all associated resources + +inputs: + app_name: + description: Name of the application to delete + required: true + cpln_org: + description: Control Plane organization name + required: true + review_app_prefix: + description: Prefix used for review app names + required: true + +runs: + using: composite + steps: + - name: Delete application + shell: bash + run: ${{ github.action_path }}/delete-app.sh + env: + APP_NAME: ${{ inputs.app_name }} + CPLN_ORG: ${{ inputs.cpln_org }} + REVIEW_APP_PREFIX: ${{ inputs.review_app_prefix }} diff --git a/.github/actions/cpflow-delete-control-plane-app/delete-app.sh b/.github/actions/cpflow-delete-control-plane-app/delete-app.sh new file mode 100755 index 00000000..d1d55ba7 --- /dev/null +++ b/.github/actions/cpflow-delete-control-plane-app/delete-app.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +set -euo pipefail + +: "${APP_NAME:?APP_NAME environment variable is required}" +: "${CPLN_ORG:?CPLN_ORG environment variable is required}" +: "${REVIEW_APP_PREFIX:?REVIEW_APP_PREFIX environment variable is required}" + +expected_prefix="${REVIEW_APP_PREFIX}-" +if [[ "$APP_NAME" != "${expected_prefix}"* ]]; then + echo "❌ ERROR: refusing to delete an app outside the review app prefix" >&2 + echo "App name: $APP_NAME" >&2 + echo "Expected prefix: ${expected_prefix}" >&2 + exit 1 +fi + +echo "🔍 Checking if application exists: $APP_NAME" +exists_output="" +set +e +exists_output="$(cpflow exists -a "$APP_NAME" --org "$CPLN_ORG" 2>&1)" +exists_status=$? +set -e + +case "$exists_status" in + 0) + ;; + 3) + if [[ -n "$exists_output" ]]; then + printf '%s\n' "$exists_output" + fi + + echo "⚠️ Application does not exist: $APP_NAME" + exit 0 + ;; + *) + echo "❌ ERROR: failed to determine whether application exists: $APP_NAME" >&2 + printf '%s\n' "$exists_output" >&2 + exit 1 + ;; +esac + +if [[ -n "$exists_output" ]]; then + printf '%s\n' "$exists_output" +fi + +echo "🗑️ Deleting application: $APP_NAME" +cpflow delete -a "$APP_NAME" --org "$CPLN_ORG" --yes + +echo "✅ Successfully deleted application: $APP_NAME" diff --git a/.github/actions/cpflow-setup-environment/action.yml b/.github/actions/cpflow-setup-environment/action.yml new file mode 100644 index 00000000..0ab65963 --- /dev/null +++ b/.github/actions/cpflow-setup-environment/action.yml @@ -0,0 +1,70 @@ +name: Setup Control Plane Environment +description: Sets up Ruby, installs the Control Plane CLI and cpflow gem, and configures a default profile + +inputs: + token: + description: Control Plane token + required: true + org: + description: Control Plane organization + required: true + ruby_version: + description: Ruby version used for cpflow + required: false + default: "3.4.6" + cpln_cli_version: + description: "@controlplane/cli version" + required: false + default: "3.3.1" + cpflow_version: + description: cpflow gem version + required: false + default: "5.1.1" + +runs: + using: composite + steps: + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ inputs.ruby_version }} + + - name: Install Control Plane CLI and cpflow gem + shell: bash + run: | + set -euo pipefail + + sudo npm install -g @controlplane/cli@${{ inputs.cpln_cli_version }} + cpln --version + + gem install cpflow -v ${{ inputs.cpflow_version }} + cpflow --version + + - name: Setup Control Plane profile and registry login + shell: bash + run: | + set -euo pipefail + + TOKEN="${{ inputs.token }}" + ORG="${{ inputs.org }}" + + if [[ -z "$TOKEN" ]]; then + echo "Error: Control Plane token not provided" >&2 + exit 1 + fi + + if [[ -z "$ORG" ]]; then + echo "Error: Control Plane organization not provided" >&2 + exit 1 + fi + + create_output="" + if ! create_output="$(cpln profile create default --token "$TOKEN" --org "$ORG" 2>&1)"; then + if ! echo "$create_output" | grep -qi "already exists"; then + echo "$create_output" >&2 + exit 1 + fi + fi + + cpln profile update default --org "$ORG" --token "$TOKEN" + cpln image docker-login --org "$ORG" diff --git a/Gemfile.lock b/Gemfile.lock index 04527605..ead984b9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -134,7 +134,7 @@ GEM coffee-script-source (1.12.2) concurrent-ruby (1.3.6) connection_pool (3.0.2) - console (1.35.1) + console (1.36.0) fiber-annotation fiber-local (~> 1.1) json @@ -201,7 +201,7 @@ GEM jbuilder (2.12.0) actionview (>= 5.0.0) activesupport (>= 5.0.0) - json (2.19.5) + json (2.19.8) jwt (3.2.0) base64 language_server-protocol (3.17.0.5) @@ -505,7 +505,7 @@ GEM bindex (>= 0.4.0) railties (>= 6.0.0) websocket (1.2.10) - websocket-driver (0.8.0) + websocket-driver (0.8.1) base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) diff --git a/README.md b/README.md index 084f29f0..986b70fe 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ ShakaCode recently migrated [HiChee.com](https://hichee.com) to Control Plane, resulting in a two-thirds reduction in server hosting costs! -See doc in [./.controlplane/readme.md](./.controlplane/readme.md) for how to easily deploy this app to Control Plane. +See [./.controlplane/readme.md](./.controlplane/readme.md) for local `cpflow` setup plus the shared `cpflow-*` GitHub Actions flow for review apps, automatic staging deploys, and manual promotion to production. The instructions leverage the `cpflow` CLI, with source code and many more tips on how to migrate from Heroku to Control Plane in https://github.com/shakacode/heroku-to-control-plane. diff --git a/client/app/bundles/comments/components/CommentBox/CommentList/CommentList.spec.jsx b/client/app/bundles/comments/components/CommentBox/CommentList/CommentList.spec.jsx index f6aa3121..b1a095b2 100644 --- a/client/app/bundles/comments/components/CommentBox/CommentList/CommentList.spec.jsx +++ b/client/app/bundles/comments/components/CommentBox/CommentList/CommentList.spec.jsx @@ -27,9 +27,7 @@ describe('CommentList', () => { ); it('renders a list of Comments in normal order', () => { - render( - , - ); + render(); // Verify both authors are rendered in order expect(screen.getByText('Frank')).toBeInTheDocument(); diff --git a/client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx b/client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx index cac19b8a..ae242b80 100644 --- a/client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx +++ b/client/app/bundles/comments/components/SimpleCommentScreen/ror_components/SimpleCommentScreen.jsx @@ -1,4 +1,5 @@ /* eslint-disable max-classes-per-file */ + 'use client'; import React from 'react'; diff --git a/client/app/bundles/server-components/components/LiveActivityRefresher.jsx b/client/app/bundles/server-components/components/LiveActivityRefresher.jsx index b2ee2c5b..1be88de3 100644 --- a/client/app/bundles/server-components/components/LiveActivityRefresher.jsx +++ b/client/app/bundles/server-components/components/LiveActivityRefresher.jsx @@ -8,22 +8,22 @@ import { useRSC } from 'react-on-rails-pro/RSCProvider'; // Same shape and dimensions as the rendered LiveActivity card. Local Suspense // fallback prevents the RSCRoute suspension from bubbling to an outer // boundary, which would collapse the whole page during in-flight fetches. -const ActivityCardSkeleton = () => ( -
-
- {['Server Time', 'Free RAM', 'Uptime (hrs)'].map((label) => ( -
-
- {label} +function ActivityCardSkeleton() { + return ( +
+
+ {['Server Time', 'Free RAM', 'Uptime (hrs)'].map((label) => ( +
+
{label}
+
-
-
- ))} + ))} +
-
-); + ); +} -const LiveActivityRefresher = () => { +function LiveActivityRefresher() { const [refreshKey, setRefreshKey] = useState(0); const [simulateError, setSimulateError] = useState(false); const { refetchComponent } = useRSC(); @@ -94,6 +94,6 @@ const LiveActivityRefresher = () => {
); -}; +} export default LiveActivityRefresher; diff --git a/client/app/bundles/server-components/components/ServerInfo.jsx b/client/app/bundles/server-components/components/ServerInfo.jsx index e09fa1d9..b788adea 100644 --- a/client/app/bundles/server-components/components/ServerInfo.jsx +++ b/client/app/bundles/server-components/components/ServerInfo.jsx @@ -33,15 +33,18 @@ function ServerInfo() { return (

- This data comes from the Node.js os module - — it runs only on the server. The lodash library - used to format it never reaches the browser. + This data comes from the Node.js os module — it + runs only on the server. The lodash library used + to format it never reaches the browser.

{grouped.map((group) => (
k).join('-')} className="space-y-1"> {group.map(([key, value]) => ( -
+
{labels[key] || key} {value}
diff --git a/client/app/bundles/server-components/components/TogglePanel.jsx b/client/app/bundles/server-components/components/TogglePanel.jsx index f5a38a9e..3c230e45 100644 --- a/client/app/bundles/server-components/components/TogglePanel.jsx +++ b/client/app/bundles/server-components/components/TogglePanel.jsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; -const TogglePanel = ({ title, children }) => { +function TogglePanel({ title, children }) { const [isOpen, setIsOpen] = useState(false); return ( @@ -22,13 +22,9 @@ const TogglePanel = ({ title, children }) => { - {isOpen && ( -
- {children} -
- )} + {isOpen &&
{children}
}
); -}; +} export default TogglePanel; diff --git a/client/app/bundles/server-components/ror_components/LiveActivity.jsx b/client/app/bundles/server-components/ror_components/LiveActivity.jsx index a76f7de3..9c52a794 100644 --- a/client/app/bundles/server-components/ror_components/LiveActivity.jsx +++ b/client/app/bundles/server-components/ror_components/LiveActivity.jsx @@ -24,21 +24,15 @@ async function LiveActivity({ simulateError = false }) {
-
- Server Time -
+
Server Time
{stats.serverTime}
-
- Free RAM -
+
Free RAM
{stats.freeMemoryMB} MB
-
- Uptime (hrs) -
+
Uptime (hrs)
{stats.uptimeHours}
diff --git a/client/app/bundles/server-components/ror_components/ServerComponentsPage.jsx b/client/app/bundles/server-components/ror_components/ServerComponentsPage.jsx index e6b8df84..1aa636f6 100644 --- a/client/app/bundles/server-components/ror_components/ServerComponentsPage.jsx +++ b/client/app/bundles/server-components/ror_components/ServerComponentsPage.jsx @@ -8,17 +8,15 @@ import CommentsFeed from '../components/CommentsFeed'; import TogglePanel from '../components/TogglePanel'; import LiveActivityRefresher from '../components/LiveActivityRefresher'; -const ServerComponentsPage = ({ comments = [] }) => { +function ServerComponentsPage({ comments = [] }) { return (
-

- React Server Components Demo -

+

React Server Components Demo

- This page is rendered using React Server Components with React on Rails Pro. - Server components run on the server and stream their output to the client, keeping - heavy dependencies out of the browser bundle entirely. + This page is rendered using React Server Components with React on Rails Pro. Server + components run on the server and stream their output to the client, keeping heavy dependencies out + of the browser bundle entirely.

@@ -45,12 +43,14 @@ const ServerComponentsPage = ({ comments = [] }) => {

- This toggle is a 'use client' component, meaning it ships JavaScript - to the browser for interactivity. But the content inside is rendered on the server - and passed as children — a key RSC pattern called the donut pattern. + This toggle is a 'use client' component, meaning it ships JavaScript to + the browser for interactivity. But the content inside is rendered on the server and passed as + children — a key RSC pattern called the donut pattern.

    -
  • The TogglePanel wrapper runs on the client (handles click events)
  • +
  • + The TogglePanel wrapper runs on the client (handles click events) +
  • The children content is rendered on the server (no JS cost)
  • Heavy libraries used by server components never reach the browser
@@ -67,12 +67,11 @@ const ServerComponentsPage = ({ comments = [] }) => {

- Click Refresh to fetch a new RSC payload — the server re-renders - this section and streams the result back, no client-side JSON parsing or loading - state plumbing. Click Simulate Error to make the server component - throw; the failure surfaces as ServerComponentFetchError and is - caught by <ErrorBoundary>, which renders a Retry button that - calls refetchComponent with corrected props. + Click Refresh to fetch a new RSC payload — the server re-renders this section and + streams the result back, no client-side JSON parsing or loading state plumbing. Click{' '} + Simulate Error to make the server component throw; the failure surfaces as{' '} + ServerComponentFetchError and is caught by <ErrorBoundary>, which + renders a Retry button that calls refetchComponent with corrected props.

@@ -86,9 +85,9 @@ const ServerComponentsPage = ({ comments = [] }) => {

- Comments come from the Rails controller as props — the canonical React on Rails Pro - pattern. The page shell renders immediately while this section streams in - progressively as Suspense boundaries resolve. + Comments come from the Rails controller as props — the canonical React on Rails Pro pattern. The + page shell renders immediately while this section streams in progressively as Suspense boundaries + resolve.

{ {/* Architecture explanation */}
-

- What makes this different? -

+

What makes this different?

Smaller Client Bundle

@@ -122,22 +119,22 @@ const ServerComponentsPage = ({ comments = [] }) => {

Direct Data Access

- Server components fetch data by calling your Rails API internally — no - client-side fetch waterfalls or loading spinners for initial data. + Server components fetch data by calling your Rails API internally — no client-side fetch + waterfalls or loading spinners for initial data.

Progressive Streaming

- The page shell renders instantly. Async components (like the comments feed) - stream in as their data resolves, with Suspense boundaries showing fallbacks. + The page shell renders instantly. Async components (like the comments feed) stream in as their + data resolves, with Suspense boundaries showing fallbacks.

Selective Hydration

- Only client components (like the toggle above) receive JavaScript. - Everything else is pure HTML — zero hydration cost. + Only client components (like the toggle above) receive JavaScript. Everything else is pure + HTML — zero hydration cost.

@@ -145,6 +142,6 @@ const ServerComponentsPage = ({ comments = [] }) => {
); -}; +} export default ServerComponentsPage; diff --git a/client/app/packs/stores-registration.js b/client/app/packs/stores-registration.js index a069ac62..9eb06333 100644 --- a/client/app/packs/stores-registration.js +++ b/client/app/packs/stores-registration.js @@ -1,4 +1,5 @@ // 'use client' keeps this pack and its store imports out of the RSC bundle. + 'use client'; import ReactOnRails from 'react-on-rails-pro'; diff --git a/config/initializers/react_on_rails.rb b/config/initializers/react_on_rails.rb index b62aea27..09008a93 100644 --- a/config/initializers/react_on_rails.rb +++ b/config/initializers/react_on_rails.rb @@ -7,10 +7,14 @@ config.auto_load_bundle = true # Build commands - # Note: react_on_rails:assets:webpack (run by assets:precompile) depends on react_on_rails:locale, - # so locale generation happens automatically. We just need rescript to run first. - config.build_test_command = "yarn res:build && RAILS_ENV=test bin/shakapacker" - config.build_production_command = "yarn res:build && RAILS_ENV=production NODE_ENV=production bin/shakapacker" + # Direct shakapacker invocations need generated ReScript and locale files first. + config.build_test_command = + "yarn res:build && RAILS_ENV=test bin/rails react_on_rails:locale && " \ + "RAILS_ENV=test bin/rails react_on_rails:generate_packs && RAILS_ENV=test bin/shakapacker" + config.build_production_command = + "yarn res:build && RAILS_ENV=production NODE_ENV=production bin/rails react_on_rails:locale && " \ + "RAILS_ENV=production NODE_ENV=production bin/rails react_on_rails:generate_packs && " \ + "RAILS_ENV=production NODE_ENV=production bin/shakapacker" # This is the file used for server rendering of React when using `(prerender: true)` # If you are never using server rendering, you may set this to "". diff --git a/config/webpack/commonWebpackConfig.js b/config/webpack/commonWebpackConfig.js index 5c14f01f..d9af60ba 100644 --- a/config/webpack/commonWebpackConfig.js +++ b/config/webpack/commonWebpackConfig.js @@ -2,12 +2,14 @@ // https://github.com/shakacode/react_on_rails_tutorial_with_ssr_and_hmr_fast_refresh/blob/master/config/webpack/commonWebpackConfig.js // Common configuration applying to client and server configuration +const path = require('path'); const { generateWebpackConfig, merge } = require('shakapacker'); const commonOptions = { resolve: { // Add .res.js extension for ReScript-compiled modules (modern ReScript convention) extensions: ['.css', '.ts', '.tsx', '.res.js'], + modules: [path.resolve(__dirname, '../../client/app'), 'node_modules'], // Shim for third-party packages (notably rescript-react-on-rails) that import // 'react-on-rails' directly and can't be source-rewritten to react-on-rails-pro. // Without this, Pro and core coexist in the bundle and trigger the runtime error @@ -113,4 +115,3 @@ const commonWebpackConfig = () => { }; module.exports = commonWebpackConfig; - diff --git a/package.json b/package.json index 3fa647ed..010c3a7d 100644 --- a/package.json +++ b/package.json @@ -23,13 +23,13 @@ "res:dev": "yarn res:clean && rescript build -w", "res:watch": "rescript build -w", "res:build": "yarn res:clean && rescript build", - "lint:eslint": "yarn eslint client --ext \".js,.jsx,.ts\"", + "lint:eslint": "yarn eslint client --ext \".js,.jsx,.ts\" --ignore-pattern \"client/app/generated/**\" --ignore-pattern \"client/app/packs/generated/**\" --ignore-pattern \"client/app/libs/i18n/*.js\" --ignore-pattern \"client/app/packs/server-bundle.js\"", "lint:prettier": "yarn prettier \"**/*.@(js|jsx)\" --list-different", "lint": " yarn lint:eslint --fix && yarn lint:prettier --w", "test": "yarn build:test && yarn lint && yarn jest", "test:client": "yarn jest", - "build:test": "rm -rf public/packs-test && RAILS_ENV=test NODE_ENV=test bin/shakapacker", - "build:dev": "rm -rf public/packs && RAILS_ENV=development NODE_ENV=development bin/shakapacker", + "build:test": "rm -rf public/packs-test && yarn res:build && RAILS_ENV=test NODE_ENV=test bundle exec rails react_on_rails:locale && RAILS_ENV=test NODE_ENV=test bundle exec rake react_on_rails:generate_packs && RAILS_ENV=test NODE_ENV=test bin/shakapacker", + "build:dev": "rm -rf public/packs && yarn res:build && RAILS_ENV=development NODE_ENV=development bundle exec rails react_on_rails:locale && RAILS_ENV=development NODE_ENV=development bundle exec rake react_on_rails:generate_packs && RAILS_ENV=development NODE_ENV=development bin/shakapacker", "build:clean": "rm -rf public/packs || true", "node-renderer": "node renderer/node-renderer.js" }, @@ -45,10 +45,10 @@ "@hotwired/stimulus-webpack-helpers": "^1.0.1", "@hotwired/turbo-rails": "^7.3.0", "@rails/actioncable": "7.0.5", - "@rspack/cli": "2.0.0-beta.7", - "@rspack/core": "2.0.0-beta.7", "@rescript/core": "^0.5.0", "@rescript/react": "^0.11.0", + "@rspack/cli": "2.0.0-beta.7", + "@rspack/core": "2.0.0-beta.7", "@swc/core": "^1.13.5", "ajv": "^8.17.1", "autoprefixer": "^10.4.14",