@metricinsights/pp-dev 0.4.0 → 0.6.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,32 @@ 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-bc9325e9.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-63f01789.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';
17
+ import svgToFont from 'svgtofont';
14
18
  import 'ejs';
19
+ import 'esbuild';
15
20
  import 'http-proxy-middleware';
21
+ import 'picocolors';
16
22
  import 'axios';
17
23
  import 'jsdom';
18
24
  import 'https';
19
25
  import 'memory-cache';
20
- import 'crypto';
21
26
  import 'process';
22
27
  import 'child_process';
23
28
  import 'console';
24
29
  import 'zlib';
25
30
 
26
- const colors$2 = createColors();
27
31
  function isDefined(value) {
28
32
  return value != null;
29
33
  }
@@ -32,8 +36,9 @@ function bindShortcuts(server, opts) {
32
36
  return;
33
37
  }
34
38
  server._shortcutsOptions = opts;
39
+ const logger = createLogger();
35
40
  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'));
41
+ logger.info(colors.dim(colors.green(' ➜')) + colors.dim(' press ') + colors.bold('h') + colors.dim(' to show help'));
37
42
  }
38
43
  const shortcuts = (opts.customShortcuts ?? []).filter(isDefined).concat(BASE_SHORTCUTS);
39
44
  let actionRunning = false;
@@ -47,10 +52,10 @@ function bindShortcuts(server, opts) {
47
52
  return;
48
53
  }
