@djangocfg/nextjs 1.0.6 → 2.1.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/package.json +12 -8
- package/src/config/constants.ts +55 -0
- package/src/config/createNextConfig.ts +209 -0
- package/src/config/index.ts +86 -1
- package/src/config/packages/checker.ts +94 -0
- package/src/config/packages/definitions.ts +67 -0
- package/src/config/packages/index.ts +27 -0
- package/src/config/packages/installer.ts +457 -0
- package/src/config/packages/updater.ts +469 -0
- package/src/config/plugins/compression.ts +97 -0
- package/src/config/plugins/devStartup.ts +123 -0
- package/src/config/plugins/index.ts +6 -0
- package/src/config/utils/deepMerge.ts +33 -0
- package/src/config/utils/env.ts +30 -0
- package/src/config/utils/index.ts +7 -0
- package/src/config/utils/version.ts +121 -0
- package/src/og-image/utils/metadata.ts +1 -2
- package/src/og-image/utils/url.ts +57 -44
- package/src/config/base-next-config.ts +0 -303
- package/src/config/deepMerge.ts +0 -33
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package Installer
|
|
3
|
+
*
|
|
4
|
+
* Provides auto-installation functionality with user confirmation and progress indication.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execSync, spawn } from 'child_process';
|
|
8
|
+
import { createInterface } from 'readline';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import consola from 'consola';
|
|
11
|
+
import Conf from 'conf';
|
|
12
|
+
import type { MissingPackage } from './checker';
|
|
13
|
+
import { getMissingPackages } from './checker';
|
|
14
|
+
import { isCI } from '../utils/env';
|
|
15
|
+
|
|
16
|
+
// Installer preferences cache
|
|
17
|
+
const installerCache = new Conf<{
|
|
18
|
+
autoInstall?: boolean;
|
|
19
|
+
skipPackages?: string[];
|
|
20
|
+
lastPrompt?: number;
|
|
21
|
+
}>({
|
|
22
|
+
projectName: 'djangocfg-nextjs-installer',
|
|
23
|
+
projectVersion: '1.0.0',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Don't prompt more than once per hour
|
|
27
|
+
const PROMPT_COOLDOWN_MS = 60 * 60 * 1000;
|
|
28
|
+
|
|
29
|
+
// Spinner frames for progress
|
|
30
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
31
|
+
|
|
32
|
+
export interface InstallOptions {
|
|
33
|
+
/** Auto-install without prompting */
|
|
34
|
+
autoInstall?: boolean;
|
|
35
|
+
/** Skip specific packages */
|
|
36
|
+
skipPackages?: string[];
|
|
37
|
+
/** Force prompt even if recently prompted */
|
|
38
|
+
force?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface InstallProgress {
|
|
42
|
+
current: number;
|
|
43
|
+
total: number;
|
|
44
|
+
package: string;
|
|
45
|
+
status: 'pending' | 'installing' | 'done' | 'error';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Detect package manager
|
|
50
|
+
*/
|
|
51
|
+
export function detectPackageManager(): 'pnpm' | 'yarn' | 'npm' {
|
|
52
|
+
try {
|
|
53
|
+
// Check for lockfiles
|
|
54
|
+
const fs = require('fs');
|
|
55
|
+
const path = require('path');
|
|
56
|
+
const cwd = process.cwd();
|
|
57
|
+
|
|
58
|
+
if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
59
|
+
if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return 'yarn';
|
|
60
|
+
if (fs.existsSync(path.join(cwd, 'package-lock.json'))) return 'npm';
|
|
61
|
+
|
|
62
|
+
// Check for global package manager
|
|
63
|
+
try {
|
|
64
|
+
execSync('pnpm --version', { stdio: 'ignore' });
|
|
65
|
+
return 'pnpm';
|
|
66
|
+
} catch {
|
|
67
|
+
try {
|
|
68
|
+
execSync('yarn --version', { stdio: 'ignore' });
|
|
69
|
+
return 'yarn';
|
|
70
|
+
} catch {
|
|
71
|
+
return 'npm';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
return 'npm';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Build install command for a single package
|
|
81
|
+
*/
|
|
82
|
+
export function buildSingleInstallCommand(
|
|
83
|
+
packageName: string,
|
|
84
|
+
isDev: boolean,
|
|
85
|
+
pm: 'pnpm' | 'yarn' | 'npm'
|
|
86
|
+
): string {
|
|
87
|
+
const devFlag = isDev ? '-D ' : '';
|
|
88
|
+
switch (pm) {
|
|
89
|
+
case 'pnpm':
|
|
90
|
+
return `pnpm add ${devFlag}${packageName}`;
|
|
91
|
+
case 'yarn':
|
|
92
|
+
return `yarn add ${devFlag}${packageName}`;
|
|
93
|
+
case 'npm':
|
|
94
|
+
return `npm install ${devFlag}${packageName}`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build install command for multiple packages (for display)
|
|
100
|
+
*/
|
|
101
|
+
export function buildInstallCommand(packages: MissingPackage[], pm: 'pnpm' | 'yarn' | 'npm'): string {
|
|
102
|
+
const devPackages = packages.filter(p => p.devDependency).map(p => p.name);
|
|
103
|
+
const prodPackages = packages.filter(p => !p.devDependency).map(p => p.name);
|
|
104
|
+
|
|
105
|
+
const commands: string[] = [];
|
|
106
|
+
|
|
107
|
+
if (devPackages.length > 0) {
|
|
108
|
+
switch (pm) {
|
|
109
|
+
case 'pnpm':
|
|
110
|
+
commands.push(`pnpm add -D ${devPackages.join(' ')}`);
|
|
111
|
+
break;
|
|
112
|
+
case 'yarn':
|
|
113
|
+
commands.push(`yarn add -D ${devPackages.join(' ')}`);
|
|
114
|
+
break;
|
|
115
|
+
case 'npm':
|
|
116
|
+
commands.push(`npm install -D ${devPackages.join(' ')}`);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (prodPackages.length > 0) {
|
|
122
|
+
switch (pm) {
|
|
123
|
+
case 'pnpm':
|
|
124
|
+
commands.push(`pnpm add ${prodPackages.join(' ')}`);
|
|
125
|
+
break;
|
|
126
|
+
case 'yarn':
|
|
127
|
+
commands.push(`yarn add ${prodPackages.join(' ')}`);
|
|
128
|
+
break;
|
|
129
|
+
case 'npm':
|
|
130
|
+
commands.push(`npm install ${prodPackages.join(' ')}`);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return commands.join(' && ');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Ask user for confirmation via readline
|
|
140
|
+
*/
|
|
141
|
+
async function askConfirmation(question: string): Promise<boolean> {
|
|
142
|
+
// Skip prompt in CI or non-TTY environments
|
|
143
|
+
if (isCI || !process.stdin.isTTY) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return new Promise((resolve) => {
|
|
148
|
+
const rl = createInterface({
|
|
149
|
+
input: process.stdin,
|
|
150
|
+
output: process.stdout,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
rl.question(question, (answer) => {
|
|
154
|
+
rl.close();
|
|
155
|
+
const normalized = answer.toLowerCase().trim();
|
|
156
|
+
resolve(normalized === '' || normalized === 'y' || normalized === 'yes');
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Create a simple spinner
|
|
163
|
+
*/
|
|
164
|
+
function createSpinner(text: string) {
|
|
165
|
+
let frameIndex = 0;
|
|
166
|
+
let interval: NodeJS.Timeout | null = null;
|
|
167
|
+
let currentText = text;
|
|
168
|
+
|
|
169
|
+
const render = () => {
|
|
170
|
+
const frame = SPINNER_FRAMES[frameIndex];
|
|
171
|
+
process.stdout.write(`\r${chalk.cyan(frame)} ${currentText}`);
|
|
172
|
+
frameIndex = (frameIndex + 1) % SPINNER_FRAMES.length;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
start() {
|
|
177
|
+
if (process.stdout.isTTY) {
|
|
178
|
+
interval = setInterval(render, 80);
|
|
179
|
+
render();
|
|
180
|
+
} else {
|
|
181
|
+
console.log(` ${currentText}`);
|
|
182
|
+
}
|
|
183
|
+
return this;
|
|
184
|
+
},
|
|
185
|
+
text(newText: string) {
|
|
186
|
+
currentText = newText;
|
|
187
|
+
if (!process.stdout.isTTY) {
|
|
188
|
+
console.log(` ${newText}`);
|
|
189
|
+
}
|
|
190
|
+
return this;
|
|
191
|
+
},
|
|
192
|
+
succeed(text?: string) {
|
|
193
|
+
if (interval) clearInterval(interval);
|
|
194
|
+
if (process.stdout.isTTY) {
|
|
195
|
+
process.stdout.write(`\r${chalk.green('✓')} ${text || currentText}\n`);
|
|
196
|
+
} else {
|
|
197
|
+
console.log(` ${chalk.green('✓')} ${text || currentText}`);
|
|
198
|
+
}
|
|
199
|
+
return this;
|
|
200
|
+
},
|
|
201
|
+
fail(text?: string) {
|
|
202
|
+
if (interval) clearInterval(interval);
|
|
203
|
+
if (process.stdout.isTTY) {
|
|
204
|
+
process.stdout.write(`\r${chalk.red('✗')} ${text || currentText}\n`);
|
|
205
|
+
} else {
|
|
206
|
+
console.log(` ${chalk.red('✗')} ${text || currentText}`);
|
|
207
|
+
}
|
|
208
|
+
return this;
|
|
209
|
+
},
|
|
210
|
+
stop() {
|
|
211
|
+
if (interval) clearInterval(interval);
|
|
212
|
+
if (process.stdout.isTTY) {
|
|
213
|
+
process.stdout.write('\r\x1b[K'); // Clear line
|
|
214
|
+
}
|
|
215
|
+
return this;
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Install a single package with progress
|
|
222
|
+
*/
|
|
223
|
+
async function installSinglePackage(
|
|
224
|
+
pkg: MissingPackage,
|
|
225
|
+
pm: 'pnpm' | 'yarn' | 'npm',
|
|
226
|
+
index: number,
|
|
227
|
+
total: number
|
|
228
|
+
): Promise<boolean> {
|
|
229
|
+
const command = buildSingleInstallCommand(pkg.name, pkg.devDependency, pm);
|
|
230
|
+
const progress = `[${index + 1}/${total}]`;
|
|
231
|
+
const spinner = createSpinner(`${chalk.dim(progress)} Installing ${chalk.cyan(pkg.name)}...`);
|
|
232
|
+
|
|
233
|
+
spinner.start();
|
|
234
|
+
|
|
235
|
+
return new Promise((resolve) => {
|
|
236
|
+
const [cmd, ...args] = command.split(' ');
|
|
237
|
+
const proc = spawn(cmd, args, {
|
|
238
|
+
shell: true,
|
|
239
|
+
cwd: process.cwd(),
|
|
240
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
let stderr = '';
|
|
244
|
+
|
|
245
|
+
proc.stderr?.on('data', (data) => {
|
|
246
|
+
stderr += data.toString();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
proc.on('close', (code) => {
|
|
250
|
+
if (code === 0) {
|
|
251
|
+
spinner.succeed(`${chalk.dim(progress)} ${chalk.cyan(pkg.name)} ${chalk.green('installed')}`);
|
|
252
|
+
resolve(true);
|
|
253
|
+
} else {
|
|
254
|
+
spinner.fail(`${chalk.dim(progress)} ${chalk.cyan(pkg.name)} ${chalk.red('failed')}`);
|
|
255
|
+
if (stderr) {
|
|
256
|
+
console.log(chalk.dim(` ${stderr.split('\n')[0]}`));
|
|
257
|
+
}
|
|
258
|
+
resolve(false);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
proc.on('error', () => {
|
|
263
|
+
spinner.fail(`${chalk.dim(progress)} ${chalk.cyan(pkg.name)} ${chalk.red('failed')}`);
|
|
264
|
+
resolve(false);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Install packages with progress indication
|
|
271
|
+
*/
|
|
272
|
+
export async function installPackagesWithProgress(packages: MissingPackage[]): Promise<boolean> {
|
|
273
|
+
if (packages.length === 0) return true;
|
|
274
|
+
|
|
275
|
+
const pm = detectPackageManager();
|
|
276
|
+
const total = packages.length;
|
|
277
|
+
|
|
278
|
+
console.log('');
|
|
279
|
+
console.log(chalk.bold(` Installing ${total} package${total > 1 ? 's' : ''}...`));
|
|
280
|
+
console.log('');
|
|
281
|
+
|
|
282
|
+
let successCount = 0;
|
|
283
|
+
let failedPackages: string[] = [];
|
|
284
|
+
|
|
285
|
+
for (let i = 0; i < packages.length; i++) {
|
|
286
|
+
const success = await installSinglePackage(packages[i], pm, i, total);
|
|
287
|
+
if (success) {
|
|
288
|
+
successCount++;
|
|
289
|
+
} else {
|
|
290
|
+
failedPackages.push(packages[i].name);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
console.log('');
|
|
295
|
+
|
|
296
|
+
if (failedPackages.length === 0) {
|
|
297
|
+
consola.success(`All ${total} packages installed successfully!`);
|
|
298
|
+
return true;
|
|
299
|
+
} else if (successCount > 0) {
|
|
300
|
+
consola.warn(`${successCount}/${total} packages installed. Failed: ${failedPackages.join(', ')}`);
|
|
301
|
+
return false;
|
|
302
|
+
} else {
|
|
303
|
+
consola.error(`Failed to install packages: ${failedPackages.join(', ')}`);
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Install packages (simple version without per-package progress)
|
|
310
|
+
*/
|
|
311
|
+
export async function installPackages(packages: MissingPackage[]): Promise<boolean> {
|
|
312
|
+
// Use progress version for multiple packages in TTY
|
|
313
|
+
if (packages.length > 1 && process.stdout.isTTY) {
|
|
314
|
+
return installPackagesWithProgress(packages);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Simple installation for single package or non-TTY
|
|
318
|
+
if (packages.length === 0) return true;
|
|
319
|
+
|
|
320
|
+
const pm = detectPackageManager();
|
|
321
|
+
const command = buildInstallCommand(packages, pm);
|
|
322
|
+
|
|
323
|
+
consola.info(`Installing: ${chalk.cyan(packages.map(p => p.name).join(', '))}`);
|
|
324
|
+
|
|
325
|
+
const spinner = createSpinner('Installing packages...');
|
|
326
|
+
spinner.start();
|
|
327
|
+
|
|
328
|
+
return new Promise((resolve) => {
|
|
329
|
+
const proc = spawn(command, {
|
|
330
|
+
shell: true,
|
|
331
|
+
cwd: process.cwd(),
|
|
332
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
proc.on('close', (code) => {
|
|
336
|
+
if (code === 0) {
|
|
337
|
+
spinner.succeed('Packages installed successfully!');
|
|
338
|
+
resolve(true);
|
|
339
|
+
} else {
|
|
340
|
+
spinner.fail('Failed to install packages');
|
|
341
|
+
resolve(false);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
proc.on('error', () => {
|
|
346
|
+
spinner.fail('Installation failed');
|
|
347
|
+
resolve(false);
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Check and prompt for missing packages
|
|
354
|
+
*
|
|
355
|
+
* Returns true if all packages are available (either already installed or just installed)
|
|
356
|
+
*/
|
|
357
|
+
export async function checkAndInstallPackages(options: InstallOptions = {}): Promise<boolean> {
|
|
358
|
+
const missing = getMissingPackages();
|
|
359
|
+
|
|
360
|
+
// Filter out skipped packages
|
|
361
|
+
const skipList = options.skipPackages || installerCache.get('skipPackages') || [];
|
|
362
|
+
const toInstall = missing.filter(p => !skipList.includes(p.name));
|
|
363
|
+
|
|
364
|
+
if (toInstall.length === 0) {
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Check cooldown (don't prompt too often)
|
|
369
|
+
const lastPrompt = installerCache.get('lastPrompt') || 0;
|
|
370
|
+
if (!options.force && (Date.now() - lastPrompt) < PROMPT_COOLDOWN_MS) {
|
|
371
|
+
// Show info but don't prompt
|
|
372
|
+
printMissingPackagesInfo(toInstall);
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Auto-install if configured
|
|
377
|
+
if (options.autoInstall || installerCache.get('autoInstall')) {
|
|
378
|
+
return installPackages(toInstall);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Show missing packages
|
|
382
|
+
console.log('');
|
|
383
|
+
consola.box('📦 Missing Optional Packages');
|
|
384
|
+
console.log('');
|
|
385
|
+
|
|
386
|
+
for (const pkg of toInstall) {
|
|
387
|
+
console.log(` ${chalk.yellow('•')} ${chalk.bold(pkg.name)}`);
|
|
388
|
+
console.log(` ${chalk.dim(pkg.description)}`);
|
|
389
|
+
console.log(` ${chalk.dim(pkg.reason)}`);
|
|
390
|
+
console.log('');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Build install command for display
|
|
394
|
+
const pm = detectPackageManager();
|
|
395
|
+
const command = buildInstallCommand(toInstall, pm);
|
|
396
|
+
|
|
397
|
+
console.log(` ${chalk.cyan('Command:')} ${command}`);
|
|
398
|
+
console.log('');
|
|
399
|
+
|
|
400
|
+
// Ask for confirmation
|
|
401
|
+
installerCache.set('lastPrompt', Date.now());
|
|
402
|
+
|
|
403
|
+
const shouldInstall = await askConfirmation(
|
|
404
|
+
`${chalk.green('?')} Install these packages now? ${chalk.dim('[Y/n]')} `
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
if (shouldInstall) {
|
|
408
|
+
const success = await installPackages(toInstall);
|
|
409
|
+
|
|
410
|
+
// Ask if user wants to enable auto-install for future
|
|
411
|
+
if (success) {
|
|
412
|
+
const enableAuto = await askConfirmation(
|
|
413
|
+
`${chalk.green('?')} Enable auto-install for future? ${chalk.dim('[y/N]')} `
|
|
414
|
+
);
|
|
415
|
+
if (enableAuto) {
|
|
416
|
+
installerCache.set('autoInstall', true);
|
|
417
|
+
consola.info('Auto-install enabled. Run with --no-auto-install to disable.');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return success;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// User declined, ask if they want to skip these packages permanently
|
|
425
|
+
const skipPermanently = await askConfirmation(
|
|
426
|
+
`${chalk.green('?')} Skip these packages in future prompts? ${chalk.dim('[y/N]')} `
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
if (skipPermanently) {
|
|
430
|
+
const currentSkip = installerCache.get('skipPackages') || [];
|
|
431
|
+
installerCache.set('skipPackages', [...currentSkip, ...toInstall.map(p => p.name)]);
|
|
432
|
+
consola.info('Packages added to skip list.');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Print info about missing packages without prompting
|
|
440
|
+
*/
|
|
441
|
+
function printMissingPackagesInfo(packages: MissingPackage[]): void {
|
|
442
|
+
if (packages.length === 0) return;
|
|
443
|
+
|
|
444
|
+
const pm = detectPackageManager();
|
|
445
|
+
const command = buildInstallCommand(packages, pm);
|
|
446
|
+
|
|
447
|
+
consola.warn(`Missing optional packages: ${packages.map(p => p.name).join(', ')}`);
|
|
448
|
+
consola.info(`Install with: ${chalk.cyan(command)}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Reset installer preferences
|
|
453
|
+
*/
|
|
454
|
+
export function resetInstallerPreferences(): void {
|
|
455
|
+
installerCache.clear();
|
|
456
|
+
consola.success('Installer preferences reset');
|
|
457
|
+
}
|