create-react-on-rails-app 16.5.1 → 16.6.0-rc.1

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.
package/lib/create-app.js CHANGED
@@ -10,6 +10,94 @@ const path_1 = __importDefault(require("path"));
10
10
  const fs_1 = __importDefault(require("fs"));
11
11
  const utils_js_1 = require("./utils.js");
12
12
  const DOCS_URL = 'https://reactonrails.com/docs/';
13
+ const DEFAULT_GIT_AUTHOR_NAME = 'React on Rails Generator';
14
+ const DEFAULT_GIT_AUTHOR_EMAIL = 'generator@reactonrails.invalid';
15
+ const SUPPORTED_RAILTIES_MAJORS = [7, 8];
16
+ // Keep this renderer intentionally minimal. It renders Rails' installed
17
+ // gitignore/gitattributes templates and falls back to bundled defaults when:
18
+ // - railties is unavailable in the current Ruby environment, or
19
+ // - railties major version is outside SUPPORTED_RAILTIES_MAJORS.
20
+ const RAILS_GIT_TEMPLATE_RENDERER = `
21
+ require "erb"
22
+
23
+ class ReactOnRailsGitTemplateOptions
24
+ def api? = false
25
+
26
+ def [](key)
27
+ key == :skip_active_record ? false : nil
28
+ end
29
+ end
30
+
31
+ class ReactOnRailsGitTemplateContext
32
+ def keeps? = true
33
+
34
+ def skip_storage? = false
35
+
36
+ def options = ReactOnRailsGitTemplateOptions.new
37
+
38
+ def get_binding
39
+ binding
40
+ end
41
+ end
42
+
43
+ spec = Gem::Specification.find_by_name("railties")
44
+ railties_major = spec.version.segments.first
45
+ unless [${SUPPORTED_RAILTIES_MAJORS.join(', ')}].include?(railties_major)
46
+ warn "Unsupported railties major version for git template rendering: #{spec.version}"
47
+ exit 1
48
+ end
49
+ template_path = File.join(spec.gem_dir, "lib/rails/generators/rails/app/templates", ARGV[0])
50
+ template = File.read(template_path)
51
+ print ERB.new(template, trim_mode: "-").result(ReactOnRailsGitTemplateContext.new.get_binding)
52
+ `;
53
+ // Fallback copy of the Rails git scaffold (validated against Rails 7.2 templates)
54
+ // for environments where rendering the installed railties templates is unavailable.
55
+ const RAILS_GITIGNORE_FALLBACK = `# See https://help.github.com/articles/ignoring-files for more about ignoring files.
56
+ #
57
+ # Temporary files generated by your text editor or operating system
58
+ # belong in git's global ignore instead:
59
+ # \`$XDG_CONFIG_HOME/git/ignore\` or \`~/.config/git/ignore\`
60
+
61
+ # Ignore bundler config.
62
+ /.bundle
63
+
64
+ # Ignore all environment files.
65
+ /.env*
66
+
67
+ # Ignore all logfiles and tempfiles.
68
+ /log/*
69
+ /tmp/*
70
+ !/log/.keep
71
+ !/tmp/.keep
72
+
73
+ # Ignore node modules.
74
+ /node_modules
75
+
76
+ # Ignore pidfiles, but keep the directory.
77
+ /tmp/pids/*
78
+ !/tmp/pids/
79
+ !/tmp/pids/.keep
80
+
81
+ # Ignore storage (uploaded files in development and any SQLite databases).
82
+ /storage/*
83
+ !/storage/.keep
84
+ /tmp/storage/*
85
+ !/tmp/storage/
86
+ !/tmp/storage/.keep
87
+
88
+ /public/assets
89
+
90
+ # Ignore key files for decrypting credentials and more.
91
+ /config/*.key
92
+ `;
93
+ const RAILS_GITATTRIBUTES_FALLBACK = `# See https://git-scm.com/docs/gitattributes for more about git attribute files.
94
+
95
+ # Mark the database schema as having been generated.
96
+ db/schema.rb linguist-generated
97
+
98
+ # Mark any vendored files as having been vendored.
99
+ vendor/* linguist-vendored
100
+ `;
13
101
  function cleanupAppDirectory(appPath, appName, cleanupSuccessMessage, cleanupFallbackMessage) {
14
102
  if (!fs_1.default.existsSync(appPath)) {
15
103
  return;
@@ -46,7 +134,7 @@ function bundleAddArgs(gemName, localPath, strict = true) {
46
134
  return args;
47
135
  }
48
136
  function buildGeneratorArgs(options) {
49
- const args = [];
137
+ const args = ['--new-app'];
50
138
  if (options.template === 'typescript') {
51
139
  args.push('--typescript');
52
140
  }
@@ -63,10 +151,9 @@ function buildGeneratorArgs(options) {
63
151
  // --force makes the generator overwrite conflicting files without prompting,
64
152
  // which is safe for a freshly scaffolded app with no custom content yet.
65
153
  args.push('--force');
66
- // The newly created app directory is not a git repo yet, so the generator's
67
- // uncommitted-changes check would always warn. --ignore-warnings bypasses all
68
- // generator validation warnings (git, Node version, package manager) which is
69
- // acceptable for a fresh app where prerequisites were already checked above.
154
+ // Keep the create-app flow non-interactive. --ignore-warnings bypasses generator
155
+ // validation warnings (git, Node version, package manager) because the CLI
156
+ // already checked prerequisites and is driving a fresh-app automation path.
70
157
  args.push('--ignore-warnings');
71
158
  return args;
72
159
  }
@@ -102,6 +189,30 @@ function rewriteFileIfPresent(filePath, transform) {
102
189
  fs_1.default.writeFileSync(filePath, updated, 'utf8');
103
190
  }
104
191
  }
192
+ function readInstalledRailsGitTemplate(appPath, templateName) {
193
+ try {
194
+ const renderedTemplate = (0, utils_js_1.execCaptureArgs)('ruby', ['-e', RAILS_GIT_TEMPLATE_RENDERER, templateName], appPath);
195
+ return renderedTemplate.length > 0 ? `${renderedTemplate}\n` : null;
196
+ }
197
+ catch {
198
+ return null;
199
+ }
200
+ }
201
+ function railsGitScaffold(appPath) {
202
+ const renderedGitignore = readInstalledRailsGitTemplate(appPath, 'gitignore.tt');
203
+ const renderedGitattributes = readInstalledRailsGitTemplate(appPath, 'gitattributes.tt');
204
+ return {
205
+ gitignore: renderedGitignore ?? RAILS_GITIGNORE_FALLBACK,
206
+ gitattributes: renderedGitattributes ?? RAILS_GITATTRIBUTES_FALLBACK,
207
+ };
208
+ }
209
+ function restoreRailsGitScaffold(appPath) {
210
+ // Rails omits .gitignore/.gitattributes when rails new runs with --skip-git.
211
+ // Recreate the defaults so the educational commit history stays clean.
212
+ const scaffold = railsGitScaffold(appPath);
213
+ fs_1.default.writeFileSync(path_1.default.join(appPath, '.gitignore'), scaffold.gitignore, 'utf8');
214
+ fs_1.default.writeFileSync(path_1.default.join(appPath, '.gitattributes'), scaffold.gitattributes, 'utf8');
215
+ }
105
216
  function normalizeGeneratedPackageManager(appPath, packageManager) {
106
217
  if (packageManager !== 'pnpm') {
107
218
  return;
@@ -133,7 +244,95 @@ function normalizeGeneratedPackageManager(appPath, packageManager) {
133
244
  });
134
245
  (0, utils_js_1.logStepDone)('pnpm configuration applied');
135
246
  }
136
- function printSuccessMessage(appName, route) {
247
+ function ensureGitRepository(appPath) {
248
+ if (fs_1.default.existsSync(path_1.default.join(appPath, '.git'))) {
249
+ return;
250
+ }
251
+ (0, utils_js_1.execLiveArgs)('git', ['init'], appPath);
252
+ }
253
+ function readGitConfig(appPath, key) {
254
+ try {
255
+ const value = (0, utils_js_1.execCaptureArgs)('git', ['config', '--get', key], appPath);
256
+ return value === '' ? null : value;
257
+ }
258
+ catch {
259
+ return null;
260
+ }
261
+ }
262
+ function educationalGitEnv(appPath) {
263
+ const userName = readGitConfig(appPath, 'user.name') ?? DEFAULT_GIT_AUTHOR_NAME;
264
+ const userEmail = readGitConfig(appPath, 'user.email') ?? DEFAULT_GIT_AUTHOR_EMAIL;
265
+ return {
266
+ ...process.env,
267
+ GIT_AUTHOR_NAME: userName,
268
+ GIT_AUTHOR_EMAIL: userEmail,
269
+ GIT_COMMITTER_NAME: userName,
270
+ GIT_COMMITTER_EMAIL: userEmail,
271
+ };
272
+ }
273
+ function gitHasPendingChanges(appPath) {
274
+ try {
275
+ return (0, utils_js_1.execCaptureArgs)('git', ['status', '--porcelain'], appPath) !== '';
276
+ }
277
+ catch {
278
+ return false;
279
+ }
280
+ }
281
+ function createEducationalCommit(appPath, subject, body, educationalGitEnvForCommit) {
282
+ ensureGitRepository(appPath);
283
+ if (!gitHasPendingChanges(appPath)) {
284
+ return false;
285
+ }
286
+ // The educational history intentionally snapshots each full scaffold step.
287
+ (0, utils_js_1.execLiveArgs)('git', ['add', '-A'], appPath);
288
+ (0, utils_js_1.execLiveArgs)('git', ['-c', 'commit.gpgsign=false', 'commit', '-m', subject, '-m', body], appPath, educationalGitEnvForCommit());
289
+ return true;
290
+ }
291
+ function railsBaselineCommitMessage() {
292
+ return {
293
+ subject: 'Create Rails app with PostgreSQL',
294
+ body: 'Generate the fresh Rails baseline before adding React on Rails.',
295
+ };
296
+ }
297
+ function reactOnRailsGemCommitMessage() {
298
+ return {
299
+ subject: 'Add react_on_rails gem',
300
+ body: 'Install the OSS gem so the next commit can run the React on Rails generator.',
301
+ };
302
+ }
303
+ function reactOnRailsProCommitMessage(modeFlag) {
304
+ return {
305
+ subject: 'Add react_on_rails_pro gem',
306
+ body: `Install the Pro gem required by ${modeFlag} before running the generator.`,
307
+ };
308
+ }
309
+ function generatorCommitMessage(options) {
310
+ const language = options.template === 'typescript' ? 'TypeScript' : 'JavaScript';
311
+ const bundler = options.rspack ? 'Rspack' : 'Webpack';
312
+ if (options.rsc) {
313
+ return {
314
+ subject: `Install React Server Components with ${language} and ${bundler}`,
315
+ body: 'Run react_on_rails:install --rsc to add the generated home page, HelloServer example, and Pro RSC wiring.',
316
+ };
317
+ }
318
+ if (options.pro) {
319
+ return {
320
+ subject: `Install React on Rails Pro with ${language} and ${bundler}`,
321
+ body: 'Run react_on_rails:install --pro to add the generated home page, SSR example, and Pro Node renderer wiring.',
322
+ };
323
+ }
324
+ return {
325
+ subject: `Install React on Rails with ${language} and ${bundler}`,
326
+ body: 'Run react_on_rails:install to add the generated home page, SSR example, and development workflow.',
327
+ };
328
+ }
329
+ function pnpmCommitMessage() {
330
+ return {
331
+ subject: 'Normalize the generated app for pnpm',
332
+ body: 'Convert npm artifacts to pnpm, remove package-lock.json, and update bin/setup to use pnpm install.',
333
+ };
334
+ }
335
+ function printSuccessMessage(appName, educationalCommitsEnabled) {
137
336
  console.log('');
138
337
  (0, utils_js_1.logSuccess)(`Created ${appName} with React on Rails!`);
139
338
  console.log('');
@@ -144,7 +343,16 @@ function printSuccessMessage(appName, route) {
144
343
  console.log('');
145
344
  (0, utils_js_1.logInfo)('Note: The generated app uses PostgreSQL by default. Start PostgreSQL before running bin/rails db:prepare.');
146
345
  console.log('');
147
- (0, utils_js_1.logInfo)(`Then visit http://localhost:3000/${route}`);
346
+ (0, utils_js_1.logInfo)('Then visit http://localhost:3000');
347
+ (0, utils_js_1.logInfo)('bin/dev will try to open the generated home page on first successful boot.');
348
+ console.log('');
349
+ if (educationalCommitsEnabled) {
350
+ (0, utils_js_1.logInfo)('Educational commits skip GPG signing so scaffold automation does not block on local signer prompts.');
351
+ (0, utils_js_1.logInfo)('Educational git history: git log --oneline --reverse');
352
+ }
353
+ else {
354
+ (0, utils_js_1.logInfo)('Educational git history is partial because git commit automation was skipped after an earlier failure.');
355
+ }
148
356
  console.log('');
149
357
  (0, utils_js_1.logInfo)(`Documentation: ${DOCS_URL}`);
150
358
  console.log('');
@@ -175,24 +383,49 @@ function validateAppName(name) {
175
383
  function createApp(appName, options) {
176
384
  const appPath = path_1.default.resolve(process.cwd(), appName);
177
385
  const proRequested = options.pro || options.rsc;
386
+ let educationalCommitsEnabled = true;
387
+ let cachedEducationalGitEnv = null;
178
388
  let proModeLabel = null;
179
- if (options.rsc) {
180
- proModeLabel = '--rsc';
181
- }
182
- else if (options.pro) {
183
- proModeLabel = '--pro';
389
+ if (proRequested) {
390
+ proModeLabel = options.rsc ? '--rsc' : '--pro';
184
391
  }
185
392
  const baseSteps = 3; // rails new + add react_on_rails + run generator
186
393
  const totalSteps = baseSteps + (proRequested ? 1 : 0);
187
394
  let currentStep = 1;
188
395
  const reactOnRailsGemPath = localGemPath('REACT_ON_RAILS_GEM_PATH');
189
396
  const reactOnRailsProGemPath = proRequested ? localGemPath('REACT_ON_RAILS_PRO_GEM_PATH') : null;
397
+ function educationalGitEnvForCommits() {
398
+ if (!cachedEducationalGitEnv) {
399
+ // The baseline commit happens before git init; in that case git config falls back
400
+ // to the user's global identity, which is the behavior we want here.
401
+ cachedEducationalGitEnv = educationalGitEnv(appPath);
402
+ }
403
+ return cachedEducationalGitEnv;
404
+ }
405
+ function recordEducationalCommit(subject, body) {
406
+ if (!educationalCommitsEnabled) {
407
+ return;
408
+ }
409
+ try {
410
+ createEducationalCommit(appPath, subject, body, educationalGitEnvForCommits);
411
+ }
412
+ catch (error) {
413
+ educationalCommitsEnabled = false;
414
+ (0, utils_js_1.logInfo)(`Educational git history paused after "${subject}" because git commit automation failed. The app scaffold will continue.`);
415
+ if (error instanceof Error && error.message) {
416
+ console.error(`Git debug info: ${error.message}`);
417
+ }
418
+ }
419
+ }
190
420
  // Step 1: Create Rails application
191
421
  // appName is validated by validateAppName() to be ^[a-zA-Z][a-zA-Z0-9]*([_-][a-zA-Z0-9]+)*$ only,
192
422
  // so it's always a simple directory name safe to use with rails new.
193
423
  (0, utils_js_1.logStep)(currentStep, totalSteps, 'Creating Rails application...');
194
424
  try {
195
- (0, utils_js_1.execLiveArgs)('rails', ['new', appName, '--database=postgresql', '--skip-javascript']);
425
+ (0, utils_js_1.execLiveArgs)('rails', ['new', appName, '--database=postgresql', '--skip-javascript', '--skip-git']);
426
+ restoreRailsGitScaffold(appPath);
427
+ const { subject, body } = railsBaselineCommitMessage();
428
+ recordEducationalCommit(subject, body);
196
429
  (0, utils_js_1.logStepDone)('Rails application created');
197
430
  }
198
431
  catch (error) {
@@ -212,6 +445,8 @@ function createApp(appName, options) {
212
445
  (0, utils_js_1.logInfo)(`Using local react_on_rails gem path: ${reactOnRailsGemPath}`);
213
446
  }
214
447
  (0, utils_js_1.execLiveArgs)('bundle', reactOnRailsArgs, appPath);
448
+ const { subject, body } = reactOnRailsGemCommitMessage();
449
+ recordEducationalCommit(subject, body);
215
450
  (0, utils_js_1.logStepDone)('react_on_rails gem added');
216
451
  }
217
452
  catch (error) {
@@ -231,6 +466,8 @@ function createApp(appName, options) {
231
466
  (0, utils_js_1.logInfo)(`Using local react_on_rails_pro gem path: ${reactOnRailsProGemPath}`);
232
467
  }
233
468
  (0, utils_js_1.execLiveArgs)('bundle', reactOnRailsProArgs, appPath);
469
+ const { subject, body } = reactOnRailsProCommitMessage(proModeLabel ?? '--pro');
470
+ recordEducationalCommit(subject, body);
234
471
  (0, utils_js_1.logStepDone)('react_on_rails_pro gem added');
235
472
  }
236
473
  catch (error) {
@@ -251,6 +488,8 @@ function createApp(appName, options) {
251
488
  ...process.env,
252
489
  REACT_ON_RAILS_PACKAGE_MANAGER: options.packageManager,
253
490
  });
491
+ const { subject, body } = generatorCommitMessage(options);
492
+ recordEducationalCommit(subject, body);
254
493
  (0, utils_js_1.logStepDone)('React on Rails setup complete');
255
494
  }
256
495
  catch (error) {
@@ -263,6 +502,10 @@ function createApp(appName, options) {
263
502
  }
264
503
  try {
265
504
  normalizeGeneratedPackageManager(appPath, options.packageManager);
505
+ if (options.packageManager === 'pnpm') {
506
+ const { subject, body } = pnpmCommitMessage();
507
+ recordEducationalCommit(subject, body);
508
+ }
266
509
  }
267
510
  catch (error) {
268
511
  (0, utils_js_1.logError)(`Failed to finish ${options.packageManager} setup. The app was created, but package manager normalization did not complete.`);
@@ -280,6 +523,6 @@ function createApp(appName, options) {
280
523
  }
281
524
  // Final success
282
525
  (0, utils_js_1.logStepDone)('Done!');
283
- printSuccessMessage(appName, options.rsc ? 'hello_server' : 'hello_world');
526
+ printSuccessMessage(appName, educationalCommitsEnabled);
284
527
  }
285
528
  //# sourceMappingURL=create-app.js.map
package/lib/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- export {};
1
+ import { Command } from 'commander';
2
+ export declare const ready: Promise<Command>;
2
3
  //# sourceMappingURL=index.d.ts.map
package/lib/index.js CHANGED
@@ -3,15 +3,17 @@ 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.ready = void 0;
6
7
  const commander_1 = require("commander");
7
8
  const chalk_1 = __importDefault(require("chalk"));
8
9
  const validators_js_1 = require("./validators.js");
9
10
  const create_app_js_1 = require("./create-app.js");
10
11
  const utils_js_1 = require("./utils.js");
12
+ const prompt_js_1 = require("./prompt.js");
11
13
  // Use require() for CJS compatibility - avoids __dirname + fs.readFileSync
12
14
  // eslint-disable-next-line @typescript-eslint/no-require-imports, global-require
13
15
  const packageJson = require('../package.json');
14
- function run(appName, rawOpts) {
16
+ async function run(appName, rawOpts) {
15
17
  const { template } = rawOpts;
16
18
  if (typeof template !== 'string' || (template !== 'javascript' && template !== 'typescript')) {
17
19
  (0, utils_js_1.logError)(`Invalid template "${String(template)}". Must be "javascript" or "typescript".`);
@@ -27,13 +29,8 @@ function run(appName, rawOpts) {
27
29
  else {
28
30
  packageManager = (0, utils_js_1.detectPackageManager)() ?? 'npm';
29
31
  }
30
- const options = {
31
- template,
32
- packageManager: packageManager,
33
- rspack: Boolean(rawOpts.rspack),
34
- pro: Boolean(rawOpts.pro),
35
- rsc: Boolean(rawOpts.rsc),
36
- };
32
+ let pro = Boolean(rawOpts.pro);
33
+ let rsc = Boolean(rawOpts.rsc);
37
34
  console.log('');
38
35
  console.log(`${chalk_1.default.bold('create-react-on-rails-app')} v${packageJson.version}`);
39
36
  console.log('');
@@ -42,6 +39,26 @@ function run(appName, rawOpts) {
42
39
  (0, utils_js_1.logError)(nameValidation.error ?? 'Invalid app name');
43
40
  process.exit(1);
44
41
  }
42
+ // When no mode flag is explicitly passed, prompt interactively (TTY only).
43
+ // Non-interactive environments (CI, pipes) fall back to standard mode.
44
+ const modeExplicit = rawOpts.pro !== undefined || rawOpts.rsc !== undefined || rawOpts.standard !== undefined;
45
+ if (!modeExplicit) {
46
+ if (process.stdin.isTTY && process.stdout.isTTY) {
47
+ const choice = await (0, prompt_js_1.promptForMode)();
48
+ pro = choice.pro;
49
+ rsc = choice.rsc;
50
+ }
51
+ else {
52
+ (0, utils_js_1.logInfo)('No mode flag specified and not running interactively (stdin/stdout is not a TTY); using standard mode.');
53
+ }
54
+ }
55
+ const options = {
56
+ template,
57
+ packageManager: packageManager,
58
+ rspack: Boolean(rawOpts.rspack),
59
+ pro,
60
+ rsc,
61
+ };
45
62
  if (options.rsc && options.pro) {
46
63
  (0, utils_js_1.logInfo)('Note: --rsc takes precedence over --pro; --pro will be ignored.');
47
64
  }
@@ -93,39 +110,60 @@ program
93
110
  .option('-t, --template <type>', 'javascript or typescript', 'typescript')
94
111
  .option('-p, --package-manager <pm>', 'npm or pnpm (auto-detected if not specified)')
95
112
  .option('--rspack', 'Use Rspack instead of Webpack (~20x faster builds)', false)
96
- .option('--pro', 'Generate React on Rails Pro setup (installs react_on_rails_pro)', false)
97
- .option('--rsc', 'Generate React Server Components setup (installs react_on_rails_pro)', false)
113
+ .option('--standard', 'Generate open-source React on Rails setup (skip prompt)')
114
+ .option('--pro', 'Generate React on Rails Pro setup (installs react_on_rails_pro)')
115
+ .option('--rsc', 'Generate React Server Components setup (installs react_on_rails_pro)')
98
116
  .addHelpText('after', `
99
117
  Examples:
100
- $ npx create-react-on-rails-app my-app
118
+ $ npx create-react-on-rails-app my-app # prompts for mode
119
+ $ npx create-react-on-rails-app my-app --rsc # skip prompt, use RSC
120
+ $ npx create-react-on-rails-app my-app --pro # skip prompt, use Pro
121
+ $ npx create-react-on-rails-app my-app --standard # skip prompt, use Standard
101
122
  $ npx create-react-on-rails-app my-app --template javascript
102
123
  $ npx create-react-on-rails-app my-app --rspack
103
- $ npx create-react-on-rails-app my-app --pro
104
- $ npx create-react-on-rails-app my-app --rsc
105
- $ npx create-react-on-rails-app my-app --rspack --pro
106
124
  $ npx create-react-on-rails-app my-app --rspack --rsc
107
125
  $ npx create-react-on-rails-app my-app --package-manager pnpm
108
126
 
127
+ When no mode flag (--standard, --pro, or --rsc) is given, an interactive prompt
128
+ lets you choose between Standard, Pro, and RSC modes (default: RSC). When stdin
129
+ or stdout is not a TTY (for example in CI, piped input, or redirected output),
130
+ standard mode is used automatically.
131
+
109
132
  What it does:
110
133
  1. Creates a new Rails app with PostgreSQL
111
- 2. Adds required gem(s) (react_on_rails, plus react_on_rails_pro for --pro/--rsc)
134
+ 2. Adds required gem(s) (react_on_rails, plus react_on_rails_pro for Pro/RSC)
112
135
  3. Runs the React on Rails generator (Shakapacker, components, webpack config)
136
+ 4. Creates educational git commits for each major scaffold step
113
137
 
114
138
  After setup, run bin/dev and visit:
115
- - http://localhost:3000/hello_world (default and --pro)
116
- - http://localhost:3000/hello_server (--rsc)
139
+ - http://localhost:3000 (generated home page)
140
+ - /hello_world (default and --pro example page)
141
+ - /hello_server (--rsc example page)
117
142
 
143
+ Inspect the generated setup history with:
144
+ - git log --oneline --reverse
145
+
146
+ The generated app includes one git commit per logical setup step.`)
147
+ .addHelpText('after', `
118
148
  --pro and --rsc support both JavaScript and TypeScript templates.
119
149
 
120
150
  Documentation: https://reactonrails.com/docs/`)
121
- .action((appName, opts) => {
151
+ .action(async (appName, opts) => {
122
152
  try {
123
- run(appName, opts);
153
+ await run(appName, opts);
124
154
  }
125
155
  catch (error) {
156
+ if (error instanceof Error && error.message === prompt_js_1.PROMPT_CANCELLED) {
157
+ console.log('');
158
+ process.exit(0);
159
+ }
126
160
  (0, utils_js_1.logError)(error instanceof Error ? error.message : String(error));
127
161
  process.exit(1);
128
162
  }
129
163
  });
130
- program.parse();
164
+ // eslint-disable-next-line import/prefer-default-export -- named export for test clarity
165
+ exports.ready = program.parseAsync().catch((error) => {
166
+ (0, utils_js_1.logError)(error instanceof Error ? error.message : String(error));
167
+ process.exit(1);
168
+ });
131
169
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ export interface ModeChoice {
2
+ pro: boolean;
3
+ rsc: boolean;
4
+ }
5
+ export declare const PROMPT_CANCELLED = "Prompt cancelled by user";
6
+ export declare function promptForMode(): Promise<ModeChoice>;
7
+ //# sourceMappingURL=prompt.d.ts.map
package/lib/prompt.js ADDED
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PROMPT_CANCELLED = void 0;
7
+ exports.promptForMode = promptForMode;
8
+ const readline_1 = __importDefault(require("readline"));
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ exports.PROMPT_CANCELLED = 'Prompt cancelled by user';
11
+ const MODES = [
12
+ {
13
+ key: '1',
14
+ label: 'Standard',
15
+ desc: 'Open-source React on Rails with SSR',
16
+ pro: false,
17
+ rsc: false,
18
+ },
19
+ {
20
+ key: '2',
21
+ label: 'Pro',
22
+ desc: 'Adds Node.js server rendering (requires react_on_rails_pro)',
23
+ pro: true,
24
+ rsc: false,
25
+ },
26
+ {
27
+ key: '3',
28
+ label: 'RSC',
29
+ desc: 'React Server Components (requires react_on_rails_pro)',
30
+ pro: false,
31
+ rsc: true,
32
+ },
33
+ ];
34
+ const DEFAULT_KEY = '3';
35
+ function promptForMode() {
36
+ return new Promise((resolve, reject) => {
37
+ const rl = readline_1.default.createInterface({
38
+ input: process.stdin,
39
+ output: process.stdout,
40
+ });
41
+ let answered = false;
42
+ rl.on('SIGINT', () => {
43
+ answered = true;
44
+ rl.close();
45
+ reject(new Error(exports.PROMPT_CANCELLED));
46
+ });
47
+ rl.once('close', () => {
48
+ if (!answered) {
49
+ reject(new Error(exports.PROMPT_CANCELLED));
50
+ }
51
+ });
52
+ console.log(chalk_1.default.bold('Select a setup mode:\n'));
53
+ for (const mode of MODES) {
54
+ const recommended = mode.key === DEFAULT_KEY ? chalk_1.default.cyan(' (recommended)') : '';
55
+ console.log(` ${mode.key}. ${chalk_1.default.bold(mode.label.padEnd(10))} ${mode.desc}${recommended}`);
56
+ }
57
+ console.log('');
58
+ rl.question(`Choice (1-3) [${DEFAULT_KEY}]: `, (answer) => {
59
+ answered = true;
60
+ rl.close();
61
+ const key = answer.trim() || DEFAULT_KEY;
62
+ const selected = MODES.find((m) => m.key === key);
63
+ if (!selected) {
64
+ console.log(chalk_1.default.yellow(`Invalid choice "${key}", defaulting to RSC.`));
65
+ resolve({ pro: false, rsc: true });
66
+ return;
67
+ }
68
+ resolve({ pro: selected.pro, rsc: selected.rsc });
69
+ });
70
+ });
71
+ }
72
+ //# sourceMappingURL=prompt.js.map
package/lib/utils.d.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  * - Callers must still ensure `command` and `args` come from trusted/validated values.
7
7
  */
8
8
  export declare function execLiveArgs(command: string, args: string[], cwd?: string, env?: NodeJS.ProcessEnv): void;
9
+ export declare function execCaptureArgs(command: string, args: string[], cwd?: string, env?: NodeJS.ProcessEnv): string;
9
10
  export declare function getCommandVersion(command: string): string | null;
10
11
  export declare function detectPackageManager(): 'npm' | 'pnpm' | null;
11
12
  export declare function logStep(current: number, total: number, message: string): void;
package/lib/utils.js CHANGED
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.execLiveArgs = execLiveArgs;
7
+ exports.execCaptureArgs = execCaptureArgs;
7
8
  exports.getCommandVersion = getCommandVersion;
8
9
  exports.detectPackageManager = detectPackageManager;
9
10
  exports.logStep = logStep;
@@ -13,6 +14,10 @@ exports.logSuccess = logSuccess;
13
14
  exports.logInfo = logInfo;
14
15
  const child_process_1 = require("child_process");
15
16
  const chalk_1 = __importDefault(require("chalk"));
17
+ function childEnv(env) {
18
+ // Always inherit PATH, HOME, and the rest of process.env; callers only add/override keys.
19
+ return env ? { ...process.env, ...env } : process.env;
20
+ }
16
21
  /**
17
22
  * Execute a command and stream output to the current terminal.
18
23
  *
@@ -24,15 +29,39 @@ function execLiveArgs(command, args, cwd, env) {
24
29
  const result = (0, child_process_1.spawnSync)(command, args, {
25
30
  stdio: 'inherit',
26
31
  cwd,
27
- ...(env ? { env } : {}),
32
+ env: childEnv(env),
28
33
  });
29
34
  if (result.error) {
30
35
  throw result.error;
31
36
  }
37
+ if (result.status === null) {
38
+ throw new Error(`Command "${command}" was terminated by ${result.signal ?? 'unknown signal'}`);
39
+ }
32
40
  if (result.status !== 0) {
33
41
  throw new Error(`Command "${command}" exited with code ${result.status}`);
34
42
  }
35
43
  }
44
+ function execCaptureArgs(command, args, cwd, env) {
45
+ const result = (0, child_process_1.spawnSync)(command, args, {
46
+ stdio: 'pipe',
47
+ encoding: 'utf8',
48
+ cwd,
49
+ env: childEnv(env),
50
+ });
51
+ if (result.error) {
52
+ throw result.error;
53
+ }
54
+ if (result.status === null) {
55
+ throw new Error(`Command "${command}" was terminated by ${result.signal ?? 'unknown signal'}`);
56
+ }
57
+ if (result.status !== 0) {
58
+ const stderr = result.stderr?.trim();
59
+ throw new Error(stderr && stderr.length > 0
60
+ ? `Command "${command}" exited with code ${result.status}: ${stderr}`
61
+ : `Command "${command}" exited with code ${result.status}`);
62
+ }
63
+ return result.stdout?.trim() ?? '';
64
+ }
36
65
  function getCommandVersion(command) {
37
66
  try {
38
67
  return (0, child_process_1.execFileSync)(command, ['--version'], { encoding: 'utf8', stdio: 'pipe' }).trim();
@@ -2,6 +2,7 @@ import { ValidationResult } from './types.js';
2
2
  export declare function validateNode(): ValidationResult;
3
3
  export declare function validateRuby(): ValidationResult;
4
4
  export declare function validateRails(): ValidationResult;
5
+ export declare function validateGit(): ValidationResult;
5
6
  export declare function validatePackageManager(pm: 'npm' | 'pnpm'): ValidationResult;
6
7
  export interface PrerequisiteResults {
7
8
  allValid: boolean;
package/lib/validators.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.validateNode = validateNode;
4
4
  exports.validateRuby = validateRuby;
5
5
  exports.validateRails = validateRails;
6
+ exports.validateGit = validateGit;
6
7
  exports.validatePackageManager = validatePackageManager;
7
8
  exports.validateAll = validateAll;
8
9
  const utils_js_1 = require("./utils.js");
@@ -85,6 +86,19 @@ function validateRails() {
85
86
  }
86
87
  return { valid: true, message: firstLine };
87
88
  }
89
+ function validateGit() {
90
+ const gitVersion = (0, utils_js_1.getCommandVersion)('git');
91
+ if (!gitVersion) {
92
+ return {
93
+ valid: false,
94
+ message: 'git is not installed or not found in PATH.\n\n' +
95
+ 'create-react-on-rails-app now records the generated app as a step-by-step git history.\n' +
96
+ 'Install git, then try again:\n' +
97
+ ' https://git-scm.com/downloads',
98
+ };
99
+ }
100
+ return { valid: true, message: gitVersion.split('\n')[0].trim() };
101
+ }
88
102
  function validatePackageManager(pm) {
89
103
  const version = (0, utils_js_1.getCommandVersion)(pm);
90
104
  if (!version) {
@@ -100,6 +114,7 @@ function validateAll(packageManager) {
100
114
  { name: 'Node.js', result: validateNode() },
101
115
  { name: 'Ruby', result: validateRuby() },
102
116
  { name: 'Rails', result: validateRails() },
117
+ { name: 'git', result: validateGit() },
103
118
  { name: 'Package Manager', result: validatePackageManager(packageManager) },
104
119
  ];
105
120
  const allValid = results.every((r) => r.result.valid);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-react-on-rails-app",
3
- "version": "16.5.1",
3
+ "version": "16.6.0-rc.1",
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"