@metricinsights/pp-dev 0.4.0 → 0.5.0

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/dist/esm/cli.js CHANGED
@@ -2,28 +2,31 @@ import * as path from 'path';
2
2
  import * as fs from 'fs';
3
3
  import { performance } from 'node:perf_hooks';
4
4
  import { cac } from 'cac';
5
- import { createColors } from 'picocolors';
6
- import { g as getViteConfig, V as VERSION } from './index-1a1d27e5.js';
7
- import { loadConfigFromFile, mergeConfig, createLogger, build, resolveConfig, optimizeDeps, preview } from 'vite';
5
+ import { g as getViteConfig, V as VERSION } from './index-4361a6ba.js';
6
+ import { c as createLogger, a as colors, i as initPPRedirect, M as MiAPI, b as initProxyCache, d as initProxy, e as initLoadPPData, f as initRewriteResponse, u as urlReplacer, g as cutUrlParams } from './plugin-78cbd0b1.js';
7
+ import { loadConfigFromFile, mergeConfig, build, resolveConfig, optimizeDeps, preview } from 'vite';
8
8
  import { URL as URL$1, parse } from 'url';
9
- import { i as initPPRedirect, M as MiAPI, a as initProxyCache, b as initProxy, c as initLoadPPData, d as initRewriteResponse, u as urlReplacer, e as cutUrlParams } from './plugin-7db34718.js';
10
9
  import * as express from 'express';
11
10
  import * as winston from 'winston';
12
- import 'vite-plugin-zip-pack';
13
- import 'esbuild';
11
+ import * as dirCompare from 'dir-compare';
12
+ import DiffMatchPatch from 'diff-match-patch';
13
+ import { isBinaryFile } from 'isbinaryfile';
14
+ import * as os from 'os';
15
+ import * as crypto from 'crypto';
16
+ import extractZip from 'extract-zip';
14
17
  import 'ejs';
18
+ import 'esbuild';
15
19
  import 'http-proxy-middleware';
20
+ import 'picocolors';
16
21
  import 'axios';
17
22
  import 'jsdom';
18
23
  import 'https';
19
24
  import 'memory-cache';
20
- import 'crypto';
21
25
  import 'process';
22
26
  import 'child_process';
23
27
  import 'console';
24
28
  import 'zlib';
25
29
 
26
- const colors$2 = createColors();
27
30
  function isDefined(value) {
28
31
  return value != null;
29
32
  }
@@ -32,8 +35,9 @@ function bindShortcuts(server, opts) {
32
35
  return;
33
36
  }
34
37
  server._shortcutsOptions = opts;
38
+ const logger = createLogger();
35
39
  if (opts.print) {
36
- server.config.logger.info(colors$2.dim(colors$2.green(' ➜')) + colors$2.dim(' press ') + colors$2.bold('h') + colors$2.dim(' to show help'));
40
+ logger.info(colors.dim(colors.green(' ➜')) + colors.dim(' press ') + colors.bold('h') + colors.dim(' to show help'));
37
41
  }
38
42
  const shortcuts = (opts.customShortcuts ?? []).filter(isDefined).concat(BASE_SHORTCUTS);
39
43
  let actionRunning = false;
@@ -47,10 +51,10 @@ function bindShortcuts(server, opts) {
47
51
  return;
48
52
  }
