@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,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package Updater
|
|
3
|
+
*
|
|
4
|
+
* Checks for outdated @djangocfg/* packages and offers to update them.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
import { createInterface } from 'readline';
|
|
9
|
+
import { createRequire } from 'module';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import consola from 'consola';
|
|
13
|
+
import Conf from 'conf';
|
|
14
|
+
import semver from 'semver';
|
|
15
|
+
import { DJANGOCFG_PACKAGES, PACKAGE_NAME } from '../constants';
|
|
16
|
+
import { isCI } from '../utils/env';
|
|
17
|
+
import { detectPackageManager } from './installer';
|
|
18
|
+
|
|
19
|
+
// Updater preferences cache
|
|
20
|
+
const updaterCache = new Conf<{
|
|
21
|
+
autoUpdate?: boolean;
|
|
22
|
+
lastCheck?: number;
|
|
23
|
+
skippedVersions?: Record<string, string>;
|
|
24
|
+
}>({
|
|
25
|
+
projectName: 'djangocfg-nextjs-updater',
|
|
26
|
+
projectVersion: '1.0.0',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Check for updates once per hour
|
|
30
|
+
const UPDATE_CHECK_COOLDOWN_MS = 60 * 60 * 1000;
|
|
31
|
+
|
|
32
|
+
// Spinner frames
|
|
33
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
34
|
+
|
|
35
|
+
export interface PackageVersion {
|
|
36
|
+
name: string;
|
|
37
|
+
current: string | null;
|
|
38
|
+
latest: string | null;
|
|
39
|
+
hasUpdate: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface UpdateOptions {
|
|
43
|
+
/** Auto-update without prompting */
|
|
44
|
+
autoUpdate?: boolean;
|
|
45
|
+
/** Force check even if recently checked (ignores cooldown) */
|
|
46
|
+
force?: boolean;
|
|
47
|
+
/** Force check even for workspace:* packages (for testing) */
|
|
48
|
+
forceCheckWorkspace?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if package is a workspace dependency
|
|
53
|
+
*/
|
|
54
|
+
function isWorkspacePackage(packageName: string): boolean {
|
|
55
|
+
try {
|
|
56
|
+
const fs = require('fs');
|
|
57
|
+
const pkgJsonPath = join(process.cwd(), 'package.json');
|
|
58
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
59
|
+
|
|
60
|
+
const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
|
|
61
|
+
const version = deps[packageName];
|
|
62
|
+
|
|
63
|
+
return version?.startsWith('workspace:') || false;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get installed version of a package from consumer's project
|
|
71
|
+
* Uses multiple fallback strategies for monorepo compatibility
|
|
72
|
+
*/
|
|
73
|
+
export function getInstalledVersion(packageName: string): string | null {
|
|
74
|
+
const fs = require('fs');
|
|
75
|
+
const cwd = process.cwd();
|
|
76
|
+
|
|
77
|
+
// Strategy 1: Direct read from node_modules (works with symlinks)
|
|
78
|
+
try {
|
|
79
|
+
const directPath = join(cwd, 'node_modules', packageName, 'package.json');
|
|
80
|
+
if (fs.existsSync(directPath)) {
|
|
81
|
+
const pkg = JSON.parse(fs.readFileSync(directPath, 'utf-8'));
|
|
82
|
+
return pkg.version || null;
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// Continue to next strategy
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Strategy 2: Use createRequire from cwd
|
|
89
|
+
try {
|
|
90
|
+
const consumerRequire = createRequire(join(cwd, 'package.json'));
|
|
91
|
+
const pkgPath = consumerRequire.resolve(`${packageName}/package.json`);
|
|
92
|
+
const pkg = require(pkgPath);
|
|
93
|
+
return pkg.version || null;
|
|
94
|
+
} catch {
|
|
95
|
+
// Continue to next strategy
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Strategy 3: Try global require
|
|
99
|
+
try {
|
|
100
|
+
const pkgPath = require.resolve(`${packageName}/package.json`);
|
|
101
|
+
const pkg = require(pkgPath);
|
|
102
|
+
return pkg.version || null;
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if we should skip update checking for this package
|
|
110
|
+
* (e.g., workspace packages shouldn't be checked against npm)
|
|
111
|
+
*/
|
|
112
|
+
export function shouldCheckUpdates(packageName: string): boolean {
|
|
113
|
+
return !isWorkspacePackage(packageName);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Fetch latest version from npm registry
|
|
118
|
+
*/
|
|
119
|
+
async function fetchLatestVersion(packageName: string): Promise<string | null> {
|
|
120
|
+
try {
|
|
121
|
+
const https = await import('https');
|
|
122
|
+
return new Promise((resolve) => {
|
|
123
|
+
const req = https.get(
|
|
124
|
+
`https://registry.npmjs.org/${packageName}/latest`,
|
|
125
|
+
{ timeout: 5000 },
|
|
126
|
+
(res: any) => {
|
|
127
|
+
let data = '';
|
|
128
|
+
res.on('data', (chunk: string) => { data += chunk; });
|
|
129
|
+
res.on('end', () => {
|
|
130
|
+
try {
|
|
131
|
+
const json = JSON.parse(data);
|
|
132
|
+
resolve(json.version || null);
|
|
133
|
+
} catch {
|
|
134
|
+
resolve(null);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
req.on('error', () => resolve(null));
|
|
140
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
141
|
+
});
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Check all @djangocfg packages for updates
|
|
149
|
+
* All packages are aligned to the version of @djangocfg/nextjs (master package)
|
|
150
|
+
* Skips workspace:* packages unless forceCheckWorkspace is true
|
|
151
|
+
*/
|
|
152
|
+
export async function checkForUpdates(options: { forceCheckWorkspace?: boolean } = {}): Promise<PackageVersion[]> {
|
|
153
|
+
const results: PackageVersion[] = [];
|
|
154
|
+
|
|
155
|
+
// First, get the latest version of master package (@djangocfg/nextjs)
|
|
156
|
+
// All other packages should align to this version
|
|
157
|
+
const masterLatest = await fetchLatestVersion(PACKAGE_NAME);
|
|
158
|
+
if (!masterLatest) {
|
|
159
|
+
// Can't determine target version, skip update check
|
|
160
|
+
return results;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check packages against master version
|
|
164
|
+
const checks = DJANGOCFG_PACKAGES.map(async (name) => {
|
|
165
|
+
// Skip workspace packages unless force mode
|
|
166
|
+
const isWorkspace = !shouldCheckUpdates(name);
|
|
167
|
+
if (!options.forceCheckWorkspace && isWorkspace) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const current = getInstalledVersion(name);
|
|
172
|
+
if (!current) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// All packages should update to master package version
|
|
177
|
+
const hasUpdate = !!(masterLatest && current && semver.gt(masterLatest, current));
|
|
178
|
+
|
|
179
|
+
return { name, current, latest: masterLatest, hasUpdate };
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const checkResults = await Promise.all(checks);
|
|
183
|
+
|
|
184
|
+
for (const result of checkResults) {
|
|
185
|
+
if (result) {
|
|
186
|
+
results.push(result);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return results;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get packages that need updating
|
|
195
|
+
*/
|
|
196
|
+
export async function getOutdatedPackages(options: { forceCheckWorkspace?: boolean } = {}): Promise<PackageVersion[]> {
|
|
197
|
+
const all = await checkForUpdates(options);
|
|
198
|
+
const skipped = updaterCache.get('skippedVersions') || {};
|
|
199
|
+
|
|
200
|
+
return all.filter(pkg => {
|
|
201
|
+
if (!pkg.hasUpdate) return false;
|
|
202
|
+
// Skip if user chose to skip this specific version
|
|
203
|
+
if (skipped[pkg.name] === pkg.latest) return false;
|
|
204
|
+
return true;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Create a simple spinner
|
|
210
|
+
*/
|
|
211
|
+
function createSpinner(text: string) {
|
|
212
|
+
let frameIndex = 0;
|
|
213
|
+
let interval: NodeJS.Timeout | null = null;
|
|
214
|
+
let currentText = text;
|
|
215
|
+
|
|
216
|
+
const render = () => {
|
|
217
|
+
const frame = SPINNER_FRAMES[frameIndex];
|
|
218
|
+
process.stdout.write(`\r${chalk.cyan(frame)} ${currentText}`);
|
|
219
|
+
frameIndex = (frameIndex + 1) % SPINNER_FRAMES.length;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
start() {
|
|
224
|
+
if (process.stdout.isTTY) {
|
|
225
|
+
interval = setInterval(render, 80);
|
|
226
|
+
render();
|
|
227
|
+
} else {
|
|
228
|
+
console.log(` ${currentText}`);
|
|
229
|
+
}
|
|
230
|
+
return this;
|
|
231
|
+
},
|
|
232
|
+
text(newText: string) {
|
|
233
|
+
currentText = newText;
|
|
234
|
+
return this;
|
|
235
|
+
},
|
|
236
|
+
succeed(text?: string) {
|
|
237
|
+
if (interval) clearInterval(interval);
|
|
238
|
+
if (process.stdout.isTTY) {
|
|
239
|
+
process.stdout.write(`\r${chalk.green('✓')} ${text || currentText}\n`);
|
|
240
|
+
} else {
|
|
241
|
+
console.log(` ${chalk.green('✓')} ${text || currentText}`);
|
|
242
|
+
}
|
|
243
|
+
return this;
|
|
244
|
+
},
|
|
245
|
+
fail(text?: string) {
|
|
246
|
+
if (interval) clearInterval(interval);
|
|
247
|
+
if (process.stdout.isTTY) {
|
|
248
|
+
process.stdout.write(`\r${chalk.red('✗')} ${text || currentText}\n`);
|
|
249
|
+
} else {
|
|
250
|
+
console.log(` ${chalk.red('✗')} ${text || currentText}`);
|
|
251
|
+
}
|
|
252
|
+
return this;
|
|
253
|
+
},
|
|
254
|
+
stop() {
|
|
255
|
+
if (interval) clearInterval(interval);
|
|
256
|
+
if (process.stdout.isTTY) {
|
|
257
|
+
process.stdout.write('\r\x1b[K');
|
|
258
|
+
}
|
|
259
|
+
return this;
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Ask user for confirmation
|
|
266
|
+
*/
|
|
267
|
+
async function askConfirmation(question: string): Promise<boolean> {
|
|
268
|
+
if (isCI || !process.stdin.isTTY) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return new Promise((resolve) => {
|
|
273
|
+
const rl = createInterface({
|
|
274
|
+
input: process.stdin,
|
|
275
|
+
output: process.stdout,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
rl.question(question, (answer) => {
|
|
279
|
+
rl.close();
|
|
280
|
+
const normalized = answer.toLowerCase().trim();
|
|
281
|
+
resolve(normalized === '' || normalized === 'y' || normalized === 'yes');
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Update a single package with progress
|
|
288
|
+
*/
|
|
289
|
+
async function updateSinglePackage(
|
|
290
|
+
pkg: PackageVersion,
|
|
291
|
+
pm: 'pnpm' | 'yarn' | 'npm',
|
|
292
|
+
index: number,
|
|
293
|
+
total: number
|
|
294
|
+
): Promise<boolean> {
|
|
295
|
+
const progress = `[${index + 1}/${total}]`;
|
|
296
|
+
const versionInfo = `${chalk.red(pkg.current)} → ${chalk.green(pkg.latest)}`;
|
|
297
|
+
const spinner = createSpinner(
|
|
298
|
+
`${chalk.dim(progress)} Updating ${chalk.cyan(pkg.name)} ${versionInfo}`
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
spinner.start();
|
|
302
|
+
|
|
303
|
+
const command = pm === 'pnpm'
|
|
304
|
+
? `pnpm add ${pkg.name}@${pkg.latest}`
|
|
305
|
+
: pm === 'yarn'
|
|
306
|
+
? `yarn add ${pkg.name}@${pkg.latest}`
|
|
307
|
+
: `npm install ${pkg.name}@${pkg.latest}`;
|
|
308
|
+
|
|
309
|
+
return new Promise((resolve) => {
|
|
310
|
+
const proc = spawn(command, {
|
|
311
|
+
shell: true,
|
|
312
|
+
cwd: process.cwd(),
|
|
313
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
proc.on('close', (code) => {
|
|
317
|
+
if (code === 0) {
|
|
318
|
+
spinner.succeed(
|
|
319
|
+
`${chalk.dim(progress)} ${chalk.cyan(pkg.name)} ${chalk.green(pkg.latest!)}`
|
|
320
|
+
);
|
|
321
|
+
resolve(true);
|
|
322
|
+
} else {
|
|
323
|
+
spinner.fail(
|
|
324
|
+
`${chalk.dim(progress)} ${chalk.cyan(pkg.name)} ${chalk.red('failed')}`
|
|
325
|
+
);
|
|
326
|
+
resolve(false);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
proc.on('error', () => {
|
|
331
|
+
spinner.fail(`${chalk.dim(progress)} ${chalk.cyan(pkg.name)} ${chalk.red('failed')}`);
|
|
332
|
+
resolve(false);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Update packages with progress
|
|
339
|
+
*/
|
|
340
|
+
export async function updatePackagesWithProgress(packages: PackageVersion[]): Promise<boolean> {
|
|
341
|
+
if (packages.length === 0) return true;
|
|
342
|
+
|
|
343
|
+
const pm = detectPackageManager();
|
|
344
|
+
const total = packages.length;
|
|
345
|
+
|
|
346
|
+
console.log('');
|
|
347
|
+
console.log(chalk.bold(` Updating ${total} package${total > 1 ? 's' : ''}...`));
|
|
348
|
+
console.log('');
|
|
349
|
+
|
|
350
|
+
let successCount = 0;
|
|
351
|
+
const failedPackages: string[] = [];
|
|
352
|
+
|
|
353
|
+
for (let i = 0; i < packages.length; i++) {
|
|
354
|
+
const success = await updateSinglePackage(packages[i], pm, i, total);
|
|
355
|
+
if (success) {
|
|
356
|
+
successCount++;
|
|
357
|
+
} else {
|
|
358
|
+
failedPackages.push(packages[i].name);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
console.log('');
|
|
363
|
+
|
|
364
|
+
if (failedPackages.length === 0) {
|
|
365
|
+
consola.success(`All ${total} packages updated successfully!`);
|
|
366
|
+
return true;
|
|
367
|
+
} else if (successCount > 0) {
|
|
368
|
+
consola.warn(`${successCount}/${total} packages updated. Failed: ${failedPackages.join(', ')}`);
|
|
369
|
+
return false;
|
|
370
|
+
} else {
|
|
371
|
+
consola.error(`Failed to update packages: ${failedPackages.join(', ')}`);
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Check and prompt for package updates
|
|
378
|
+
*/
|
|
379
|
+
export async function checkAndUpdatePackages(options: UpdateOptions = {}): Promise<boolean> {
|
|
380
|
+
// Check cooldown
|
|
381
|
+
const lastCheck = updaterCache.get('lastCheck') || 0;
|
|
382
|
+
if (!options.force && (Date.now() - lastCheck) < UPDATE_CHECK_COOLDOWN_MS) {
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
updaterCache.set('lastCheck', Date.now());
|
|
387
|
+
|
|
388
|
+
// Check for updates
|
|
389
|
+
const spinner = createSpinner('Checking for updates...');
|
|
390
|
+
spinner.start();
|
|
391
|
+
|
|
392
|
+
const outdated = await getOutdatedPackages({
|
|
393
|
+
forceCheckWorkspace: options.forceCheckWorkspace,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
spinner.stop();
|
|
397
|
+
|
|
398
|
+
console.log(chalk.dim(` Found ${outdated.length} outdated package(s)`));
|
|
399
|
+
|
|
400
|
+
if (outdated.length === 0) {
|
|
401
|
+
console.log(chalk.green(' ✓ All packages are up to date'));
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Auto-update if configured
|
|
406
|
+
if (options.autoUpdate || updaterCache.get('autoUpdate')) {
|
|
407
|
+
return updatePackagesWithProgress(outdated);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Show outdated packages
|
|
411
|
+
console.log('');
|
|
412
|
+
consola.box('📦 Updates Available');
|
|
413
|
+
console.log('');
|
|
414
|
+
|
|
415
|
+
for (const pkg of outdated) {
|
|
416
|
+
console.log(
|
|
417
|
+
` ${chalk.yellow('•')} ${chalk.bold(pkg.name)} ` +
|
|
418
|
+
`${chalk.red(pkg.current)} → ${chalk.green(pkg.latest)}`
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
console.log('');
|
|
422
|
+
|
|
423
|
+
// Ask for confirmation
|
|
424
|
+
const shouldUpdate = await askConfirmation(
|
|
425
|
+
`${chalk.green('?')} Update these packages now? ${chalk.dim('[Y/n]')} `
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
if (shouldUpdate) {
|
|
429
|
+
const success = await updatePackagesWithProgress(outdated);
|
|
430
|
+
|
|
431
|
+
if (success) {
|
|
432
|
+
const enableAuto = await askConfirmation(
|
|
433
|
+
`${chalk.green('?')} Enable auto-update for future? ${chalk.dim('[y/N]')} `
|
|
434
|
+
);
|
|
435
|
+
if (enableAuto) {
|
|
436
|
+
updaterCache.set('autoUpdate', true);
|
|
437
|
+
consola.info('Auto-update enabled.');
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return success;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// User declined, ask if they want to skip these versions
|
|
445
|
+
const skipVersions = await askConfirmation(
|
|
446
|
+
`${chalk.green('?')} Skip these versions in future? ${chalk.dim('[y/N]')} `
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
if (skipVersions) {
|
|
450
|
+
const skipped = updaterCache.get('skippedVersions') || {};
|
|
451
|
+
for (const pkg of outdated) {
|
|
452
|
+
if (pkg.latest) {
|
|
453
|
+
skipped[pkg.name] = pkg.latest;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
updaterCache.set('skippedVersions', skipped);
|
|
457
|
+
consola.info('Versions skipped. Will prompt again for newer versions.');
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Reset updater preferences
|
|
465
|
+
*/
|
|
466
|
+
export function resetUpdaterPreferences(): void {
|
|
467
|
+
updaterCache.clear();
|
|
468
|
+
consola.success('Updater preferences reset');
|
|
469
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compression Plugin Setup
|
|
3
|
+
*
|
|
4
|
+
* Adds Gzip and Brotli compression for static builds.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Configuration as WebpackConfig } from 'webpack';
|
|
8
|
+
import { isPackageInstalled } from '../packages/checker';
|
|
9
|
+
|
|
10
|
+
export interface CompressionPluginOptions {
|
|
11
|
+
/** Enable gzip compression */
|
|
12
|
+
gzip?: boolean;
|
|
13
|
+
/** Enable brotli compression */
|
|
14
|
+
brotli?: boolean;
|
|
15
|
+
/** Minimum file size to compress (default: 8192) */
|
|
16
|
+
threshold?: number;
|
|
17
|
+
/** Minimum compression ratio (default: 0.8) */
|
|
18
|
+
minRatio?: number;
|
|
19
|
+
/** Brotli compression level (default: 8) */
|
|
20
|
+
brotliLevel?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const DEFAULT_OPTIONS: Required<CompressionPluginOptions> = {
|
|
24
|
+
gzip: true,
|
|
25
|
+
brotli: true,
|
|
26
|
+
threshold: 8192,
|
|
27
|
+
minRatio: 0.8,
|
|
28
|
+
brotliLevel: 8,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Add compression plugins to webpack config
|
|
33
|
+
*
|
|
34
|
+
* Returns true if plugins were added, false if compression-webpack-plugin is not installed
|
|
35
|
+
*/
|
|
36
|
+
export function addCompressionPlugins(
|
|
37
|
+
config: WebpackConfig,
|
|
38
|
+
options: CompressionPluginOptions = {}
|
|
39
|
+
): boolean {
|
|
40
|
+
// Check if compression-webpack-plugin is installed
|
|
41
|
+
if (!isPackageInstalled('compression-webpack-plugin')) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Dynamic import to avoid bundling if not installed
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
50
|
+
const CompressionPlugin = require('compression-webpack-plugin');
|
|
51
|
+
|
|
52
|
+
if (!config.plugins) {
|
|
53
|
+
config.plugins = [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Gzip compression
|
|
57
|
+
if (opts.gzip) {
|
|
58
|
+
config.plugins.push(
|
|
59
|
+
new CompressionPlugin({
|
|
60
|
+
filename: '[path][base].gz',
|
|
61
|
+
algorithm: 'gzip',
|
|
62
|
+
test: /\.(js|css|html|svg|json)$/,
|
|
63
|
+
threshold: opts.threshold,
|
|
64
|
+
minRatio: opts.minRatio,
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Brotli compression
|
|
70
|
+
if (opts.brotli) {
|
|
71
|
+
config.plugins.push(
|
|
72
|
+
new CompressionPlugin({
|
|
73
|
+
filename: '[path][base].br',
|
|
74
|
+
algorithm: 'brotliCompress',
|
|
75
|
+
test: /\.(js|css|html|svg|json)$/,
|
|
76
|
+
threshold: opts.threshold,
|
|
77
|
+
minRatio: opts.minRatio,
|
|
78
|
+
compressionOptions: {
|
|
79
|
+
level: opts.brotliLevel,
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return true;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
// compression-webpack-plugin failed to load
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Check if compression is available
|
|
94
|
+
*/
|
|
95
|
+
export function isCompressionAvailable(): boolean {
|
|
96
|
+
return isPackageInstalled('compression-webpack-plugin');
|
|
97
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev Startup Webpack Plugin
|
|
3
|
+
*
|
|
4
|
+
* Handles banner display, version checking, package updates, and browser auto-open.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Compiler } from 'webpack';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { DJANGO_CFG_BANNER } from '../constants';
|
|
10
|
+
import { getCurrentVersion } from '../utils/version';
|
|
11
|
+
import { checkAndInstallPackages } from '../packages/installer';
|
|
12
|
+
import { checkAndUpdatePackages } from '../packages/updater';
|
|
13
|
+
|
|
14
|
+
// Track if startup tasks were already run (persists across HMR)
|
|
15
|
+
let startupDone = false;
|
|
16
|
+
let browserOpened = false;
|
|
17
|
+
|
|
18
|
+
export interface DevStartupPluginOptions {
|
|
19
|
+
/** Auto-open browser */
|
|
20
|
+
openBrowser?: boolean;
|
|
21
|
+
/** Check for missing optional packages (default: true) */
|
|
22
|
+
checkPackages?: boolean;
|
|
23
|
+
/** Auto-install missing packages without prompting */
|
|
24
|
+
autoInstall?: boolean;
|
|
25
|
+
/** Check for @djangocfg/* package updates (default: true) */
|
|
26
|
+
checkUpdates?: boolean;
|
|
27
|
+
/** Auto-update packages without prompting */
|
|
28
|
+
autoUpdate?: boolean;
|
|
29
|
+
/** Force check workspace:* packages (for testing in monorepo) */
|
|
30
|
+
forceCheckWorkspace?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Webpack plugin for dev startup tasks
|
|
35
|
+
*/
|
|
36
|
+
export class DevStartupPlugin {
|
|
37
|
+
private options: DevStartupPluginOptions;
|
|
38
|
+
|
|
39
|
+
constructor(options: DevStartupPluginOptions = {}) {
|
|
40
|
+
this.options = options;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
apply(compiler: Compiler): void {
|
|
44
|
+
// Use tapPromise for proper async handling
|
|
45
|
+
compiler.hooks.done.tapPromise('DevStartupPlugin', async () => {
|
|
46
|
+
// Run startup tasks only once
|
|
47
|
+
if (!startupDone) {
|
|
48
|
+
startupDone = true;
|
|
49
|
+
await this.runStartupTasks();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Auto-open browser if enabled
|
|
53
|
+
if (this.options.openBrowser && !browserOpened) {
|
|
54
|
+
browserOpened = true;
|
|
55
|
+
this.openBrowser();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private async runStartupTasks(): Promise<void> {
|
|
61
|
+
// 1. Print banner
|
|
62
|
+
console.log('\n' + chalk.yellowBright.bold(DJANGO_CFG_BANNER));
|
|
63
|
+
|
|
64
|
+
// 2. Print current version
|
|
65
|
+
const version = getCurrentVersion();
|
|
66
|
+
if (version) {
|
|
67
|
+
console.log(chalk.dim(` 📦 @djangocfg/nextjs v${version}\n`));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 3. Check for package updates
|
|
71
|
+
if (this.options.checkUpdates !== false) {
|
|
72
|
+
try {
|
|
73
|
+
await checkAndUpdatePackages({
|
|
74
|
+
autoUpdate: this.options.autoUpdate,
|
|
75
|
+
forceCheckWorkspace: this.options.forceCheckWorkspace,
|
|
76
|
+
force: true, // Force check ignoring cooldown
|
|
77
|
+
});
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.log(chalk.red(' Update check failed:'), err);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 4. Check for missing optional packages
|
|
84
|
+
if (this.options.checkPackages !== false) {
|
|
85
|
+
await checkAndInstallPackages({
|
|
86
|
+
autoInstall: this.options.autoInstall,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private openBrowser(): void {
|
|
92
|
+
// Delay to ensure server is ready
|
|
93
|
+
setTimeout(async () => {
|
|
94
|
+
try {
|
|
95
|
+
const { exec } = await import('child_process');
|
|
96
|
+
const port = process.env.PORT || '3000';
|
|
97
|
+
const url = `http://localhost:${port}`;
|
|
98
|
+
|
|
99
|
+
const command = process.platform === 'darwin'
|
|
100
|
+
? 'open'
|
|
101
|
+
: process.platform === 'win32'
|
|
102
|
+
? 'start'
|
|
103
|
+
: 'xdg-open';
|
|
104
|
+
|
|
105
|
+
exec(`${command} ${url}`, (error) => {
|
|
106
|
+
if (error) {
|
|
107
|
+
console.warn(`Failed to open browser: ${error.message}`);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.warn('Failed to open browser');
|
|
112
|
+
}
|
|
113
|
+
}, 2000);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Reset plugin state (useful for tests)
|
|
119
|
+
*/
|
|
120
|
+
export function resetDevStartupState(): void {
|
|
121
|
+
startupDone = false;
|
|
122
|
+
browserOpened = false;
|
|
123
|
+
}
|