49
54
  if (input === 'h') {
50
- server.config.logger.info([
55
+ logger.info([
51
56
  '',
52
- colors$2.bold(' Shortcuts'),
53
- ...shortcuts.map((shortcut) => colors$2.dim(' press ') + colors$2.bold(shortcut.key) + colors$2.dim(` to ${shortcut.description}`)),
57
+ colors.bold(' Shortcuts'),
58
+ ...shortcuts.map((shortcut) => colors.dim(' press ') + colors.bold(shortcut.key) + colors.dim(` to ${shortcut.description}`)),
54
59
  ].join('\n'));
55
60
  }
56
61
  const shortcut = shortcuts.find((shortcut) => shortcut.key === input);
@@ -116,7 +121,6 @@ const BASE_SHORTCUTS = [
116
121
  },
117
122
  ];
118
123
 
119
- const colors$1 = createColors();
120
124
  function createDevServer(logLevel = 'info') {
121
125
  const server = express.default();
122
126
  const logger = winston.createLogger({
@@ -136,24 +140,516 @@ function createDevServer(logLevel = 'info') {
136
140
  if (!listener) {
137
141
  throw new Error('Server is not listening');
138
142
  }
139
- const colorUrl = (url) => colors$1.cyan(url.replace(/:(\d+)\//, (_, port) => `:${colors$1.bold(port)}/`));
143
+ const colorUrl = (url) => colors.cyan(url.replace(/:(\d+)\//, (_, port) => `:${colors.bold(port)}/`));
140
144
  const address = listener.address();
141
145
  if (address && typeof address === 'object') {
142
146
  if (address.address === '::') {
143
147
  const url = new URL$1(base || '', `http://localhost:${address.port}`);
144
- logger.info(` ${colors$1.green('➜')} ${colors$1.bold('Local')}: ${colorUrl(url.toString())}`);
148
+ logger.info(` ${colors.green('➜')} ${colors.bold('Local')}: ${colorUrl(url.toString())}`);
145
149
  }
146
150
  else {
147
151
  const url = new URL$1(base || '', `http://[${address.address}]:${address.port}`);
148
- logger.info(` ${colors$1.green('➜')} ${colors$1.bold('Local')}: ${colorUrl(url.toString())}`);
152
+ logger.info(` ${colors.green('➜')} ${colors.bold('Local')}: ${colorUrl(url.toString())}`);
149
153
  }
150
154
  }
151
155
  };
152
156
  return server;
153
157
  }
154
158
 
159
+ const changelogTemplate = /* HTML */ `<!DOCTYPE html>
160
+ <html lang="en">
161
+ <head>
162
+ <meta charset="UTF-8" />
163
+ <title>Changelog Diff</title>
164
+ <style>
165
+ tr,
166
+ td {
167
+ padding: 0;
168
+ }
169
+ .diff-file {
170
+ margin-top: 20px;
171
+ border: 1px solid #e1e4e8;
172
+ border-radius: 6px;
173
+ }
174
+ .diff-file-title {
175
+ padding: 10px 20px;
176
+ background-color: #f6f8fa;
177
+ border-bottom: 1px solid #e1e4e8;
178
+ border-radius: 6px 6px 0 0;
179
+ font-weight: bold;
180
+ }
181
+ .diff-file-title .renamed {
182
+ font-weight: normal;
183
+ }
184
+ .diff-file-title .renamed .from {
185
+ color: #cb2431;
186
+ }
187
+ .diff-file-title .renamed .to {
188
+ color: #22863a;
189
+ }
190
+ .diff-file-title .added {
191
+ color: #22863a;
192
+ }
193
+ .diff-file-title .deleted {
194
+ color: #cb2431;
195
+ }
196
+ .diff-file-content {
197
+ }
198
+ .diff-table {
199
+ tab-size: 8;
200
+ width: 100%;
201
+ border-collapse: separate;
202
+ border-spacing: 0;
203
+ }
204
+ .blob-num {
205
+ position: relative;
206
+ color: #1f2328;
207
+ width: 1%;
208
+ min-width: 50px;
209
+ padding: 0 10px;
210
+ font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
211
+ font-size: 12px;
212
+ line-height: 20px;
213
+ text-align: right;
214
+ white-space: nowrap;
215
+ vertical-align: top;
216
+ cursor: pointer;
217
+ -webkit-user-select: none;
218
+ user-select: none;
219
+ }
220
+ .blob-num.addition {
221
+ background-color: #ccffd8;
222
+ border-color: #1f883e;
223
+ }
224
+ .blob-num.deletion {
225
+ background-color: #ffd7d5;
226
+ border-color: #cf222e;
227
+ }
228
+ .blob-num::before {
229
+ content: attr(data-line-number);
230
+ }
231
+ .blob-code {
232
+ position: relative;
233
+ padding: 0 10px 0 22px;
234
+ vertical-align: top;
235
+ color: #1f2329;
236
+ }
237
+ .blob-code.code-addition {
238
+ background-color: #e6ffec;
239
+ }
240
+ .blob-code.code-deletion {
241
+ background-color: #ffebe9;
242
+ }
243
+ .blob-code.skip,
244
+ .blob-code.message {
245
+ text-align: center;
246
+ }
247
+ .blob-code.skip .blob-code-inner,
248
+ .blob-code.message .blob-code-inner {
249
+ font-weight: bold;
250
+ color: #6a737d;
251
+ padding: 10px 0;
252
+ }
253
+ .blob-code-inner {
254
+ display: table-cell;
255
+ overflow: visible;
256
+ font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
257
+ font-size: 12px;
258
+ word-wrap: anywhere;
259
+ white-space: pre-wrap;
260
+ }
261
+ .blob-code-inner::before {
262
+ content: attr(data-code-prefix);
263
+ position: absolute;
264
+ top: 1px;
265
+ left: 8px;
266
+ padding-right: 8px;
267
+ }
268
+ </style>
269
+ </head>
270
+ <body>
271
+ <h1>Changelog Diff</h1>
272
+
273
+ %FILES%
274
+ </body>
275
+ </html>`;
276
+ const LINE_ADDED = 1;
277
+ const LINE_REMOVED = -1;
278
+ const LINE_CONTEXT = 0;
279
+ /**
280
+ * Escape HTML sequence
281
+ * @param str
282
+ */
283
+ function escapeHTMLSequence(str) {
284
+ return str.replace(/[\u00A0-\u9999<>&]/g, (i) => '&#' + i.charCodeAt(0) + ';');
285
+ }
286
+ const fileNameWithHashRegEx = /(.+)(-[a-f0-9]{6,20})(\.[a-z0-9]+)$/i;
287
+ /**
288
+ * Changelog generator class
289
+ * @class
290
+ * @classdesc
291
+ * Generates a changelog file based on the differences between two asset folders or zip files
292
+ * @example
293
+ * const changelogGenerator = new ChangelogGenerator({
294
+ * previousAssetsPath: './dist-zip/previous',
295
+ * currentAssetsPath: './dist-zip/current',
296
+ * destinationPath: './dist-zip',
297
+ * changelogFilename: 'CHANGELOG.html',
298
+ * });
299
+ * changelogGenerator.generateChangelog();
300
+ */
301
+ class ChangelogGenerator {
302
+ oldAssetsPath;
303
+ newAssetsPath;
304
+ destinationPath;
305
+ changelogFilename;
306
+ changelogTemplate = changelogTemplate;
307
+ diffFileTemplateHandler;
308
+ diffLineTemplateHandler;
309
+ contextLines = 3;
310
+ logger;
311
+ /**
312
+ * Changelog generator class constructor
313
+ * @param opts
314
+ */
315
+ constructor(opts) {
316
+ const { oldAssetsPath, newAssetsPath, destinationPath, changelogTemplate, diffFileTemplateHandler, diffLineTemplateHandler, changelogFilename, contextLines, } = opts;
317
+ this.logger = createLogger();
318
+ if (!oldAssetsPath || !newAssetsPath || !destinationPath) {
319
+ throw new Error('Previous assets path, current assets path and destination path are required');
320
+ }
321
+ if (oldAssetsPath === newAssetsPath) {
322
+ throw new Error('Previous and current assets paths must be different');
323
+ }
324
+ if (!this.isExists(oldAssetsPath)) {
325
+ throw new Error(`Previous assets path ${oldAssetsPath} does not exist`);
326
+ }
327
+ if (!this.isExists(newAssetsPath)) {
328
+ throw new Error(`Current assets path ${newAssetsPath} does not exist`);
329
+ }
330
+ if (this.isZipFile(oldAssetsPath)) {
331
+ const unzipDestinationPath = path.resolve(os.tmpdir(), crypto.createHash('md5').update(oldAssetsPath).digest('hex'));
332
+ this.oldAssetsPath = this.unzipFile(oldAssetsPath, unzipDestinationPath)
333
+ .then(() => {
334
+ return this.normalizeAssetFolderPath(unzipDestinationPath);
335
+ })
336
+ .then((path) => {
337
+ if (this.isEmptyFolder(path)) {
338
+ throw new Error(`Previous assets path ${path} is empty`);
339
+ }
340
+ return path;
341
+ });
342
+ }
343
+ else if (this.isFolder(oldAssetsPath)) {
344
+ const path = this.normalizeAssetFolderPath(oldAssetsPath);
345
+ if (this.isEmptyFolder(path)) {
346
+ throw new Error(`Previous assets path ${path} is empty`);
347
+ }
348
+ this.oldAssetsPath = Promise.resolve(path);
349
+ }
350
+ else {
351
+ throw new Error(`Invalid previous assets path ${oldAssetsPath}. It must be a folder or a zip file`);
352
+ }
353
+ if (this.isZipFile(newAssetsPath)) {
354
+ const unzipDestinationPath = path.resolve(os.tmpdir(), crypto.createHash('md5').update(newAssetsPath).digest('hex'));
355
+ this.newAssetsPath = this.unzipFile(newAssetsPath, unzipDestinationPath)
356
+ .then(() => {
357
+ return this.normalizeAssetFolderPath(unzipDestinationPath);
358
+ })
359
+ .then((path) => {
360
+ if (this.isEmptyFolder(path)) {
361
+ throw new Error(`Current assets path ${path} is empty`);
362
+ }
363
+ return path;
364
+ });
365
+ }
366
+ else if (this.isFolder(newAssetsPath)) {
367
+ const path = this.normalizeAssetFolderPath(newAssetsPath);
368
+ if (this.isEmptyFolder(path)) {
369
+ throw new Error(`Current assets path ${newAssetsPath} is empty`);
370
+ }
371
+ this.newAssetsPath = Promise.resolve(path);
372
+ }
373
+ else {
374
+ throw new Error(`Invalid current assets path ${newAssetsPath}. It must be a folder or a zip file`);
375
+ }
376
+ this.destinationPath = destinationPath;
377
+ this.mkdirpSync(this.destinationPath);
378
+ if (changelogFilename) {
379
+ this.changelogFilename = changelogFilename;
380
+ }
381
+ else {
382
+ this.changelogFilename = 'CHANGELOG.html';
383
+ }
384
+ if (changelogTemplate) {
385
+ if (this.templateIsValid(changelogTemplate)) {
386
+ this.changelogTemplate = changelogTemplate;
387
+ }
388
+ else {
389
+ this.logger.warn(colors.yellow('Invalid changelog template, using default'));
390
+ }
391
+ }
392
+ if (typeof diffFileTemplateHandler === 'function') {
393
+ this.diffFileTemplateHandler = diffFileTemplateHandler;
394
+ }
395
+ if (typeof diffLineTemplateHandler === 'function') {
396
+ this.diffLineTemplateHandler = diffLineTemplateHandler;
397
+ }
398
+ if (contextLines) {
399
+ this.contextLines = contextLines;
400
+ }
401
+ }
402
+ templateIsValid(template) {
403
+ return template.includes('%FILES%');
404
+ }
405
+ isExists(assetPath) {
406
+ return fs.existsSync(assetPath);
407
+ }
408
+ isZipFile(assetPath) {
409
+ return assetPath.endsWith('.zip');
410
+ }
411
+ isFolder(assetPath) {
412
+ return fs.lstatSync(assetPath).isDirectory();
413
+ }
414
+ isEmptyFolder(assetPath) {
415
+ return fs.readdirSync(assetPath, { withFileTypes: true }).length === 0;
416
+ }
417
+ mkdirpSync(dir) {
418
+ if (!fs.existsSync(dir)) {
419
+ fs.mkdirSync(dir, { recursive: true });
420
+ }
421
+ }
422
+ async unzipFile(assetPath, destinationPath) {
423
+ fs.rmSync(destinationPath, { force: true, recursive: true });
424
+ return extractZip(assetPath, { dir: destinationPath });
425
+ }
426
+ normalizeAssetFolderPath(assetPath) {
427
+ const folderContent = fs.readdirSync(assetPath);
428
+ if (folderContent.length === 1 && fs.lstatSync(path.join(assetPath, folderContent[0])).isDirectory()) {
429
+ return this.normalizeAssetFolderPath(path.join(assetPath, folderContent[0]));
430
+ }
431
+ return assetPath;
432
+ }
433
+ pathToPosix(path) {
434
+ return path.replace(/\\/g, '/');
435
+ }
436
+ diffTableTemplate(diffLinesHTML) {
437
+ return /* HTML */ `<table class="diff-table">
438
+ <tbody>
439
+ ${diffLinesHTML}
440
+ </tbody>
441
+ </table>`;
442
+ }
443
+ getDiffTableHTML(diffLines) {
444
+ const diffLinesHTML = diffLines
445
+ .filter((value, index, array) => {
446
+ if (value.lineType === LINE_CONTEXT) {
447
+ return ((index >= 0 && index < this.contextLines) ||
448
+ (index > array.length - (this.contextLines + 1) && index <= array.length - 1) ||
449
+ array
450
+ .slice(index - this.contextLines, index + this.contextLines + 1)
451
+ .some((line) => line.lineType !== LINE_CONTEXT));
452
+ }
453
+ return true;
454
+ })
455
+ .map((line, index, array) => {
456
+ if (index > 0) {
457
+ const prevLine = array[index - 1];
458
+ if (line.lineNumber - prevLine.lineNumber > 1) {
459
+ return [{ lineNumber: -1, lineContent: '', lineType: LINE_CONTEXT }, line];
460
+ }
461
+ }
462
+ return [line];
463
+ })
464
+ .flat()
465
+ .map((line) => this.diffLineHTML(line))
466
+ .join('');
467
+ return this.diffTableTemplate(diffLinesHTML);
468
+ }
469
+ diffLineMessageTemplate(message) {
470
+ return /* HTML */ `<tr>
471
+ <td class="blob-num"></td>
472
+ <td class="blob-num"></td>
473
+ <td class="blob-code message">
474
+ <span class="blob-code-inner">${message}</span>
475
+ </td>
476
+ </tr>`;
477
+ }
478
+ diffLineSkipTemplate() {
479
+ return /* HTML */ `<tr>
480
+ <td class="blob-num"></td>
481
+ <td class="blob-num"></td>
482
+ <td class="blob-code skip">
483
+ <span class="blob-code-inner">Skip</span>
484
+ </td>
485
+ </tr>`;
486
+ }
487
+ diffLineTemplate(diffLine) {
488
+ const { lineNumber, lineContent, lineType } = diffLine;
489
+ const numClass = lineType === LINE_ADDED ? 'addition' : lineType === LINE_REMOVED ? 'deletion' : '';
490
+ const codeClass = lineType === LINE_ADDED ? 'code-addition' : lineType === LINE_REMOVED ? 'code-deletion' : '';
491
+ const prefix = lineType === LINE_ADDED ? '+' : lineType === LINE_REMOVED ? '-' : ' ';
492
+ return /* HTML */ `<tr>
493
+ <td
494
+ class="blob-num ${numClass}${numClass === 'addition' ? ' empty' : ''}"
495
+ ${numClass !== 'addition' ? ` data-line-number="${lineNumber}"` : ''}
496
+ ></td>
497
+ <td
498
+ class="blob-num ${numClass}${numClass === 'deletion' ? ' empty' : ''}"
499
+ ${numClass !== 'deletion' ? ` data-line-number="${lineNumber}"` : ''}
500
+ ></td>
501
+ <td class="blob-code ${codeClass}">
502
+ <span class="blob-code-inner" data-code-prefix="${prefix}">${escapeHTMLSequence(lineContent ?? '')}</span>
503
+ </td>
504
+ </tr>`;
505
+ }
506
+ diffLineHTML(diffLine) {
507
+ if (this.diffLineTemplateHandler) {
508
+ return this.diffLineTemplateHandler(diffLine);
509
+ }
510
+ const { lineNumber } = diffLine;
511
+ if (lineNumber === -1) {
512
+ return this.diffLineSkipTemplate();
513
+ }
514
+ return this.diffLineTemplate(diffLine);
515
+ }
516
+ diffFileTemplate(filename, htmlDiff) {
517
+ if (this.diffFileTemplateHandler) {
518
+ return this.diffFileTemplateHandler(filename, htmlDiff);
519
+ }
520
+ return /* HTML */ `<div class="diff-file">
521
+ <div class="diff-file-title">${filename}</div>
522
+ <div class="diff-file-content">${htmlDiff}</div>
523
+ </div>`;
524
+ }
525
+ async generateAssetFoldersDiff() {
526
+ this.logger.info(colors.blue(`Comparing asset folders ${await this.oldAssetsPath} and ${await this.newAssetsPath}`));
527
+ const dirDiff = await dirCompare.compare(await this.oldAssetsPath, await this.newAssetsPath, {
528
+ compareContent: true,
529
+ skipSymlinks: true,
530
+ compareSize: true,
531
+ compareDate: false,
532
+ compareNameHandler: (name1, name2) => {
533
+ if (fileNameWithHashRegEx.test(name1)) {
534
+ name1 = name1.replace(fileNameWithHashRegEx, '$1$3');
535
+ }
536
+ if (fileNameWithHashRegEx.test(name2)) {
537
+ name2 = name2.replace(fileNameWithHashRegEx, '$1$3');
538
+ }
539
+ if (name1.localeCompare(name2) === 0) {
540
+ return 0;
541
+ }
542
+ return name1.localeCompare(name2) > 0 ? 1 : -1;
543
+ },
544
+ });
545
+ return dirDiff.diffSet?.filter((d) => d.state !== 'equal' || d.name1 !== d.name2) || [];
546
+ }
547
+ async generateAssetFilesDiff(oldFileString, newFileString) {
548
+ const dmp = new DiffMatchPatch();
549
+ const linesChars = dmp.diff_linesToChars_(oldFileString, newFileString);
550
+ const linesDiff = dmp.diff_main(linesChars.chars1, linesChars.chars2, false);
551
+ dmp.diff_charsToLines_(linesDiff, linesChars.lineArray);
552
+ let lineNumber = 0;
553
+ return linesDiff
554
+ .map((diffLine) => {
555
+ const [lineType, lineContent] = diffLine;
556
+ const linesCount = lineContent.endsWith('\n')
557
+ ? lineContent.split('\n').length - 1
558
+ : lineContent.split('\n').length;
559
+ const lines = lineContent
560
+ .split('\n')
561
+ .map((line, index) => {
562
+ return { lineContent: line, lineNumber: lineNumber + index + 1, lineType };
563
+ })
564
+ .slice(0, linesCount);
565
+ if (lineType !== LINE_REMOVED) {
566
+ lineNumber += linesCount;
567
+ }
568
+ return lines;
569
+ })
570
+ .flat();
571
+ }
572
+ /**
573
+ * Generate diff for a file
574
+ * @param difference
575
+ * @private
576
+ */
577
+ async generateFilesDiff(difference) {
578
+ if (difference.state === 'equal') {
579
+ const filepath1 = this.pathToPosix(path.join('.', difference.relativePath, difference.name1 ?? ''));
580
+ const filepath2 = this.pathToPosix(path.join('.', difference.relativePath, difference.name2 ?? ''));
581
+ return this.diffFileTemplate(
582
+ /* HTML */ `<span class="renamed"
583
+ >Renamed <span class="from">${filepath1}</span> -> <span class="to">${filepath2}</span></span
584
+ >`, this.diffTableTemplate(this.diffLineMessageTemplate('No changes')));
585
+ }
586
+ const assetFile1Path = difference.path1 && difference.name1 ? path.join(difference.path1, difference.name1) : null;
587
+ const assetFile2Path = difference.path2 && difference.name2 ? path.join(difference.path2, difference.name2) : null;
588
+ const filepath = this.pathToPosix(path.join('.', difference.relativePath, (difference.name1 || difference.name2) ?? ''));
589
+ if (difference.state === 'left' && assetFile1Path) {
590
+ return this.diffFileTemplate(
591
+ /* HTML */ `<span class="removed">Removed ${filepath}</span>`, this.diffTableTemplate(this.diffLineMessageTemplate('File removed')));
592
+ }
593
+ if (difference.state === 'right' && assetFile2Path) {
594
+ return this.diffFileTemplate(
595
+ /* HTML */ `<span class="added">Added ${filepath}</span>`, this.diffTableTemplate(this.diffLineMessageTemplate('File added')));
596
+ }
597
+ if (difference.state === 'distinct' && assetFile1Path && assetFile2Path) {
598
+ const filepath1 = this.pathToPosix(path.join('.', difference.relativePath, difference.name1 ?? ''));
599
+ const filepath2 = this.pathToPosix(path.join('.', difference.relativePath, difference.name2 ?? ''));
600
+ const filenameTitle = difference.name1 !== difference.name2
601
+ ? /* HTML */ `<span class="renamed"
602
+ >Renamed <span class="from">${filepath1}</span> -> <span class="to">${filepath2}</span></span
603
+ >`
604
+ : filepath;
605
+ return this.diffFileTemplate(filenameTitle, (await isBinaryFile(assetFile1Path)) || (await isBinaryFile(assetFile2Path))
606
+ ? this.diffTableTemplate(this.diffLineMessageTemplate('Binary file'))
607
+ : this.getDiffTableHTML(await this.generateAssetFilesDiff(fs.readFileSync(assetFile1Path, 'utf-8'), fs.readFileSync(assetFile2Path, 'utf-8'))));
608
+ }
609
+ return '';
610
+ }
611
+ /**
612
+ * Generate changelog file based on the differences between two asset folders or zip files
613
+ * and write it to the destination folder
614
+ */
615
+ async generateChangelog() {
616
+ this.logger.info(colors.green('Generating changelog'));
617
+ const diffSet = await this.generateAssetFoldersDiff();
618
+ const htmlDiff = (await Promise.all(diffSet.map((difference) => this.generateFilesDiff(difference)))).join('');
619
+ this.logger.info(colors.green('Writing changelog file'));
620
+ const templateArray = this.changelogTemplate.split('%FILES%');
621
+ templateArray.splice(1, 0, htmlDiff);
622
+ const changelogHTML = templateArray.join('');
623
+ fs.writeFileSync(path.join(this.destinationPath, this.changelogFilename), changelogHTML);
624
+ this.logger.info(colors.green(`Changelog file written to ${path.join(this.destinationPath, this.changelogFilename)}`));
625
+ }
626
+ }
627
+
628
+ class IconFontGenerator {
629
+ sourceDir;
630
+ outputDir;
631
+ fontName;
632
+ constructor(options) {
633
+ this.sourceDir = options.sourceDir;
634
+ this.outputDir = options.outputDir;
635
+ this.fontName = options.fontName;
636
+ }
637
+ async generate() {
638
+ await svgToFont({
639
+ src: this.sourceDir,
640
+ dist: this.outputDir,
641
+ fontName: this.fontName,
642
+ css: true,
643
+ typescript: true,
644
+ startUnicode: 0xea01,
645
+ svgicons2svgfont: {
646
+ fontHeight: 1024,
647
+ },
648
+ });
649
+ }
650
+ }
651
+
155
652
  const cli = cac('pp-dev');
156
- const colors = createColors();
157
653
  let profileSession = global.__pp_dev_profile_session;
158
654
  let profileCount = 0;
159
655
  const stopProfiler = (log) => {
@@ -244,19 +740,21 @@ cli
244
740
  clearScreen: options.clearScreen,
245
741
  optimizeDeps: { force: options.force },
246
742
  server: cleanOptions(options),
743
+ customLogger: createLogger(options.logLevel),
247
744
  }, true));
745
+ if (!server.config.base || server.config.base === '/') {
746
+ throw new Error('base cannot be equal to "/" or empty string');
747
+ }
248
748
  if (!server.httpServer) {
249
749
  throw new Error('HTTP server not available');
250
750
  }
251
751
  await server.listen();
252
- const info = server.config.logger.info;
752
+ const logger = createLogger(options.logLevel);
253
753
  const ppDevStartTime = global.__pp_dev_start_time ?? false;
254
754
  const startupDurationString = ppDevStartTime
255
755
  ? colors.dim(`ready in ${colors.reset(colors.bold(Math.ceil(performance.now() - ppDevStartTime)))} ms`)
256
756
  : '';
257
- info(`\n ${colors.green(`${colors.bold('PP-DEV')} v${VERSION}`)} ${startupDurationString}\n`, {
258
- clear: !server.config.logger.hasWarned,
259
- });
757
+ logger.info(`\n ${colors.green(`${colors.bold('PP-DEV')} v${VERSION}`)} ${startupDurationString}\n`);
260
758
  server.printUrls();
261
759
  bindShortcuts(server, {
262
760
  print: true,
@@ -266,7 +764,7 @@ cli
266
764
  description: 'start/stop the profiler',
267
765
  async action(server) {
268
766
  if (profileSession) {
269
- await stopProfiler(server.config.logger.info);
767
+ await stopProfiler(logger.info);
270
768
  }
271
769
  else {
272
770
  const inspector = await import('node:inspector').then((r) => r.default);
@@ -275,7 +773,7 @@ cli
275
773
  profileSession.connect();
276
774
  profileSession.post('Profiler.enable', () => {
277
775
  profileSession?.post('Profiler.start', () => {
278
- server.config.logger.info('Profiler started');
776
+ logger.info('Profiler started');
279
777
  res();
280
778
  });
281
779
  });
@@ -321,16 +819,20 @@ cli
321
819
  .action(async (root, options) => {
322
820
  filterDuplicateOptions(options);
323
821
  const { default: next } = await import('next');
822
+ const logger = createLogger();
324
823
  const server = createDevServer(options.logLevel);
325
824
  const opts = cleanOptions(options);
326
825
  const app = next({ dev: true, hostname: opts.host, port: opts.port });
327
- const handle = app.getRequestHandler();
328
826
  await app.prepare();
329
827
  const nextServer = (await app.getServer());
330
828
  let base = nextServer.nextConfig.basePath;
829
+ const { assetPrefix } = nextServer.nextConfig;
331
830
  if (!base.endsWith('/')) {
332
831
  base += '/';
333
832
  }
833
+ if (base === '/') {
834
+ throw new Error('basePath cannot be equal to "/" or empty string');
835
+ }
334
836
  const baseWithoutTrailingSlash = base.substring(0, base.lastIndexOf('/'));
335
837
  const templateName = nextServer.nextConfig.serverRuntimeConfig.templateName;
336
838
  const ppDevConfig = nextServer.nextConfig.serverRuntimeConfig.ppDevConfig;
@@ -355,10 +857,14 @@ cli
355
857
  }
356
858
  server.use(initProxyCache({ devServer: server, ttl }));
357
859
  }
860
+ const proxyIgnore = ['/@vite', '/@metricinsights', '/@', baseWithoutTrailingSlash];
861
+ if (assetPrefix) {
862
+ proxyIgnore.push(assetPrefix);
863
+ }
358
864
  server.use(initProxy({
359
865
  devServer: server,
360
866
  baseURL: backendBaseURL,
361
- proxyIgnore: ['/@vite', '/@metricinsights', '/@', baseWithoutTrailingSlash],
867
+ proxyIgnore,
362
868
  disableSSLValidation,
363
869
  }));
364
870
  const isIndexRegExp = new RegExp(`^((${base})|/)$`);
@@ -370,7 +876,12 @@ cli
370
876
  return Buffer.from(urlReplacer(baseUrlHost, req.headers.host ?? '', mi.buildPage(response, miHudLess)));
371
877
  }));
372
878
  }
879
+ const handle = app.getRequestHandler();
373
880
  server.all('*', (req, res) => {
881
+ if (req.url.startsWith(assetPrefix) && assetPrefix !== baseWithoutTrailingSlash) {
882
+ const parsedUrl = parse(req.url.replace(assetPrefix, baseWithoutTrailingSlash), true);
883
+ return handle(req, res, parsedUrl);
884
+ }
374
885
  const parsedUrl = parse(req.url, true);
375
886
  handle(req, res, parsedUrl);
376
887
  });
@@ -387,12 +898,11 @@ cli
387
898
  }));
388
899
  }
389
900
  });
