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 +257 -14
- package/lib/index.d.ts +2 -1
- package/lib/index.js +58 -20
- package/lib/prompt.d.ts +7 -0
- package/lib/prompt.js +72 -0
- package/lib/utils.d.ts +1 -0
- package/lib/utils.js +30 -1
- package/lib/validators.d.ts +1 -0
- package/lib/validators.js +15 -0
- package/package.json +1 -1
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
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
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
|
|
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)(
|
|
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 (
|
|
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,
|
|
526
|
+
printSuccessMessage(appName, educationalCommitsEnabled);
|
|
284
527
|
}
|
|
285
528
|
//# sourceMappingURL=create-app.js.map
|
package/lib/index.d.ts
CHANGED
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
|
-
|
|
31
|
-
|
|
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('--
|
|
97
|
-
.option('--
|
|
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
|
|
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
|
|
116
|
-
-
|
|
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
|
-
|
|
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
|
package/lib/prompt.d.ts
ADDED
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
|
-
|
|
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();
|
package/lib/validators.d.ts
CHANGED
|
@@ -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