Skip to content

Commit 0f2bffa

Browse files
committed
Fix Pro RSC Rspack runtime readiness
1 parent 2b6184a commit 0f2bffa

4 files changed

Lines changed: 115 additions & 4 deletions

File tree

.github/workflows/pro-integration-tests.yml

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -687,8 +687,56 @@ jobs:
687687
done
688688
}
689689
690-
wait_for_service "Rails server" false "http://localhost:3000"
691-
wait_for_service "Node renderer" true "http://[::1]:3800/info" "http://127.0.0.1:3800/info"
690+
wait_for_h2c_service() {
691+
name="$1"
692+
authority="$2"
693+
path="$3"
694+
timeout=300
695+
elapsed=0
696+
697+
while true; do
698+
if node - "$authority" "$path" <<'NODE'
699+
const http2 = require('node:http2');
700+
701+
const [authority, path] = process.argv.slice(2);
702+
const client = http2.connect(authority);
703+
const request = client.request({ ':method': 'GET', ':path': path });
704+
let status;
705+
706+
const fail = () => {
707+
client.destroy();
708+
process.exit(1);
709+
};
710+
711+
client.on('error', fail);
712+
request.on('error', fail);
713+
request.on('response', (headers) => {
714+
status = headers[':status'];
715+
});
716+
request.on('data', () => {});
717+
request.on('end', () => {
718+
client.close();
719+
process.exit(status >= 200 && status < 300 ? 0 : 1);
720+
});
721+
request.end();
722+
setTimeout(fail, 2000);
723+
NODE
724+
then
725+
echo "${name} started at ${authority}${path} after ${elapsed}s"
726+
return 0
727+
fi
728+
729+
sleep 1
730+
elapsed=$((elapsed + 1))
731+
if [ $elapsed -ge $timeout ]; then
732+
echo "Timeout waiting for ${name} to start after ${timeout}s"
733+
exit 1
734+
fi
735+
done
736+
}
737+
738+
wait_for_service "Rails server" true "http://localhost:3000/empty"
739+
wait_for_h2c_service "Node renderer" "http://[::1]:3800" "/info"
692740
693741
- name: Install Playwright dependencies
694742
run: cd spec/dummy && npx playwright install --with-deps

react_on_rails_pro/spec/dummy/bin/shakapacker

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
require "pathname"
55

66
ENV["RAILS_ENV"] ||= "development"
7-
ENV["NODE_ENV"] ||= ENV.fetch("RAILS_ENV")
7+
ENV["NODE_ENV"] ||= ENV.fetch("RAILS_ENV", "development")
88
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", Pathname.new(__FILE__).realpath)
99

1010
require "bundler/setup"
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
// Set this before loading the shared webpack config because Shakapacker reads
22
// the assets_bundler setting while its config module is required.
3+
const previousAssetsBundler = process.env.SHAKAPACKER_ASSETS_BUNDLER;
34
process.env.SHAKAPACKER_ASSETS_BUNDLER = 'rspack';
45

5-
module.exports = require('../webpack/webpack.config');
6+
try {
7+
// eslint-disable-next-line global-require
8+
module.exports = require('../webpack/webpack.config');
9+
} finally {
10+
if (previousAssetsBundler === undefined) {
11+
delete process.env.SHAKAPACKER_ASSETS_BUNDLER;
12+
} else {
13+
process.env.SHAKAPACKER_ASSETS_BUNDLER = previousAssetsBundler;
14+
}
15+
}

react_on_rails_pro/spec/dummy/tests/rsc-rspack-config.test.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ const rspackConfigPath = path.resolve(__dirname, '../config/rspack/rspack.config
55
const shakapackerBinPath = path.resolve(__dirname, '../bin/shakapacker');
66
const clientConfigPath = path.resolve(__dirname, '../config/webpack/clientWebpackConfig.js');
77
const serverConfigPath = path.resolve(__dirname, '../config/webpack/serverWebpackConfig.js');
8+
const rscConfigPath = path.resolve(__dirname, '../config/webpack/rscWebpackConfig.js');
9+
10+
afterEach(() => {
11+
jest.resetModules();
12+
jest.clearAllMocks();
13+
});
814

915
describe('Pro dummy RSC rspack config', () => {
1016
it('provides a rspack entrypoint that delegates to the shared build config', () => {
@@ -46,4 +52,51 @@ describe('Pro dummy RSC rspack config', () => {
4652
expect(clientSource).toContain('path: false');
4753
expect(clientSource).toContain('stream: false');
4854
});
55+
56+
it('wraps function-shaped JavaScript loader rules once for the RSC loader', () => {
57+
jest.doMock('shakapacker', () => ({
58+
config: {
59+
source_entry_path: 'packs',
60+
source_path: 'client/app',
61+
},
62+
}));
63+
64+
const functionRule = {
65+
use() {
66+
return [{ loader: 'babel-loader', options: { caller: { ssr: true } } }];
67+
},
68+
};
69+
const serverConfig = {
70+
entry: { 'server-bundle': './server-bundle.js' },
71+
module: { rules: [functionRule] },
72+
output: {},
73+
plugins: [],
74+
resolve: { alias: {} },
75+
};
76+
const configureServer = jest.fn(() => serverConfig);
77+
const extractLoader = (rule, loaderName) =>
78+
Array.isArray(rule.use) ? rule.use.find((item) => item?.loader?.includes(loaderName)) : null;
79+
80+
jest.doMock(serverConfigPath, () => ({
81+
default: configureServer,
82+
extractLoader,
83+
}));
84+
85+
// eslint-disable-next-line global-require, import/no-dynamic-require
86+
const configureRsc = require(rscConfigPath);
87+
const firstConfig = configureRsc();
88+
const wrappedUse = firstConfig.module.rules[0].use;
89+
const firstUseResult = wrappedUse({});
90+
91+
configureRsc();
92+
const secondUseResult = firstConfig.module.rules[0].use({});
93+
94+
expect(configureServer).toHaveBeenCalledWith(true);
95+
expect(wrappedUse.name).toBe('rscLoaderWrapper');
96+
expect(firstUseResult).toEqual([
97+
{ loader: 'babel-loader', options: { caller: { ssr: true } } },
98+
{ loader: 'react-on-rails-rsc/WebpackLoader' },
99+
]);
100+
expect(secondUseResult).toEqual(firstUseResult);
101+
});
49102
});

0 commit comments

Comments
 (0)