Skip to content

Commit cfc5abe

Browse files
committed
feat: Enhance OpenNext.js CLI with new migration and template features
- Introduced a new `migrate` command to facilitate migration from Vercel and Netlify to OpenNext.js Cloudflare, including auto-detection of source platforms. - Added support for applying templates during project initialization, allowing users to select from various predefined templates for enhanced project setup. - Implemented a dry-run feature to preview changes without applying them, improving user experience and safety during operations. - Updated CLI configuration prompts to include theme selection and improved grouping of related prompts for better usability. - Refactored existing commands to utilize task management for sequential operations, enhancing the responsiveness and clarity of CLI actions. These enhancements significantly improve the functionality and user experience of the OpenNext.js CLI, making it easier for users to migrate and set up projects.
1 parent beeae8c commit cfc5abe

27 files changed

+2453
-523
lines changed

eslint.config.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,17 @@ export default tseslint.config(
3939
'@typescript-eslint/require-await': 'off',
4040
},
4141
},
42+
{
43+
// p.tasks() requires async functions, but some tasks may not use await
44+
files: [
45+
'packages/opennextjs-cli/src/commands/**/*.ts',
46+
],
47+
rules: {
48+
'@typescript-eslint/require-await': 'off',
49+
// Allow missing return types for task functions (they're inferred from p.tasks())
50+
'@typescript-eslint/explicit-function-return-type': 'off',
51+
},
52+
},
4253
{
4354
ignores: [
4455
'node_modules/**',

packages/opennextjs-cli/src/cli.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@ import { cloudflareCommand } from './commands/cloudflare.js';
2020
import { doctorCommand } from './commands/doctor.js';
2121
import { mcpCommand } from './commands/mcp.js';
2222
import { setupCommand } from './commands/setup.js';
23+
import { migrateCommand } from './commands/migrate.js';
24+
import { completionCommand } from './commands/completion.js';
2325
import { setLogLevel } from './utils/logger.js';
2426
import { getMergedConfig } from './utils/config-manager.js';
27+
import { applyTheme } from './utils/theme.js';
2528

2629
/**
2730
* Main CLI program instance
@@ -51,9 +54,14 @@ program
5154
.hook('preAction', (thisCommand) => {
5255
const opts = thisCommand.opts();
5356

54-
// Load config for default verbose setting
57+
// Load config for default settings
5558
const config = getMergedConfig();
5659

60+
// Apply theme from config
61+
if (config.theme) {
62+
applyTheme(config.theme);
63+
}
64+
5765
// Set log level from flags or config
5866
if (opts['debug']) {
5967
setLogLevel('debug');
@@ -96,4 +104,6 @@ program.addCommand(envCommand());
96104
program.addCommand(cloudflareCommand());
97105
program.addCommand(doctorCommand());
98106
program.addCommand(mcpCommand());
99-
program.addCommand(setupCommand());
107+
program.addCommand(setupCommand());
108+
program.addCommand(migrateCommand());
109+
program.addCommand(completionCommand());

packages/opennextjs-cli/src/commands/add.ts

Lines changed: 72 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { performSafetyChecks, isSafeToWrite } from '../utils/safety.js';
2121
import { detectMonorepo, isInMonorepo } from '../utils/monorepo-detector.js';
2222
import { getRollbackManager } from '../utils/rollback.js';
2323
import { getMergedConfig } from '../utils/config-manager.js';
24+
import { createDryRunManager } from '../utils/dry-run.js';
2425

2526
/**
2627
* Creates the `add` command for adding OpenNext to existing projects
@@ -63,6 +64,10 @@ export function addCommand(): Command {
6364
'--skip-backup',
6465
'Skip creating backups of existing configuration files'
6566
)
67+
.option(
68+
'--dry-run',
69+
'Preview changes without applying them'
70+
)
6671
.addHelpText(
6772
'after',
6873
`
@@ -103,17 +108,25 @@ Troubleshooting:
103108
• Verify Node.js version is 18+ (check with: node --version)
104109
`
105110
)
106-
.action(async (_options: {
111+
.action(async (options: {
107112
yes?: boolean;
108113
workerName?: string;
109114
cachingStrategy?: string;
110115
skipCloudflareCheck?: boolean;
111116
skipBackup?: boolean;
117+
dryRun?: boolean;
112118
}) => {
113119
const rollbackManager = getRollbackManager();
114120
const projectRoot = process.cwd();
121+
const dryRun = options.dryRun || false;
122+
const dryRunManager = createDryRunManager(dryRun);
115123

116124
try {
125+
if (dryRun) {
126+
p.intro('🔍 OpenNext.js CLI (Dry Run)');
127+
p.note('Running in dry-run mode. No files will be modified.', 'Dry Run Mode');
128+
}
129+
117130
// Safety checks
118131
logger.section('Safety Checks');
119132
const safetyCheck = performSafetyChecks(projectRoot, 'add');
@@ -132,7 +145,7 @@ Troubleshooting:
132145
logger.warning(` • ${warning}`);
133146
}
134147

135-
if (!_options.yes) {
148+
if (!options.yes) {
136149
const continueAnyway = await promptConfirmation(
137150
'Continue despite warnings?',
138151
false
@@ -151,7 +164,7 @@ Troubleshooting:
151164
p.log.info(`Type: ${monorepo.type}`);
152165
p.log.info(`Root: ${monorepo.rootPath}`);
153166

154-
if (!_options.yes) {
167+
if (!options.yes) {
155168
const confirmed = await promptConfirmation(
156169
`You're in a ${monorepo.type} monorepo. Continue with setup in this package?`,
157170
true
@@ -165,8 +178,8 @@ Troubleshooting:
165178

166179
// Load CLI configuration
167180
const cliConfig = getMergedConfig(projectRoot);
168-
if (cliConfig.autoBackup === false && !_options.skipBackup) {
169-
_options.skipBackup = true;
181+
if (cliConfig.autoBackup === false && !options.skipBackup) {
182+
options.skipBackup = true;
170183
}
171184

172185
logger.section('Project Detection');
@@ -226,7 +239,7 @@ Troubleshooting:
226239
}
227240

228241
// Backup existing files
229-
if (!_options.skipBackup) {
242+
if (!options.skipBackup) {
230243
const filesToBackup = ['wrangler.toml', 'open-next.config.ts', 'next.config.mjs'];
231244
const backups = backupFiles(filesToBackup, join(projectRoot, '.backup'));
232245
if (backups.some((b) => b !== undefined)) {
@@ -248,31 +261,65 @@ Troubleshooting:
248261

249262
logger.section('Setup');
250263

251-
// Generate configuration files
252-
const configSpinner = p.spinner();
253-
configSpinner.start('Generating configuration files...');
254-
await generateCloudflareConfig(config, process.cwd());
255-
configSpinner.stop('Configuration files generated');
264+
// Track files that would be created
265+
if (dryRun) {
266+
dryRunManager.track({ type: 'create', path: 'open-next.config.ts' });
267+
dryRunManager.track({ type: 'create', path: 'wrangler.toml' });
268+
dryRunManager.track({ type: 'update', path: 'next.config.mjs' });
269+
dryRunManager.track({ type: 'update', path: 'package.json' });
270+
dryRunManager.track({ type: 'create', path: 'scripts/patch-nextjs-source.js' });
271+
dryRunManager.track({ type: 'create', path: 'scripts/patch-init.js' });
272+
273+
dryRunManager.displayPreview();
274+
275+
p.note(
276+
'Dependencies that would be installed:\n' +
277+
' • @opennextjs/cloudflare\n' +
278+
' • wrangler (dev)',
279+
'Dry Run Preview'
280+
);
281+
282+
p.outro('Dry run complete. Run without --dry-run to apply changes.');
283+
return;
284+
}
285+
286+
// Use tasks() for sequential operations
287+
const tasks = [
288+
{
289+
title: 'Generating configuration files',
290+
task: async () => {
291+
await generateCloudflareConfig(config, process.cwd());
292+
},
293+
},
294+
];
256295

257296
// Install OpenNext.js Cloudflare if not already installed
258297
if (!detection.hasOpenNext) {
259-
const installSpinner = p.spinner();
260-
installSpinner.start('Installing @opennextjs/cloudflare...');
261-
addDependency('@opennextjs/cloudflare', false);
262-
installSpinner.stop('@opennextjs/cloudflare installed');
298+
tasks.push({
299+
title: 'Installing @opennextjs/cloudflare',
300+
task: async () => {
301+
addDependency('@opennextjs/cloudflare', false);
302+
},
303+
});
263304
}
264305

265-
// Install wrangler as dev dependency
266-
const wranglerSpinner = p.spinner();
267-
wranglerSpinner.start('Installing wrangler...');
268-
addDependency('wrangler', true);
269-
wranglerSpinner.stop('wrangler installed');
306+
// Always install wrangler
307+
tasks.push(
308+
{
309+
title: 'Installing wrangler',
310+
task: async () => {
311+
addDependency('wrangler', true);
312+
},
313+
},
314+
{
315+
title: 'Installing dependencies',
316+
task: async () => {
317+
installDependencies();
318+
},
319+
}
320+
);
270321

271-
// Install all dependencies
272-
const depsSpinner = p.spinner();
273-
depsSpinner.start('Installing dependencies...');
274-
installDependencies();
275-
depsSpinner.stop('Dependencies installed');
322+
await p.tasks(tasks);
276323

277324
logger.success('OpenNext.js Cloudflare has been added to your project!');
278325
p.note(

0 commit comments

Comments
 (0)