390
- const info = server.config.logger.info;
391
901
  const ppDevStartTime = global.__pp_dev_start_time ?? false;
392
902
  const startupDurationString = ppDevStartTime
393
903
  ? colors.dim(`ready in ${colors.reset(colors.bold(Math.ceil(performance.now() - ppDevStartTime)))} ms`)
394
904
  : '';
395
- info(`\n ${colors.green(`${colors.bold('PP-DEV')} v${VERSION}`)} ${startupDurationString}\n`, {
905
+ logger.info(`\n ${colors.green(`${colors.bold('PP-DEV')} v${VERSION}`)} ${startupDurationString}\n`, {
396
906
  clear: true,
397
907
  });
398
908
  server.printUrls(base);
@@ -458,6 +968,7 @@ cli
458
968
  .option('--force', `[boolean] force the optimizer to ignore the cache and re-bundle (experimental)`)
459
969
  .option('--emptyOutDir', `[boolean] force empty outDir when it's outside of root`)
460
970
  .option('-w, --watch', `[boolean] rebuilds when modules have changed on disk`)
971
+ .option('--changelog [assetsFile]', `[boolean | string] generate changelog between assetsFile and current build (default: false)`)
461
972
  .action(async (root, options) => {
462
973
  filterDuplicateOptions(options);
463
974
  const buildOptions = cleanOptions(options);
@@ -468,7 +979,7 @@ cli
468
979
  const { plugins, ...fileConfig } = configFromFile.config;
469
980
  config = mergeConfig(config, fileConfig);
470
981
  }
471
- await build(mergeConfig(config, {
982
+ const buildConfig = mergeConfig(config, {
472
983
  root,
473
984
  base: options.base,
474
985
  mode: options.mode,
@@ -477,7 +988,55 @@ cli
477
988
  clearScreen: options.clearScreen,
478
989
  optimizeDeps: { force: options.force },
479
990
  build: buildOptions,
480
- }, true));
991
+ }, true);
992
+ await build(buildConfig);
993
+ if (buildOptions.changelog) {
994
+ const executionRoot = root || process.cwd();
995
+ const outDir = buildConfig.build?.outDir || 'dist';
996
+ let oldAssetsPath = '';
997
+ if (typeof buildOptions.changelog === 'string') {
998
+ oldAssetsPath = path.resolve(executionRoot, buildOptions.changelog);
999
+ }
1000
+ else {
1001
+ const backupsDirPath = path.resolve(executionRoot, buildConfig.ppDevConfig?.syncBackupsDir || 'backups');
1002
+ if (!fs.existsSync(backupsDirPath)) {
1003
+ createLogger(options.logLevel).warn(colors.yellow(`backups directory not found, skipping changelog generation`));
1004
+ return;
1005
+ }
1006
+ const backups = fs.readdirSync(backupsDirPath, { withFileTypes: true });
1007
+ if (!backups.length) {
1008
+ createLogger(options.logLevel).warn(colors.yellow(`no backups found, skipping changelog generation`));
1009
+ return;
1010
+ }
1011
+ const latestBackup = backups
1012
+ .filter((value) => {
1013
+ return value.isFile() && value.name.endsWith('.zip');
1014
+ })
1015
+ .reduce((latest, current) => {
1016
+ const latestTime = fs.statSync(path.resolve(backupsDirPath, latest.name)).mtimeMs;
1017
+ const currentTime = fs.statSync(path.resolve(backupsDirPath, current.name)).mtimeMs;
1018
+ return latestTime > currentTime ? latest : current;
1019
+ }, backups[0]).name;
1020
+ oldAssetsPath = path.resolve(backupsDirPath, latestBackup);
1021
+ }
1022
+ const currentAssetFilePath = path.resolve(executionRoot, outDir);
1023
+ let changelogDestination = 'dist-zip';
1024
+ if (buildConfig.ppDevConfig) {
1025
+ if (buildConfig.ppDevConfig.distZip === false) {
1026
+ changelogDestination = buildConfig.build?.outDir || 'dist';
1027
+ }
1028
+ else if (typeof buildConfig.ppDevConfig.distZip === 'object' &&
1029
+ typeof buildConfig.ppDevConfig.distZip.outDir === 'string') {
1030
+ changelogDestination = buildConfig.ppDevConfig.distZip.outDir;
1031
+ }
1032
+ }
1033
+ const changelogGenerator = new ChangelogGenerator({
1034
+ oldAssetsPath,
1035
+ newAssetsPath: currentAssetFilePath,
1036
+ destinationPath: path.resolve(executionRoot, changelogDestination),
1037
+ });
1038
+ await changelogGenerator.generateChangelog();
1039
+ }
481
1040
  }
482
1041
  catch (e) {
483
1042
  createLogger(options.logLevel).error(colors.red(`error during build:\n${e.stack}`), { error: e });
@@ -487,6 +1046,53 @@ cli
487
1046
  stopProfiler((message) => createLogger(options.logLevel).info(message));
488
1047
  }
489
1048
  });
