create-react-on-rails-app 16.4.0-rc.0 → 16.4.0-rc.10

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.
@@ -1,4 +1,5 @@
1
1
  import { CliOptions } from './types.js';
2
+ export declare function buildGeneratorArgs(options: CliOptions): string[];
2
3
  export declare function validateAppName(name: string): {
3
4
  success: boolean;
4
5
  error?: string;
package/lib/create-app.js CHANGED
@@ -3,12 +3,47 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.buildGeneratorArgs = buildGeneratorArgs;
6
7
  exports.validateAppName = validateAppName;
7
8
  exports.createApp = createApp;
8
9
  const path_1 = __importDefault(require("path"));
9
10
  const fs_1 = __importDefault(require("fs"));
10
11
  const utils_js_1 = require("./utils.js");
11
- const TOTAL_STEPS = 4;
12
+ function cleanupAppDirectory(appPath, appName, cleanupSuccessMessage, cleanupFallbackMessage) {
13
+ if (!fs_1.default.existsSync(appPath)) {
14
+ return;
15
+ }
16
+ (0, utils_js_1.logInfo)(`Cleaning up "${appName}" directory...`);
17
+ try {
18
+ fs_1.default.rmSync(appPath, { recursive: true, force: true });
19
+ (0, utils_js_1.logInfo)(cleanupSuccessMessage);
20
+ }
21
+ catch {
22
+ (0, utils_js_1.logError)(cleanupFallbackMessage);
23
+ }
24
+ }
25
+ function localGemPath(envVarName) {
26
+ const value = process.env[envVarName];
27
+ if (!value || value.trim() === '') {
28
+ return null;
29
+ }
30
+ const resolvedPath = path_1.default.resolve(value);
31
+ if (!fs_1.default.existsSync(resolvedPath)) {
32
+ (0, utils_js_1.logError)(`Local gem path from ${envVarName} does not exist: ${resolvedPath}`);
33
+ process.exit(1);
34
+ }
35
+ return resolvedPath;
36
+ }
37
+ function bundleAddArgs(gemName, localPath, strict = true) {
38
+ const args = ['add', gemName];
39
+ if (localPath) {
40
+ args.push('--path', localPath);
41
+ }
42
+ else if (strict) {
43
+ args.push('--strict');
44
+ }
45
+ return args;
46
+ }
12
47
  function buildGeneratorArgs(options) {
13
48
  const args = [];
14
49
  if (options.template === 'typescript') {
@@ -17,10 +52,13 @@ function buildGeneratorArgs(options) {
17
52
  if (options.rspack) {
18
53
  args.push('--rspack');
19
54
  }
55
+ if (options.rsc) {
56
+ args.push('--rsc');
57
+ }
20
58
  args.push('--ignore-warnings');
21
59
  return args;
22
60
  }
23
- function printSuccessMessage(appName) {
61
+ function printSuccessMessage(appName, route) {
24
62
  console.log('');
25
63
  (0, utils_js_1.logSuccess)(`Created ${appName} with React on Rails!`);
26
64
  console.log('');
@@ -28,36 +66,45 @@ function printSuccessMessage(appName) {
28
66
  console.log(` cd ${appName}`);
29
67
  console.log(' bin/dev');
30
68
  console.log('');
31
- (0, utils_js_1.logInfo)('Then visit http://localhost:3000/hello_world');
69
+ (0, utils_js_1.logInfo)(`Then visit http://localhost:3000/${route}`);
32
70
  console.log('');
33
71
  (0, utils_js_1.logInfo)('Documentation: https://www.shakacode.com/react-on-rails/docs/');
34
72
  console.log('');
35
73
  }
36
74
  function validateAppName(name) {
37
- if (!name || name.trim() === '') {
75
+ if (!name) {
76
+ return { success: false, error: 'App name is required.' };
77
+ }
78
+ const trimmedName = name.trim();
79
+ if (!trimmedName) {
38
80
  return { success: false, error: 'App name is required.' };
39
81
  }
40
- if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
82
+ if (!/^[a-zA-Z][a-zA-Z0-9]*([_-][a-zA-Z0-9]+)*$/.test(trimmedName)) {
41
83
  return {
42
84
  success: false,
43
- error: 'App name can only contain letters, numbers, hyphens, and underscores.',
85
+ error: 'App name must start with a letter, end with a letter or number, and may contain hyphens or underscores only between alphanumeric characters.',
44
86
  };
45
87
  }
46
- const appPath = path_1.default.resolve(process.cwd(), name);
88
+ const appPath = path_1.default.resolve(process.cwd(), trimmedName);
47
89
  if (fs_1.default.existsSync(appPath)) {
48
90
  return {
49
91
  success: false,
50
- error: `Directory "${name}" already exists. Please choose a different name or remove it.`,
92
+ error: `Directory "${trimmedName}" already exists. Please choose a different name or remove it.`,
51
93
  };
52
94
  }
53
95
  return { success: true };
54
96
  }
55
97
  function createApp(appName, options) {
56
98
  const appPath = path_1.default.resolve(process.cwd(), appName);
99
+ const baseSteps = 3; // rails new + add react_on_rails + run generator
100
+ const totalSteps = baseSteps + (options.rsc ? 1 : 0);
101
+ let currentStep = 1;
102
+ const reactOnRailsGemPath = localGemPath('REACT_ON_RAILS_GEM_PATH');
103
+ const reactOnRailsProGemPath = options.rsc ? localGemPath('REACT_ON_RAILS_PRO_GEM_PATH') : null;
57
104
  // Step 1: Create Rails application
58
- // appName is validated by validateAppName() to be [a-zA-Z0-9_-]+ only,
105
+ // appName is validated by validateAppName() to be ^[a-zA-Z][a-zA-Z0-9]*([_-][a-zA-Z0-9]+)*$ only,
59
106
  // so it's always a simple directory name safe to use with rails new.
60
- (0, utils_js_1.logStep)(1, TOTAL_STEPS, 'Creating Rails application...');
107
+ (0, utils_js_1.logStep)(currentStep, totalSteps, 'Creating Rails application...');
61
108
  try {
62
109
  (0, utils_js_1.execLiveArgs)('rails', ['new', appName, '--database=postgresql', '--skip-javascript']);
63
110
  (0, utils_js_1.logStepDone)('Rails application created');
@@ -67,12 +114,18 @@ function createApp(appName, options) {
67
114
  if (error instanceof Error && error.message) {
68
115
  console.error(`Debug info: ${error.message}`);
69
116
  }
117
+ cleanupAppDirectory(appPath, appName, 'Directory removed. Fix the Rails app creation issue and rerun.', `Delete the created "${appName}" directory and rerun once the Rails app creation issue is resolved.`);
70
118
  process.exit(1);
71
119
  }
72
120
  // Step 2: Add react_on_rails gem
73
- (0, utils_js_1.logStep)(2, TOTAL_STEPS, 'Adding react_on_rails gem...');
121
+ currentStep += 1;
122
+ (0, utils_js_1.logStep)(currentStep, totalSteps, 'Adding react_on_rails gem...');
74
123
  try {
75
- (0, utils_js_1.execLiveArgs)('bundle', ['add', 'react_on_rails', '--strict'], appPath);
124
+ const reactOnRailsArgs = bundleAddArgs('react_on_rails', reactOnRailsGemPath);
125
+ if (reactOnRailsGemPath) {
126
+ (0, utils_js_1.logInfo)(`Using local react_on_rails gem path: ${reactOnRailsGemPath}`);
127
+ }
128
+ (0, utils_js_1.execLiveArgs)('bundle', reactOnRailsArgs, appPath);
76
129
  (0, utils_js_1.logStepDone)('react_on_rails gem added');
77
130
  }
78
131
  catch (error) {
@@ -80,11 +133,33 @@ function createApp(appName, options) {
80
133
  if (error instanceof Error && error.message) {
81
134
  console.error(`Debug info: ${error.message}`);
82
135
  }
136
+ cleanupAppDirectory(appPath, appName, 'Directory removed. Fix gem installation access or connectivity issues and rerun.', `Delete the created "${appName}" directory and rerun once gem installation issues are resolved.`);
83
137
  process.exit(1);
84
138
  }
85
- // Step 3: Run react_on_rails generator
139
+ if (options.rsc) {
140
+ currentStep += 1;
141
+ (0, utils_js_1.logStep)(currentStep, totalSteps, 'Adding react_on_rails_pro gem (--rsc)...');
142
+ try {
143
+ const reactOnRailsProArgs = bundleAddArgs('react_on_rails_pro', reactOnRailsProGemPath, false);
144
+ if (reactOnRailsProGemPath) {
145
+ (0, utils_js_1.logInfo)(`Using local react_on_rails_pro gem path: ${reactOnRailsProGemPath}`);
146
+ }
147
+ (0, utils_js_1.execLiveArgs)('bundle', reactOnRailsProArgs, appPath);
148
+ (0, utils_js_1.logStepDone)('react_on_rails_pro gem added');
149
+ }
150
+ catch (error) {
151
+ (0, utils_js_1.logError)('Failed to add react_on_rails_pro gem required by --rsc.');
152
+ if (error instanceof Error && error.message) {
153
+ console.error(`Debug info: ${error.message}`);
154
+ }
155
+ cleanupAppDirectory(appPath, appName, 'Directory removed. Ensure react_on_rails_pro is installable in your Bundler/RubyGems setup, then rerun with --rsc.', `Ensure react_on_rails_pro is installable, then delete the created "${appName}" directory and rerun with --rsc.`);
156
+ process.exit(1);
157
+ }
158
+ }
159
+ // Final generator step
86
160
  const generatorArgs = buildGeneratorArgs(options);
87
- (0, utils_js_1.logStep)(3, TOTAL_STEPS, 'Running React on Rails generator...');
161
+ currentStep += 1;
162
+ (0, utils_js_1.logStep)(currentStep, totalSteps, 'Running React on Rails generator...');
88
163
  try {
89
164
  (0, utils_js_1.execLiveArgs)('bundle', ['exec', 'rails', 'generate', 'react_on_rails:install', ...generatorArgs], appPath);
90
165
  (0, utils_js_1.logStepDone)('React on Rails setup complete');
@@ -94,10 +169,11 @@ function createApp(appName, options) {
94
169
  if (error instanceof Error && error.message) {
95
170
  console.error(`Debug info: ${error.message}`);
96
171
  }
172
+ cleanupAppDirectory(appPath, appName, 'Directory removed. Fix the generator issue and rerun.', `Delete the created "${appName}" directory and rerun once the generator issue is resolved.`);
97
173
  process.exit(1);
98
174
  }
99
- // Step 4: Success
100
- (0, utils_js_1.logStep)(4, TOTAL_STEPS, 'Done!');
101
- printSuccessMessage(appName);
175
+ // Final success
176
+ (0, utils_js_1.logStepDone)('Done!');
177
+ printSuccessMessage(appName, options.rsc ? 'hello_server' : 'hello_world');
102
178
  }
103
179
  //# sourceMappingURL=create-app.js.map
package/lib/index.js CHANGED
@@ -31,6 +31,7 @@ function run(appName, rawOpts) {
31
31
  template,
32
32
  packageManager: packageManager,
33
33
  rspack: Boolean(rawOpts.rspack),
34
+ rsc: Boolean(rawOpts.rsc),
34
35
  };
35
36
  console.log('');
36
37
  console.log(`${chalk_1.default.bold('create-react-on-rails-app')} v${packageJson.version}`);
@@ -40,6 +41,12 @@ function run(appName, rawOpts) {
40
41
  (0, utils_js_1.logError)(nameValidation.error ?? 'Invalid app name');
41
42
  process.exit(1);
42
43
  }
44
+ if (options.rsc) {
45
+ (0, utils_js_1.logInfo)('Note: --rsc installs react_on_rails_pro and requires that gem to be installable.');
46
+ (0, utils_js_1.logInfo)('If installation fails, verify your Bundler/RubyGems setup for react_on_rails_pro, then rerun with --rsc.');
47
+ (0, utils_js_1.logInfo)('Pro setup docs: https://www.shakacode.com/react-on-rails-pro/docs/installation/');
48
+ console.log('');
49
+ }
43
50
  (0, utils_js_1.logInfo)('Checking prerequisites...');
44
51
  const { allValid, results } = (0, validators_js_1.validateAll)(options.packageManager);
45
52
  for (const { name, result } of results) {
@@ -60,7 +67,7 @@ function run(appName, rawOpts) {
60
67
  process.exit(1);
61
68
  }
62
69
  console.log('');
63
- (0, utils_js_1.logInfo)(`Creating "${appName}" with template: ${options.template}, package manager: ${options.packageManager}${options.rspack ? ', bundler: rspack' : ''}`);
70
+ (0, utils_js_1.logInfo)(`Creating "${appName}" with template: ${options.template}, package manager: ${options.packageManager}${options.rspack ? ', bundler: rspack' : ''}${options.rsc ? ', mode: rsc' : ''}`);
64
71
  (0, create_app_js_1.createApp)(appName, options);
65
72
  }
66
73
  const program = new commander_1.Command();
@@ -74,19 +81,26 @@ program
74
81
  .option('-t, --template <type>', 'javascript or typescript', 'typescript')
75
82
  .option('-p, --package-manager <pm>', 'npm or pnpm (auto-detected if not specified)')
76
83
  .option('--rspack', 'Use Rspack instead of Webpack (~20x faster builds)', false)
84
+ .option('--rsc', 'Generate React Server Components setup (installs react_on_rails_pro)', false)
77
85
  .addHelpText('after', `
78
86
  Examples:
79
87
  $ npx create-react-on-rails-app my-app
80
88
  $ npx create-react-on-rails-app my-app --template javascript
81
89
  $ npx create-react-on-rails-app my-app --rspack
90
+ $ npx create-react-on-rails-app my-app --rsc
91
+ $ npx create-react-on-rails-app my-app --rspack --rsc
82
92
  $ npx create-react-on-rails-app my-app --package-manager pnpm
83
93
 
84
94
  What it does:
85
95
  1. Creates a new Rails app with PostgreSQL
86
- 2. Adds the react_on_rails gem
96
+ 2. Adds required gem(s) (react_on_rails, plus react_on_rails_pro for --rsc)
87
97
  3. Runs the React on Rails generator (Shakapacker, components, webpack config)
88
98
 
89
- After setup, run bin/dev and visit http://localhost:3000/hello_world
99
+ After setup, run bin/dev and visit:
100
+ - http://localhost:3000/hello_world (default)
101
+ - http://localhost:3000/hello_server (--rsc)
102
+
103
+ --rsc supports both JavaScript and TypeScript templates.
90
104
 
91
105
  Documentation: https://www.shakacode.com/react-on-rails/docs/`)
92
106
  .action((appName, opts) => {
package/lib/types.d.ts CHANGED
@@ -2,6 +2,7 @@ export interface CliOptions {
2
2
  template: 'javascript' | 'typescript';
3
3
  packageManager: 'npm' | 'pnpm';
4
4
  rspack: boolean;
5
+ rsc: boolean;
5
6
  }
6
7
  export interface ValidationResult {
7
8
  valid: boolean;
package/lib/utils.d.ts CHANGED
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Execute a command and stream output to the current terminal.
3
+ *
4
+ * Security contract:
5
+ * - This uses `spawnSync` with `shell: false`, so command and args are passed as literal tokens.
6
+ * - Callers must still ensure `command` and `args` come from trusted/validated values.
7
+ */
1
8
  export declare function execLiveArgs(command: string, args: string[], cwd?: string): void;
2
9
  export declare function getCommandVersion(command: string): string | null;
3
10
  export declare function detectPackageManager(): 'npm' | 'pnpm' | null;
package/lib/utils.js CHANGED
@@ -13,6 +13,13 @@ exports.logSuccess = logSuccess;
13
13
  exports.logInfo = logInfo;
14
14
  const child_process_1 = require("child_process");
15
15
  const chalk_1 = __importDefault(require("chalk"));
16
+ /**
17
+ * Execute a command and stream output to the current terminal.
18
+ *
19
+ * Security contract:
20
+ * - This uses `spawnSync` with `shell: false`, so command and args are passed as literal tokens.
21
+ * - Callers must still ensure `command` and `args` come from trusted/validated values.
22
+ */
16
23
  function execLiveArgs(command, args, cwd) {
17
24
  const result = (0, child_process_1.spawnSync)(command, args, {
18
25
  stdio: 'inherit',
package/lib/validators.js CHANGED
@@ -9,6 +9,8 @@ const utils_js_1 = require("./utils.js");
9
9
  const MIN_NODE_VERSION = 18;
10
10
  const MIN_RUBY_MAJOR = 3;
11
11
  const MIN_RUBY_MINOR = 0;
12
+ const MIN_RAILS_MAJOR = 7;
13
+ const MIN_RAILS_MINOR = 0;
12
14
  function validateNode() {
13
15
  const version = process.versions.node;
14
16
  const major = parseInt(version.split('.')[0], 10);
@@ -64,7 +66,24 @@ function validateRails() {
64
66
  'Then try again.',
65
67
  };
66
68
  }
67
- return { valid: true, message: railsVersion.split('\n')[0].trim() };
69
+ const firstLine = railsVersion.split('\n')[0].trim();
70
+ const match = firstLine.match(/rails\s+(\d+)\.(\d+)/i);
71
+ if (!match) {
72
+ return {
73
+ valid: false,
74
+ message: `Could not parse Rails version from: "${firstLine}". Please ensure Rails ${MIN_RAILS_MAJOR}.${MIN_RAILS_MINOR}+ is installed.`,
75
+ };
76
+ }
77
+ const major = parseInt(match[1], 10);
78
+ const minor = parseInt(match[2], 10);
79
+ // minor check is 0 now; kept for parity with Ruby validator and easy bumping later
80
+ if (major < MIN_RAILS_MAJOR || (major === MIN_RAILS_MAJOR && minor < MIN_RAILS_MINOR)) {
81
+ return {
82
+ valid: false,
83
+ message: `${firstLine} detected. React on Rails requires Rails ${MIN_RAILS_MAJOR}.${MIN_RAILS_MINOR}+.`,
84
+ };
85
+ }
86
+ return { valid: true, message: firstLine };
68
87
  }
69
88
  function validatePackageManager(pm) {
70
89
  const version = (0, utils_js_1.getCommandVersion)(pm);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-react-on-rails-app",
3
- "version": "16.4.0-rc.0",
3
+ "version": "16.4.0-rc.10",
4
4
  "description": "Create React on Rails applications with a single command",
5
5
  "bin": {
6
6
  "create-react-on-rails-app": "./bin/create-react-on-rails-app.js"
@@ -44,7 +44,7 @@
44
44
  "scripts": {
45
45
  "build": "pnpm run clean && tsc",
46
46
  "build-watch": "pnpm run clean && tsc --watch",
47
- "clean": "rm -rf ./lib",
47
+ "clean": "node -e \"require('fs').rmSync('./lib', { recursive: true, force: true })\"",
48
48
  "test": "jest tests",
49
49
  "type-check": "tsc --noEmit --noErrorTruncation"
50
50
  }