49
53
  if (input === 'h') {
50
- server.config.logger.info([
54
+ logger.info([
51
55
  '',
52
- colors$2.bold(' Shortcuts'),
53
- ...shortcuts.map((shortcut) => colors$2.dim(' press ') + colors$2.bold(shortcut.key) + colors$2.dim(` to ${shortcut.description}`)),
56
+ colors.bold(' Shortcuts'),
57
+ ...shortcuts.map((shortcut) => colors.dim(' press ') + colors.bold(shortcut.key) + colors.dim(` to ${shortcut.description}`)),
54
58
  ].join('\n'));
55
59
  }
56
60
  const shortcut = shortcuts.find((shortcut) => shortcut.key === input);
@@ -116,7 +120,6 @@ const BASE_SHORTCUTS = [
116
120
  },
117
121
  ];
118
122
 
119
- const colors$1 = createColors();
120
123
  function createDevServer(logLevel = 'info') {
121
124
  const server = express.default();
122
125
  const logger = winston.createLogger({
@@ -136,24 +139,492 @@ function createDevServer(logLevel = 'info') {
136
139
  if (!listener) {
137
140
  throw new Error('Server is not listening');
138
141
  }
139
- const colorUrl = (url) => colors$1.cyan(url.replace(/:(\d+)\//, (_, port) => `:${colors$1.bold(port)}/`));
142
+ const colorUrl = (url) => colors.cyan(url.replace(/:(\d+)\//, (_, port) => `:${colors.bold(port)}/`));
140
143
  const address = listener.address();
141
144
  if (address && typeof address === 'object') {
142
145
  if (address.address === '::') {
143
146
  const url = new URL$1(base || '', `http://localhost:${address.port}`);
144
- logger.info(` ${colors$1.green('➜')} ${colors$1.bold('Local')}: ${colorUrl(url.toString())}`);
147
+ logger.info(` ${colors.green('➜')} ${colors.bold('Local')}: ${colorUrl(url.toString())}`);
145
148
  }
146
149
  else {
147
150
  const url = new URL$1(base || '', `http://[${address.address}]:${address.port}`);
148
- logger.info(` ${colors$1.green('➜')} ${colors$1.bold('Local')}: ${colorUrl(url.toString())}`);
151
+ logger.info(` ${colors.green('➜')} ${colors.bold('Local')}: ${colorUrl(url.toString())}`);
149
152
  }
150
153
  }
151
154
  };
152
155
  return server;
153
156
  }
154
157
 
158
+ const changelogTemplate = /* HTML */ `<!DOCTYPE html>
159
+ <html lang="en">
160
+ <head>
161
+ <meta charset="UTF-8" />
162
+ <title>Changelog Diff</title>
163
+ <style>
164
+ tr,
165
+ td {
166
+ padding: 0;
167
+ }
168
+ .diff-file {
169
+ margin-top: 20px;
170
+ border: 1px solid #e1e4e8;
171
+ border-radius: 6px;
172
+ }
173
+ .diff-file-title {
174
+ padding: 10px 20px;
175
+ background-color: #f6f8fa;
176
+ border-bottom: 1px solid #e1e4e8;
177
+ border-radius: 6px 6px 0 0;
178
+ font-weight: bold;
179
+ }
180
+ .diff-file-title .renamed {
181
+ font-weight: normal;
182
+ }
183
+ .diff-file-title .renamed .from {
184
+ color: #cb2431;
185
+ }
186
+ .diff-file-title .renamed .to {
187
+ color: #22863a;
188
+ }
189
+ .diff-file-title .added {
190
+ color: #22863a;
191
+ }
192
+ .diff-file-title .deleted {
193
+ color: #cb2431;
194
+ }
195
+ .diff-file-content {
196
+ }
197
+ .diff-table {
198
+ tab-size: 8;
199
+ width: 100%;
200
+ border-collapse: separate;
201
+ border-spacing: 0;
202
+ }
203
+ .blob-num {
204
+ position: relative;
205
+ color: #1f2328;
206
+ width: 1%;
207
+ min-width: 50px;
208
+ padding: 0 10px;
209
+ font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
210
+ font-size: 12px;
211
+ line-height: 20px;
212
+ text-align: right;
213
+ white-space: nowrap;
214
+ vertical-align: top;
215
+ cursor: pointer;
216
+ -webkit-user-select: none;
217
+ user-select: none;
218
+ }
219
+ .blob-num.addition {
220
+ background-color: #ccffd8;
221
+ border-color: #1f883e;
222
+ }
223
+ .blob-num.deletion {
224
+ background-color: #ffd7d5;
225
+ border-color: #cf222e;
226
+ }
227
+ .blob-num::before {
228
+ content: attr(data-line-number);
229
+ }
230
+ .blob-code {
231
+ position: relative;
232
+ padding: 0 10px 0 22px;
233
+ vertical-align: top;
234
+ color: #1f2329;
235
+ }
236
+ .blob-code.code-addition {
237
+ background-color: #e6ffec;
238
+ }
239
+ .blob-code.code-deletion {
240
+ background-color: #ffebe9;
241
+ }
242
+ .blob-code.skip,
243
+ .blob-code.message {
244
+ text-align: center;
245
+ }
246
+ .blob-code.skip .blob-code-inner,
247
+ .blob-code.message .blob-code-inner {
248
+ font-weight: bold;
249
+ color: #6a737d;
250
+ padding: 10px 0;
251
+ }
252
+ .blob-code-inner {
253
+ display: table-cell;
254
+ overflow: visible;
255
+ font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
256
+ font-size: 12px;
257
+ word-wrap: anywhere;
258
+ white-space: pre-wrap;
259
+ }
260
+ .blob-code-inner::before {
261
+ content: attr(data-code-prefix);
262
+ position: absolute;
263
+ top: 1px;
264
+ left: 8px;
265
+ padding-right: 8px;
266
+ }
267
+ </style>
268
+ </head>
269
+ <body>
270
+ <h1>Changelog Diff</h1>
271
+
272
+ %FILES%
273
+ </body>
274
+ </html>`;
275
+ const LINE_ADDED = 1;
276
+ const LINE_REMOVED = -1;
277
+ const LINE_CONTEXT = 0;
278
+ /**
279
+ * Escape HTML sequence
280
+ * @param str
281
+ */
282
+ function escapeHTMLSequence(str) {
283
+ return str.replace(/[\u00A0-\u9999<>&]/g, (i) => '&#' + i.charCodeAt(0) + ';');
284
+ }
285
+ const fileNameWithHashRegEx = /(.+)(-[a-f0-9]{6,20})(\.[a-z0-9]+)$/i;
286
+ /**
287
+ * Changelog generator class
288
+ * @class
289
+ * @classdesc
290
+ * Generates a changelog file based on the differences between two asset folders or zip files
291
+ * @example
292
+ * const changelogGenerator = new ChangelogGenerator({
293
+ * previousAssetsPath: './dist-zip/previous',
294
+ * currentAssetsPath: './dist-zip/current',
295
+ * destinationPath: './dist-zip',
296
+ * changelogFilename: 'CHANGELOG.html',
297
+ * });
298
+ * changelogGenerator.generateChangelog();
299
+ */
300
+ class ChangelogGenerator {
301
+ oldAssetsPath;
302
+ newAssetsPath;
303
+ destinationPath;
304
+ changelogFilename;
305
+ changelogTemplate = changelogTemplate;
306
+ diffFileTemplateHandler;
307
+ diffLineTemplateHandler;
308
+ contextLines = 3;
309
+ logger;
310
+ /**
311
+ * Changelog generator class constructor
312
+ * @param opts
313
+ */
314
+ constructor(opts) {
315
+ const { oldAssetsPath, newAssetsPath, destinationPath, changelogTemplate, diffFileTemplateHandler, diffLineTemplateHandler, changelogFilename, contextLines, } = opts;
316
+ this.logger = createLogger();
317
+ if (!oldAssetsPath || !newAssetsPath || !destinationPath) {
318
+ throw new Error('Previous assets path, current assets path and destination path are required');
319
+ }
320
+ if (oldAssetsPath === newAssetsPath) {
321
+ throw new Error('Previous and current assets paths must be different');
322
+ }
323
+ if (!this.isExists(oldAssetsPath)) {
324
+ throw new Error(`Previous assets path ${oldAssetsPath} does not exist`);
325
+ }
326
+ if (!this.isExists(newAssetsPath)) {
327
+ throw new Error(`Current assets path ${newAssetsPath} does not exist`);
328
+ }
329
+ if (this.isZipFile(oldAssetsPath)) {
330
+ const unzipDestinationPath = path.resolve(os.tmpdir(), crypto.createHash('md5').update(oldAssetsPath).digest('hex'));
331
+ this.oldAssetsPath = this.unzipFile(oldAssetsPath, unzipDestinationPath)
332
+ .then(() => {
333
+ return this.normalizeAssetFolderPath(unzipDestinationPath);
334
+ })
335
+ .then((path) => {
336
+ if (this.isEmptyFolder(path)) {
337
+ throw new Error(`Previous assets path ${path} is empty`);
338
+ }
339
+ return path;
340
+ });
341
+ }
342
+ else if (this.isFolder(oldAssetsPath)) {
343
+ const path = this.normalizeAssetFolderPath(oldAssetsPath);
344
+ if (this.isEmptyFolder(path)) {
345
+ throw new Error(`Previous assets path ${path} is empty`);
346
+ }
347
+ this.oldAssetsPath = Promise.resolve(path);
348
+ }
349
+ else {
350
+ throw new Error(`Invalid previous assets path ${oldAssetsPath}. It must be a folder or a zip file`);
351
+ }
352
+ if (this.isZipFile(newAssetsPath)) {
353
+ const unzipDestinationPath = path.resolve(os.tmpdir(), crypto.createHash('md5').update(newAssetsPath).digest('hex'));
354
+ this.newAssetsPath = this.unzipFile(newAssetsPath, unzipDestinationPath)
355
+ .then(() => {
356
+ return this.normalizeAssetFolderPath(unzipDestinationPath);
357
+ })
358
+ .then((path) => {
359
+ if (this.isEmptyFolder(path)) {
360
+ throw new Error(`Current assets path ${path} is empty`);
361
+ }
362
+ return path;
363
+ });
364
+ }
365
+ else if (this.isFolder(newAssetsPath)) {
366
+ const path = this.normalizeAssetFolderPath(newAssetsPath);
367
+ if (this.isEmptyFolder(path)) {
368
+ throw new Error(`Current assets path ${newAssetsPath} is empty`);
369
+ }
370
+ this.newAssetsPath = Promise.resolve(path);
371
+ }
372
+ else {
373
+ throw new Error(`Invalid current assets path ${newAssetsPath}. It must be a folder or a zip file`);
374
+ }
375
+ this.destinationPath = destinationPath;
376
+ this.mkdirpSync(this.destinationPath);
377
+ if (changelogFilename) {
378
+ this.changelogFilename = changelogFilename;
379
+ }
380
+ else {
381
+ this.changelogFilename = 'CHANGELOG.html';
382
+ }
383
+ if (changelogTemplate) {
384
+ if (this.templateIsValid(changelogTemplate)) {
385
+ this.changelogTemplate = changelogTemplate;
386
+ }
387
+ else {
388
+ this.logger.warn(colors.yellow('Invalid changelog template, using default'));
389
+ }
390
+ }
391
+ if (typeof diffFileTemplateHandler === 'function') {
392
+ this.diffFileTemplateHandler = diffFileTemplateHandler;
393
+ }
394
+ if (typeof diffLineTemplateHandler === 'function') {
395
+ this.diffLineTemplateHandler = diffLineTemplateHandler;
396
+ }
397
+ if (contextLines) {
398
+ this.contextLines = contextLines;
399
+ }
400
+ }
401
+ templateIsValid(template) {
402
+ return template.includes('%FILES%');
403
+ }
404
+ isExists(assetPath) {
405
+ return fs.existsSync(assetPath);
406
+ }
407
+ isZipFile(assetPath) {
408
+ return assetPath.endsWith('.zip');
409
+ }
410
+ isFolder(assetPath) {
411
+ return fs.lstatSync(assetPath).isDirectory();
412
+ }
413
+ isEmptyFolder(assetPath) {
414
+ return fs.readdirSync(assetPath, { withFileTypes: true }).length === 0;
415
+ }
416
+ mkdirpSync(dir) {
417
+ if (!fs.existsSync(dir)) {
418
+ fs.mkdirSync(dir, { recursive: true });
419
+ }
420
+ }
421
+ async unzipFile(assetPath, destinationPath) {
422
+ fs.rmSync(destinationPath, { force: true, recursive: true });
423
+ return extractZip(assetPath, { dir: destinationPath });
424
+ }
425
+ normalizeAssetFolderPath(assetPath) {
426
+ const folderContent = fs.readdirSync(assetPath);
427
+ if (folderContent.length === 1 && fs.lstatSync(path.join(assetPath, folderContent[0])).isDirectory()) {
428
+ return this.normalizeAssetFolderPath(path.join(assetPath, folderContent[0]));
429
+ }
430
+ return assetPath;
431
+ }
432
+ pathToPosix(path) {
433
+ return path.replace(/\\/g, '/');
434
+ }
435
+ diffTableTemplate(diffLinesHTML) {
436
+ return /* HTML */ `<table class="diff-table">
437
+ <tbody>
438
+ ${diffLinesHTML}
439
+ </tbody>
440
+ </table>`;
441
+ }
442
+ getDiffTableHTML(diffLines) {
443
+ const diffLinesHTML = diffLines
444
+ .filter((value, index, array) => {
445
+ if (value.lineType === LINE_CONTEXT) {
446
+ return ((index >= 0 && index < this.contextLines) ||
447
+ (index > array.length - (this.contextLines + 1) && index <= array.length - 1) ||
448
+ array
449
+ .slice(index - this.contextLines, index + this.contextLines + 1)
450
+ .some((line) => line.lineType !== LINE_CONTEXT));
451
+ }
452
+ return true;
453
+ })
454
+ .map((line, index, array) => {
455
+ if (index > 0) {
456
+ const prevLine = array[index - 1];
457
+ if (line.lineNumber - prevLine.lineNumber > 1) {
458
+ return [{ lineNumber: -1, lineContent: '', lineType: LINE_CONTEXT }, line];
459
+ }
460
+ }
461
+ return [line];
462
+ })
463
+ .flat()
464
+ .map((line) => this.diffLineHTML(line))
465
+ .join('');
466
+ return this.diffTableTemplate(diffLinesHTML);
467
+ }
468
+ diffLineMessageTemplate(message) {
469
+ return /* HTML */ `<tr>
470
+ <td class="blob-num"></td>
471
+ <td class="blob-num"></td>
472
+ <td class="blob-code message">
473
+ <span class="blob-code-inner">${message}</span>
474
+ </td>
475
+ </tr>`;
476
+ }
477
+ diffLineSkipTemplate() {
478
+ return /* HTML */ `<tr>
479
+ <td class="blob-num"></td>
480
+ <td class="blob-num"></td>
481
+ <td class="blob-code skip">
482
+ <span class="blob-code-inner">Skip</span>
483
+ </td>
484
+ </tr>`;
485
+ }
486
+ diffLineTemplate(diffLine) {
487
+ const { lineNumber, lineContent, lineType } = diffLine;
488
+ const numClass = lineType === LINE_ADDED ? 'addition' : lineType === LINE_REMOVED ? 'deletion' : '';
489
+ const codeClass = lineType === LINE_ADDED ? 'code-addition' : lineType === LINE_REMOVED ? 'code-deletion' : '';
490
+ const prefix = lineType === LINE_ADDED ? '+' : lineType === LINE_REMOVED ? '-' : ' ';
491
+ return /* HTML */ `<tr>
492
+ <td
493
+ class="blob-num ${numClass}${numClass === 'addition' ? ' empty' : ''}"
494
+ ${numClass !== 'addition' ? ` data-line-number="${lineNumber}"` : ''}
495
+ ></td>
496
+ <td
497
+ class="blob-num ${numClass}${numClass === 'deletion' ? ' empty' : ''}"
498
+ ${numClass !== 'deletion' ? ` data-line-number="${lineNumber}"` : ''}
499
+ ></td>
500
+ <td class="blob-code ${codeClass}">
501
+ <span class="blob-code-inner" data-code-prefix="${prefix}">${escapeHTMLSequence(lineContent ?? '')}</span>
502
+ </td>
503
+ </tr>`;
504
+ }
505
+ diffLineHTML(diffLine) {
506
+ if (this.diffLineTemplateHandler) {
507
+ return this.diffLineTemplateHandler(diffLine);
508
+ }
509
+ const { lineNumber } = diffLine;
510
+ if (lineNumber === -1) {
511
+ return this.diffLineSkipTemplate();
512
+ }
513
+ return this.diffLineTemplate(diffLine);
514
+ }
515
+ diffFileTemplate(filename, htmlDiff) {
516
+ if (this.diffFileTemplateHandler) {
517
+ return this.diffFileTemplateHandler(filename, htmlDiff);
518
+ }
519
+ return /* HTML */ `<div class="diff-file">
520
+ <div class="diff-file-title">${filename}</div>
521
+ <div class="diff-file-content">${htmlDiff}</div>
522
+ </div>`;
523
+ }
524
+ async generateAssetFoldersDiff() {
525
+ this.logger.info(colors.blue(`Comparing asset folders ${await this.oldAssetsPath} and ${await this.newAssetsPath}`));
526
+ const dirDiff = await dirCompare.compare(await this.oldAssetsPath, await this.newAssetsPath, {
527
+ compareContent: true,
528
+ skipSymlinks: true,
529
+ compareSize: true,
530
+ compareDate: false,
531
+ compareNameHandler: (name1, name2) => {
532
+ if (fileNameWithHashRegEx.test(name1)) {
533
+ name1 = name1.replace(fileNameWithHashRegEx, '$1$3');
534
+ }
535
+ if (fileNameWithHashRegEx.test(name2)) {
536
+ name2 = name2.replace(fileNameWithHashRegEx, '$1$3');
537
+ }
538
+ if (name1.localeCompare(name2) === 0) {
539
+ return 0;
540
+ }
541
+ return name1.localeCompare(name2) > 0 ? 1 : -1;
542
+ },
543
+ });
544
+ return dirDiff.diffSet?.filter((d) => d.state !== 'equal' || d.name1 !== d.name2) || [];
545
+ }
546
+ async generateAssetFilesDiff(oldFileString, newFileString) {
547
+ const dmp = new DiffMatchPatch();
548
+ const linesChars = dmp.diff_linesToChars_(oldFileString, newFileString);
549
+ const linesDiff = dmp.diff_main(linesChars.chars1, linesChars.chars2, false);
550
+ dmp.diff_charsToLines_(linesDiff, linesChars.lineArray);
551
+ let lineNumber = 0;
552
+ return linesDiff
553
+ .map((diffLine) => {
554
+ const [lineType, lineContent] = diffLine;
555
+ const linesCount = lineContent.endsWith('\n')
556
+ ? lineContent.split('\n').length - 1
557
+ : lineContent.split('\n').length;
558
+ const lines = lineContent
559
+ .split('\n')
560
+ .map((line, index) => {
561
+ return { lineContent: line, lineNumber: lineNumber + index + 1, lineType };
562
+ })
563
+ .slice(0, linesCount);
564
+ if (lineType !== LINE_REMOVED) {
565
+ lineNumber += linesCount;
566
+ }
567
+ return lines;
568
+ })
569
+ .flat();
570
+ }
571
+ /**
572
+ * Generate diff for a file
573
+ * @param difference
574
+ * @private
575
+ */
576
+ async generateFilesDiff(difference) {
577
+ if (difference.state === 'equal') {
578
+ const filepath1 = this.pathToPosix(path.join('.', difference.relativePath, difference.name1 ?? ''));
579
+ const filepath2 = this.pathToPosix(path.join('.', difference.relativePath, difference.name2 ?? ''));
580
+ return this.diffFileTemplate(
581
+ /* HTML */ `<span class="renamed"
582
+ >Renamed <span class="from">${filepath1}</span> -> <span class="to">${filepath2}</span></span
583
+ >`, this.diffTableTemplate(this.diffLineMessageTemplate('No changes')));
584
+ }
585
+ const assetFile1Path = difference.path1 && difference.name1 ? path.join(difference.path1, difference.name1) : null;
586
+ const assetFile2Path = difference.path2 && difference.name2 ? path.join(difference.path2, difference.name2) : null;
587
+ const filepath = this.pathToPosix(path.join('.', difference.relativePath, (difference.name1 || difference.name2) ?? ''));
588
+ if (difference.state === 'left' && assetFile1Path) {
589
+ return this.diffFileTemplate(
590
+ /* HTML */ `<span class="removed">Removed ${filepath}</span>`, this.diffTableTemplate(this.diffLineMessageTemplate('File removed')));
591
+ }
592
+ if (difference.state === 'right' && assetFile2Path) {
593
+ return this.diffFileTemplate(
594
+ /* HTML */ `<span class="added">Added ${filepath}</span>`, this.diffTableTemplate(this.diffLineMessageTemplate('File added')));
595
+ }
596
+ if (difference.state === 'distinct' && assetFile1Path && assetFile2Path) {
597
+ const filepath1 = this.pathToPosix(path.join('.', difference.relativePath, difference.name1 ?? ''));
598
+ const filepath2 = this.pathToPosix(path.join('.', difference.relativePath, difference.name2 ?? ''));
599
+ const filenameTitle = difference.name1 !== difference.name2
600
+ ? /* HTML */ `<span class="renamed"
601
+ >Renamed <span class="from">${filepath1}</span> -> <span class="to">${filepath2}</span></span
602
+ >`
603
+ : filepath;
604
+ return this.diffFileTemplate(filenameTitle, (await isBinaryFile(assetFile1Path)) || (await isBinaryFile(assetFile2Path))
605
+ ? this.diffTableTemplate(this.diffLineMessageTemplate('Binary file'))
606
+ : this.getDiffTableHTML(await this.generateAssetFilesDiff(fs.readFileSync(assetFile1Path, 'utf-8'), fs.readFileSync(assetFile2Path, 'utf-8'))));
607
+ }
608
+ return '';
609
+ }
610
+ /**
611
+ * Generate changelog file based on the differences between two asset folders or zip files
612
+ * and write it to the destination folder
613
+ */
614
+ async generateChangelog() {
615
+ this.logger.info(colors.green('Generating changelog'));
616
+ const diffSet = await this.generateAssetFoldersDiff();
617
+ const htmlDiff = (await Promise.all(diffSet.map((difference) => this.generateFilesDiff(difference)))).join('');
618
+ this.logger.info(colors.green('Writing changelog file'));
619
+ const templateArray = this.changelogTemplate.split('%FILES%');
620
+ templateArray.splice(1, 0, htmlDiff);
621
+ const changelogHTML = templateArray.join('');
622
+ fs.writeFileSync(path.join(this.destinationPath, this.changelogFilename), changelogHTML);
623
+ this.logger.info(colors.green(`Changelog file written to ${path.join(this.destinationPath, this.changelogFilename)}`));
624
+ }
625
+ }
626
+
155
627
  const cli = cac('pp-dev');
156
- const colors = createColors();
157
628
  let profileSession = global.__pp_dev_profile_session;
158
629
  let profileCount = 0;
159
630
  const stopProfiler = (log) => {
@@ -244,19 +715,21 @@ cli
244
715
  clearScreen: options.clearScreen,
245
716
  optimizeDeps: { force: options.force },
246
717
  server: cleanOptions(options),
718
+ customLogger: createLogger(options.logLevel),
247
719
  }, true));
720
+ if (!server.config.base || server.config.base === '/') {
721
+ throw new Error('base cannot be equal to "/" or empty string');
722
+ }
248
723
  if (!server.httpServer) {
249
724
  throw new Error('HTTP server not available');
250
725
  }
251
726
  await server.listen();
252
- const info = server.config.logger.info;
727
+ const logger = createLogger(options.logLevel);
253
728
  const ppDevStartTime = global.__pp_dev_start_time ?? false;
254
729
  const startupDurationString = ppDevStartTime
255
730
  ? colors.dim(`ready in ${colors.reset(colors.bold(Math.ceil(performance.now() - ppDevStartTime)))} ms`)
256
731
  : '';
257
- info(`\n ${colors.green(`${colors.bold('PP-DEV')} v${VERSION}`)} ${startupDurationString}\n`, {
258
- clear: !server.config.logger.hasWarned,
259
- });
732
+ logger.info(`\n ${colors.green(`${colors.bold('PP-DEV')} v${VERSION}`)} ${startupDurationString}\n`);
260
733
  server.printUrls();
261
734
  bindShortcuts(server, {
262
735
  print: true,
@@ -266,7 +739,7 @@ cli
266
739
  description: 'start/stop the profiler',
267
740
  async action(server) {
268
741
  if (profileSession) {
269
- await stopProfiler(server.config.logger.info);
742
+ await stopProfiler(logger.info);
270
743
  }
271
744
  else {
272
745
  const inspector = await import('node:inspector').then((r) => r.default);
@@ -275,7 +748,7 @@ cli
275
748
  profileSession.connect();
276
749
  profileSession.post('Profiler.enable', () => {
277
750
  profileSession?.post('Profiler.start', () => {
278
- server.config.logger.info('Profiler started');
751
+ logger.info('Profiler started');
279
752
  res();
280
753
  });
281
754
  });
@@ -331,6 +804,9 @@ cli
331
804
  if (!base.endsWith('/')) {
332
805
  base += '/';
333
806
  }
807
+ if (base === '/') {
808
+ throw new Error('basePath cannot be equal to "/" or empty string');
809
+ }
334
810
  const baseWithoutTrailingSlash = base.substring(0, base.lastIndexOf('/'));
335
811
  const templateName = nextServer.nextConfig.serverRuntimeConfig.templateName;
336
812
  const ppDevConfig = nextServer.nextConfig.serverRuntimeConfig.ppDevConfig;
@@ -387,12 +863,12 @@ cli
387
863
  }));
388
864
  }
389
865
  });
390
- const info = server.config.logger.info;
866
+ const logger = createLogger();
391
867
  const ppDevStartTime = global.__pp_dev_start_time ?? false;
392
868
  const startupDurationString = ppDevStartTime
393
869
  ? colors.dim(`ready in ${colors.reset(colors.bold(Math.ceil(performance.now() - ppDevStartTime)))} ms`)
394
870
  : '';
395
- info(`\n ${colors.green(`${colors.bold('PP-DEV')} v${VERSION}`)} ${startupDurationString}\n`, {
871
+ logger.info(`\n ${colors.green(`${colors.bold('PP-DEV')} v${VERSION}`)} ${startupDurationString}\n`, {
396
872
  clear: true,
397
873
  });
398
874
  server.printUrls(base);
@@ -458,6 +934,7 @@ cli
458
934
  .option('--force', `[boolean] force the optimizer to ignore the cache and re-bundle (experimental)`)
459
935
  .option('--emptyOutDir', `[boolean] force empty outDir when it's outside of root`)
460
936
  .option('-w, --watch', `[boolean] rebuilds when modules have changed on disk`)
937
+ .option('--changelog [assetsFile]', `[boolean | string] generate changelog between assetsFile and current build (default: false)`)
461
938
  .action(async (root, options) => {
462
939
  filterDuplicateOptions(options);
463
940
  const buildOptions = cleanOptions(options);
@@ -468,7 +945,7 @@ cli
468
945
  const { plugins, ...fileConfig } = configFromFile.config;
469
946
  config = mergeConfig(config, fileConfig);
470
947
  }
471
- await build(mergeConfig(config, {
948
+ const buildConfig = mergeConfig(config, {
472
949
  root,
473
950
  base: options.base,
474
951
  mode: options.mode,
@@ -477,7 +954,55 @@ cli
477
954
  clearScreen: options.clearScreen,
478
955
  optimizeDeps: { force: options.force },
479
956
  build: buildOptions,
480
- }, true));
957
+ }, true);
958
+ await build(buildConfig);
959
+ if (buildOptions.changelog) {
960
+ const executionRoot = root || process.cwd();
961
+ const outDir = buildConfig.build?.outDir || 'dist';
962
+ let oldAssetsPath = '';
963
+ if (typeof buildOptions.changelog === 'string') {
964
+ oldAssetsPath = path.resolve(executionRoot, buildOptions.changelog);
965
+ }
966
+ else {
967
+ const backupsDirPath = path.resolve(executionRoot, buildConfig.ppDevConfig?.syncBackupsDir || 'backups');
968
+ if (!fs.existsSync(backupsDirPath)) {
969
+ createLogger(options.logLevel).warn(colors.yellow(`backups directory not found, skipping changelog generation`));
970
+ return;
971
+ }
972
+ const backups = fs.readdirSync(backupsDirPath, { withFileTypes: true });
973
+ if (!backups.length) {
974
+ createLogger(options.logLevel).warn(colors.yellow(`no backups found, skipping changelog generation`));
975
+ return;
976
+ }
977
+ const latestBackup = backups
978
+ .filter((value) => {
979
+ return value.isFile() && value.name.endsWith('.zip');
980
+ })
981
+ .reduce((latest, current) => {
982
+ const latestTime = fs.statSync(path.resolve(backupsDirPath, latest.name)).mtimeMs;
983
+ const currentTime = fs.statSync(path.resolve(backupsDirPath, current.name)).mtimeMs;
984
+ return latestTime > currentTime ? latest : current;
985
+ }, backups[0]).name;
986
+ oldAssetsPath = path.resolve(backupsDirPath, latestBackup);
987
+ }
988
+ const currentAssetFilePath = path.resolve(executionRoot, outDir);
989
+ let changelogDestination = 'dist-zip';
990
+ if (buildConfig.ppDevConfig) {
991
+ if (buildConfig.ppDevConfig.distZip === false) {
992
+ changelogDestination = buildConfig.build?.outDir || 'dist';
993
+ }
994
+ else if (typeof buildConfig.ppDevConfig.distZip === 'object' &&
995
+ typeof buildConfig.ppDevConfig.distZip.outDir === 'string') {
996
+ changelogDestination = buildConfig.ppDevConfig.distZip.outDir;
997
+ }
998
+ }
999
+ const changelogGenerator = new ChangelogGenerator({
1000
+ oldAssetsPath,
1001
+ newAssetsPath: currentAssetFilePath,
1002
+ destinationPath: path.resolve(executionRoot, changelogDestination),
1003
+ });
1004
+ await changelogGenerator.generateChangelog();
1005
+ }
481
1006
  }
482
1007
  catch (e) {
483
1008
  createLogger(options.logLevel).error(colors.red(`error during build:\n${e.stack}`), { error: e });
@@ -487,6 +1012,32 @@ cli
487
1012
  stopProfiler((message) => createLogger(options.logLevel).info(message));
488
1013
  }
489
1014
  });
1015
+ // changelog
1016
+ cli
1017
+ .command('changelog [oldAssetPath] [newAssetPath]', 'generate changelog between two assets files/folders')
1018
+ .option('--oldAssetsPath <oldAssetsPath>', `[string] path to the old assets zip file or folder`)
1019
+ .option('--newAssetsPath <newAssetsPath>', `[string] path to the new assets zip file or folder`)
1020
+ .option('--destination <destination>', `[string] destination folder for the changelog (default: .)`)
1021
+ .option('--filename <filename>', `[string] filename for the changelog (default: CHANGELOG.html)`)
1022
+ .action(async (oldAssetPath, newAssetPath, options) => {
1023
+ filterDuplicateOptions(options);
1024
+ const { oldAssetsPath: oldPath = oldAssetPath, newAssetsPath: newPath = newAssetPath, destination = '.', filename = 'CHANGELOG.html', logLevel, } = options;
1025
+ const root = process.cwd();
1026
+ if (!oldPath || !newPath) {
1027
+ createLogger(logLevel).error(colors.red(`error during changelog generation: oldAssetPath and newAssetPath are required`));
1028
+ process.exit(1);
1029
+ }
1030
+ const fullOldPath = path.resolve(root, oldPath);
1031
+ const fullNewPath = path.resolve(root, newPath);
1032
+ const fullDestination = path.resolve(root, destination);
1033
+ const changelogGenerator = new ChangelogGenerator({
1034
+ oldAssetsPath: fullOldPath,
1035
+ newAssetsPath: fullNewPath,
1036
+ destinationPath: fullDestination,
1037
+ changelogFilename: filename,
1038
+ });
1039
+ await changelogGenerator.generateChangelog();
1040
+ });
490
1041
  // optimize
491
1042
  cli
492
1043
  .command('optimize [root]', 'pre-bundle dependencies')