1049
+ // changelog
1050
+ cli
1051
+ .command('changelog [oldAssetPath] [newAssetPath]', 'generate changelog between two assets files/folders')
1052
+ .option('--oldAssetsPath <oldAssetsPath>', `[string] path to the old assets zip file or folder`)
1053
+ .option('--newAssetsPath <newAssetsPath>', `[string] path to the new assets zip file or folder`)
1054
+ .option('--destination <destination>', `[string] destination folder for the changelog (default: .)`)
1055
+ .option('--filename <filename>', `[string] filename for the changelog (default: CHANGELOG.html)`)
1056
+ .action(async (oldAssetPath, newAssetPath, options) => {
1057
+ filterDuplicateOptions(options);
1058
+ const { oldAssetsPath: oldPath = oldAssetPath, newAssetsPath: newPath = newAssetPath, destination = '.', filename = 'CHANGELOG.html', logLevel, } = options;
1059
+ const root = process.cwd();
1060
+ if (!oldPath || !newPath) {
1061
+ createLogger(logLevel).error(colors.red(`error during changelog generation: oldAssetPath and newAssetPath are required`));
1062
+ process.exit(1);
1063
+ }
1064
+ const fullOldPath = path.resolve(root, oldPath);
1065
+ const fullNewPath = path.resolve(root, newPath);
1066
+ const fullDestination = path.resolve(root, destination);
1067
+ const changelogGenerator = new ChangelogGenerator({
1068
+ oldAssetsPath: fullOldPath,
1069
+ newAssetsPath: fullNewPath,
1070
+ destinationPath: fullDestination,
1071
+ changelogFilename: filename,
1072
+ });
1073
+ await changelogGenerator.generateChangelog();
1074
+ });
1075
+ cli
1076
+ .command('generate-icon-font [source] [destination]', 'generate icon font from SVG files')
1077
+ .option('--source <source>', `[string] path to the source directory with SVG files`)
1078
+ .option('--destination <destination>', `[string] path to the destination directory to save the generated font files`)
1079
+ .option('--font-name, -n <fontName>', `[string] name of the font to generate (default: 'icon-font')`)
1080
+ .action(async (source, destination, options) => {
1081
+ filterDuplicateOptions(options);
1082
+ const { source: sourceDir = source, destination: destDir = destination, fontName = 'icon-font' } = options;
1083
+ const root = process.cwd();
1084
+ const fullSourceDir = path.resolve(root, sourceDir);
1085
+ const fullDestDir = path.resolve(root, destDir);
1086
+ const iconFontGenerator = new IconFontGenerator({
1087
+ sourceDir: fullSourceDir,
1088
+ outputDir: fullDestDir,
1089
+ fontName,
1090
+ });
1091
+ const logger = createLogger(options.logLevel);
1092
+ logger.info(`Generating icon font from SVG files in ${colors.dim(fullSourceDir)}`);
1093
+ await iconFontGenerator.generate();
1094
+ logger.info(`Icon font generated and saved to ${colors.dim(fullDestDir)}`);
1095
+ });
490
1096
  // optimize
491
1097
  cli
492
1098
  .command('optimize [root]', 'pre-bundle dependencies')