@sveltejs/vite-plugin-svelte 1.2.0 → 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sveltejs/vite-plugin-svelte",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "license": "MIT",
5
5
  "author": "dominikg",
6
6
  "files": [
@@ -65,7 +65,7 @@
65
65
  "diff-match-patch": "^1.0.5",
66
66
  "esbuild": "^0.15.14",
67
67
  "rollup": "^2.79.1",
68
- "svelte": "3.53.1",
68
+ "svelte": "^3.53.1",
69
69
  "tsup": "^6.5.0",
70
70
  "vite": "^3.2.3"
71
71
  },
package/src/index.ts CHANGED
@@ -15,7 +15,6 @@ import {
15
15
  patchResolvedViteConfig,
16
16
  preResolveOptions
17
17
  } from './utils/options';
18
- import { VitePluginSvelteCache } from './utils/vite-plugin-svelte-cache';
19
18
 
20
19
  import { ensureWatchedFile, setupWatchers } from './utils/watch';
21
20
  import { resolveViaPackageJsonSvelte } from './utils/resolve';
@@ -23,6 +22,7 @@ import { PartialResolvedId } from 'rollup';
23
22
  import { toRollupError } from './utils/error';
24
23
  import { saveSvelteMetadata } from './utils/optimizer';
25
24
  import { svelteInspector } from './ui/inspector/plugin';
25
+ import { VitePluginSvelteCache } from './utils/vite-plugin-svelte-cache';
26
26
 
27
27
  interface PluginAPI {
28
28
  /**
@@ -228,6 +228,9 @@ export function svelte(inlineOptions?: Partial<Options>): Plugin[] {
228
228
  throw toRollupError(e, options);
229
229
  }
230
230
  }
231
+ },
232
+ async buildEnd() {
233
+ await options.stats?.finishAll();
231
234
  }
232
235
  }
233
236
  ];
@@ -5,11 +5,13 @@ import { createMakeHot } from 'svelte-hmr';
5
5
  import { SvelteRequest } from './id';
6
6
  import { safeBase64Hash } from './hash';
7
7
  import { log } from './log';
8
+ import { StatCollection } from './vite-plugin-svelte-stats';
8
9
 
9
10
  const scriptLangRE = /<script [^>]*lang=["']?([^"' >]+)["']?[^>]*>/;
10
11
 
11
- const _createCompileSvelte = (makeHot: Function) =>
12
- async function compileSvelte(
12
+ const _createCompileSvelte = (makeHot: Function) => {
13
+ let stats: StatCollection | undefined;
14
+ return async function compileSvelte(
13
15
  svelteRequest: SvelteRequest,
14
16
  code: string,
15
17
  options: Partial<ResolvedOptions>
@@ -18,6 +20,31 @@ const _createCompileSvelte = (makeHot: Function) =>
18
20
  const { emitCss = true } = options;
19
21
  const dependencies = [];
20
22
 
23
+ if (options.stats) {
24
+ if (options.isBuild) {
25
+ if (!stats) {
26
+ // build is either completely ssr or csr, create stats collector on first compile
27
+ // it is then finished in the buildEnd hook.
28
+ stats = options.stats.startCollection(`${ssr ? 'ssr' : 'dom'} compile`, {
29
+ logInProgress: () => false
30
+ });
31
+ }
32
+ } else {
33
+ // dev time ssr, it's a ssr request and there are no stats, assume new page load and start collecting
34
+ if (ssr && !stats) {
35
+ stats = options.stats.startCollection('ssr compile');
36
+ }
37
+ // stats are being collected but this isn't an ssr request, assume page loaded and stop collecting
38
+ if (!ssr && stats) {
39
+ stats.finish();
40
+ stats = undefined;
41
+ }
42
+ // TODO find a way to trace dom compile during dev
43
+ // problem: we need to call finish at some point but have no way to tell if page load finished
44
+ // also they for hmr updates too
45
+ }
46
+ }
47
+
21
48
  const compileOptions: CompileOptions = {
22
49
  ...options.compilerOptions,
23
50
  filename,
@@ -67,7 +94,13 @@ const _createCompileSvelte = (makeHot: Function) =>
67
94
  ...dynamicCompileOptions
68
95
  }
69
96
  : compileOptions;
97
+
98
+ const endStat = stats?.start(filename);
70
99
  const compiled = compile(finalCode, finalCompileOptions);
100
+ if (endStat) {
101
+ endStat();
102
+ }
103
+
71
104
  const hasCss = compiled.css?.code?.trim().length > 0;
72
105
  // compiler might not emit css with mode none or it may be empty
73
106
  if (emitCss && hasCss) {
@@ -99,7 +132,7 @@ const _createCompileSvelte = (makeHot: Function) =>
99
132
  dependencies
100
133
  };
101
134
  };
102
-
135
+ };
103
136
  function buildMakeHot(options: ResolvedOptions) {
104
137
  const needsMakeHot = options.hot !== false && options.isServe && !options.isProduction;
105
138
  if (needsMakeHot) {
@@ -18,3 +18,5 @@ export const SVELTE_HMR_IMPORTS = [
18
18
  'svelte-hmr/runtime/proxy-adapter-dom.js',
19
19
  'svelte-hmr'
20
20
  ];
21
+
22
+ export const SVELTE_EXPORT_CONDITIONS = ['svelte'];
@@ -1,4 +1,4 @@
1
- import { promises as fs } from 'fs';
1
+ import { readFileSync } from 'fs';
2
2
  import { compile, preprocess } from 'svelte/compiler';
3
3
  import { DepOptimizationOptions } from 'vite';
4
4
  import { Compiled } from './compile';
@@ -6,6 +6,7 @@ import { log } from './log';
6
6
  import { CompileOptions, ResolvedOptions } from './options';
7
7
  import { toESBuildError } from './error';
8
8
  import { atLeastSvelte } from './svelte-version';
9
+ import { StatCollection } from './vite-plugin-svelte-stats';
9
10
 
10
11
  type EsbuildOptions = NonNullable<DepOptimizationOptions['esbuildOptions']>;
11
12
  type EsbuildPlugin = NonNullable<EsbuildOptions['plugins']>[number];
@@ -23,23 +24,32 @@ export function esbuildSveltePlugin(options: ResolvedOptions): EsbuildPlugin {
23
24
 
24
25
  const svelteExtensions = (options.extensions ?? ['.svelte']).map((ext) => ext.slice(1));
25
26
  const svelteFilter = new RegExp(`\\.(` + svelteExtensions.join('|') + `)(\\?.*)?$`);
26
-
27
+ let statsCollection: StatCollection | undefined;
28
+ build.onStart(() => {
29
+ statsCollection = options.stats?.startCollection('prebundle libraries', {
30
+ logResult: (c) => c.stats.length > 1
31
+ });
32
+ });
27
33
  build.onLoad({ filter: svelteFilter }, async ({ path: filename }) => {
28
- const code = await fs.readFile(filename, 'utf8');
34
+ const code = readFileSync(filename, 'utf8');
29
35
  try {
30
- const contents = await compileSvelte(options, { filename, code });
36
+ const contents = await compileSvelte(options, { filename, code }, statsCollection);
31
37
  return { contents };
32
38
  } catch (e) {
33
39
  return { errors: [toESBuildError(e, options)] };
34
40
  }
35
41
  });
42
+ build.onEnd(() => {
43
+ statsCollection?.finish();
44
+ });
36
45
  }
37
46
  };
38
47
  }
39
48
 
40
49
  async function compileSvelte(
41
50
  options: ResolvedOptions,
42
- { filename, code }: { filename: string; code: string }
51
+ { filename, code }: { filename: string; code: string },
52
+ statsCollection?: StatCollection
43
53
  ): Promise<string> {
44
54
  let css = options.compilerOptions.css;
45
55
  if (css !== 'none') {
@@ -83,8 +93,10 @@ async function compileSvelte(
83
93
  ...dynamicCompileOptions
84
94
  }
85
95
  : compileOptions;
86
-
96
+ const endStat = statsCollection?.start(filename);
87
97
  const compiled = compile(finalCode, finalCompileOptions) as Compiled;
88
-
98
+ if (endStat) {
99
+ endStat();
100
+ }
89
101
  return compiled.js.code + '//# sourceMappingURL=' + compiled.js.map.toUrl();
90
102
  }
@@ -2,7 +2,12 @@
2
2
  import { ConfigEnv, ResolvedConfig, UserConfig, ViteDevServer, normalizePath } from 'vite';
3
3
  import { log } from './log';
4
4
  import { loadSvelteConfig } from './load-svelte-config';
5
- import { SVELTE_HMR_IMPORTS, SVELTE_IMPORTS, SVELTE_RESOLVE_MAIN_FIELDS } from './constants';
5
+ import {
6
+ SVELTE_EXPORT_CONDITIONS,
7
+ SVELTE_HMR_IMPORTS,
8
+ SVELTE_IMPORTS,
9
+ SVELTE_RESOLVE_MAIN_FIELDS
10
+ } from './constants';
6
11
  // eslint-disable-next-line node/no-missing-import
7
12
  import type { CompileOptions, Warning } from 'svelte/types/compiler/interfaces';
8
13
  import type {
@@ -27,6 +32,7 @@ import {
27
32
  } from 'vitefu';
28
33
  import { atLeastSvelte } from './svelte-version';
29
34
  import { isCommonDepWithoutSvelteField } from './dependencies';
35
+ import { VitePluginSvelteStats } from './vite-plugin-svelte-stats';
30
36
 
31
37
  // svelte 3.53.0 changed compilerOptions.css from boolean to string | boolen, use string when available
32
38
  const cssAsString = atLeastSvelte('3.53.0');
@@ -129,9 +135,11 @@ export async function preResolveOptions(
129
135
  ...viteUserConfig,
130
136
  root: resolveViteRoot(viteUserConfig)
131
137
  };
138
+ const isBuild = viteEnv.command === 'build';
132
139
  const defaultOptions: Partial<Options> = {
133
140
  extensions: ['.svelte'],
134
- emitCss: true
141
+ emitCss: true,
142
+ prebundleSvelteLibraries: !isBuild
135
143
  };
136
144
  const svelteConfig = convertPluginOptions(
137
145
  await loadSvelteConfig(viteConfigWithResolvedRoot, inlineOptions)
@@ -139,7 +147,7 @@ export async function preResolveOptions(
139
147
 
140
148
  const extraOptions: Partial<PreResolvedOptions> = {
141
149
  root: viteConfigWithResolvedRoot.root!,
142
- isBuild: viteEnv.command === 'build',
150
+ isBuild,
143
151
  isServe: viteEnv.command === 'serve',
144
152
  isDebug: process.env.DEBUG != null
145
153
  };
@@ -203,6 +211,14 @@ export function resolveOptions(
203
211
  addExtraPreprocessors(merged, viteConfig);
204
212
  enforceOptionsForHmr(merged);
205
213
  enforceOptionsForProduction(merged);
214
+ // mergeConfigs would mangle functions on the stats class, so do this afterwards
215
+ const isLogLevelInfo = [undefined, 'info'].includes(viteConfig.logLevel);
216
+ const disableCompileStats = merged.experimental?.disableCompileStats;
217
+ const statsEnabled =
218
+ disableCompileStats !== true && disableCompileStats !== (merged.isBuild ? 'build' : 'dev');
219
+ if (statsEnabled && isLogLevelInfo) {
220
+ merged.stats = new VitePluginSvelteStats();
221
+ }
206
222
  return merged;
207
223
  }
208
224
 
@@ -315,7 +331,8 @@ export async function buildExtraViteConfig(
315
331
  const extraViteConfig: Partial<UserConfig> = {
316
332
  resolve: {
317
333
  mainFields: [...SVELTE_RESOLVE_MAIN_FIELDS],
318
- dedupe: [...SVELTE_IMPORTS, ...SVELTE_HMR_IMPORTS]
334
+ dedupe: [...SVELTE_IMPORTS, ...SVELTE_HMR_IMPORTS],
335
+ conditions: [...SVELTE_EXPORT_CONDITIONS]
319
336
  }
320
337
  // this option is still awaiting a PR in vite to be supported
321
338
  // see https://github.com/sveltejs/vite-plugin-svelte/issues/60
@@ -358,12 +375,17 @@ export async function buildExtraViteConfig(
358
375
 
359
376
  // handle prebundling for svelte files
360
377
  if (options.prebundleSvelteLibraries) {
361
- extraViteConfig.optimizeDeps.extensions = options.extensions ?? ['.svelte'];
362
- // Add esbuild plugin to prebundle Svelte files.
363
- // Currently a placeholder as more information is needed after Vite config is resolved,
364
- // the real Svelte plugin is added in `patchResolvedViteConfig()`
365
- extraViteConfig.optimizeDeps.esbuildOptions = {
366
- plugins: [{ name: facadeEsbuildSveltePluginName, setup: () => {} }]
378
+ extraViteConfig.optimizeDeps = {
379
+ ...extraViteConfig.optimizeDeps,
380
+ // Experimental Vite API to allow these extensions to be scanned and prebundled
381
+ // @ts-ignore
382
+ extensions: options.extensions ?? ['.svelte'],
383
+ // Add esbuild plugin to prebundle Svelte files.
384
+ // Currently a placeholder as more information is needed after Vite config is resolved,
385
+ // the real Svelte plugin is added in `patchResolvedViteConfig()`
386
+ esbuildOptions: {
387
+ plugins: [{ name: facadeEsbuildSveltePluginName, setup: () => {} }]
388
+ }
367
389
  };
368
390
  }
369
391
 
@@ -377,9 +399,44 @@ export async function buildExtraViteConfig(
377
399
  log.debug('enabling "experimental.hmrPartialAccept" in vite config');
378
400
  extraViteConfig.experimental = { hmrPartialAccept: true };
379
401
  }
402
+ validateViteConfig(extraViteConfig, config, options);
380
403
  return extraViteConfig;
381
404
  }
382
405
 
406
+ function validateViteConfig(
407
+ extraViteConfig: Partial<UserConfig>,
408
+ config: UserConfig,
409
+ options: PreResolvedOptions
410
+ ) {
411
+ const { prebundleSvelteLibraries, isBuild } = options;
412
+ if (prebundleSvelteLibraries) {
413
+ const isEnabled = (option: 'dev' | 'build' | boolean) =>
414
+ option !== true && option !== (isBuild ? 'build' : 'dev');
415
+ const logWarning = (name: string, value: 'dev' | 'build' | boolean, recommendation: string) =>
416
+ log.warn.once(
417
+ `Incompatible options: \`prebundleSvelteLibraries: true\` and vite \`${name}: ${JSON.stringify(
418
+ value
419
+ )}\` ${isBuild ? 'during build.' : '.'} ${recommendation}`
420
+ );
421
+ const viteOptimizeDepsDisabled = config.optimizeDeps?.disabled ?? 'build'; // fall back to vite default
422
+ const isOptimizeDepsEnabled = isEnabled(viteOptimizeDepsDisabled);
423
+ if (!isBuild && !isOptimizeDepsEnabled) {
424
+ logWarning(
425
+ 'optimizeDeps.disabled',
426
+ viteOptimizeDepsDisabled,
427
+ 'Forcing `optimizeDeps.disabled: "build"`. Disable prebundleSvelteLibraries or update your vite config to enable optimizeDeps during dev.'
428
+ );
429
+ extraViteConfig.optimizeDeps!.disabled = 'build';
430
+ } else if (isBuild && isOptimizeDepsEnabled) {
431
+ logWarning(
432
+ 'optimizeDeps.disabled',
433
+ viteOptimizeDepsDisabled,
434
+ 'Disable optimizeDeps or prebundleSvelteLibraries for build if you experience errors.'
435
+ );
436
+ }
437
+ }
438
+ }
439
+
383
440
  async function buildExtraConfigForDependencies(options: PreResolvedOptions, config: UserConfig) {
384
441
  // extra handling for svelte dependencies in the project
385
442
  const depsConfig = await crawlFrameworkPkgs({
@@ -387,7 +444,17 @@ async function buildExtraConfigForDependencies(options: PreResolvedOptions, conf
387
444
  isBuild: options.isBuild,
388
445
  viteUserConfig: config,
389
446
  isFrameworkPkgByJson(pkgJson) {
390
- return !!pkgJson.svelte;
447
+ let hasSvelteCondition = false;
448
+ if (typeof pkgJson.exports === 'object') {
449
+ // use replacer as a simple way to iterate over nested keys
450
+ JSON.stringify(pkgJson.exports, (key, value) => {
451
+ if (SVELTE_EXPORT_CONDITIONS.includes(key)) {
452
+ hasSvelteCondition = true;
453
+ }
454
+ return value;
455
+ });
456
+ }
457
+ return hasSvelteCondition || !!pkgJson.svelte;
391
458
  },
392
459
  isSemiFrameworkPkgByJson(pkgJson) {
393
460
  return !!pkgJson.dependencies?.svelte || !!pkgJson.peerDependencies?.svelte;
@@ -551,9 +618,11 @@ export interface PluginOptions {
551
618
  disableDependencyReinclusion?: boolean | string[];
552
619
 
553
620
  /**
554
- * Force Vite to pre-bundle Svelte libraries
621
+ * Enable support for Vite's dependency optimization to prebundle Svelte libraries.
555
622
  *
556
- * @default false
623
+ * To disable prebundling for a specific library, add it to `optimizeDeps.exclude`.
624
+ *
625
+ * @default true for dev, false for build
557
626
  */
558
627
  prebundleSvelteLibraries?: boolean;
559
628
 
@@ -655,6 +724,13 @@ export interface ExperimentalOptions {
655
724
  *
656
725
  */
657
726
  sendWarningsToBrowser?: boolean;
727
+
728
+ /**
729
+ * disable svelte compile statistics
730
+ *
731
+ * @default false
732
+ */
733
+ disableCompileStats?: 'dev' | 'build' | boolean;
658
734
  }
659
735
 
660
736
  export interface InspectorOptions {
@@ -741,6 +817,7 @@ export interface PreResolvedOptions extends Options {
741
817
  export interface ResolvedOptions extends PreResolvedOptions {
742
818
  isProduction: boolean;
743
819
  server?: ViteDevServer;
820
+ stats?: VitePluginSvelteStats;
744
821
  }
745
822
 
746
823
  export type {
@@ -0,0 +1,206 @@
1
+ import { log } from './log';
2
+ //eslint-disable-next-line node/no-missing-import
3
+ import { findClosestPkgJsonPath } from 'vitefu';
4
+ import { readFileSync } from 'fs';
5
+ import { performance } from 'perf_hooks';
6
+
7
+ interface Stat {
8
+ file: string;
9
+ pkg?: string;
10
+ start: number;
11
+ end: number;
12
+ }
13
+
14
+ export interface StatCollection {
15
+ name: string;
16
+ options: CollectionOptions;
17
+ //eslint-disable-next-line no-unused-vars
18
+ start: (file: string) => () => void;
19
+ stats: Stat[];
20
+ packageStats?: PackageStats[];
21
+ collectionStart: number;
22
+ duration?: number;
23
+ finish: () => Promise<void> | void;
24
+ finished: boolean;
25
+ }
26
+
27
+ interface PackageStats {
28
+ pkg: string;
29
+ files: number;
30
+ duration: number;
31
+ }
32
+
33
+ export interface CollectionOptions {
34
+ //eslint-disable-next-line no-unused-vars
35
+ logInProgress: (collection: StatCollection, now: number) => boolean;
36
+ //eslint-disable-next-line no-unused-vars
37
+ logResult: (collection: StatCollection) => boolean;
38
+ }
39
+
40
+ const defaultCollectionOptions: CollectionOptions = {
41
+ // log after 500ms and more than one file processed
42
+ logInProgress: (c, now) => now - c.collectionStart > 500 && c.stats.length > 1,
43
+ // always log results
44
+ logResult: () => true
45
+ };
46
+
47
+ function humanDuration(n: number) {
48
+ // 99.9ms 0.10s
49
+ return n < 100 ? `${n.toFixed(1)}ms` : `${(n / 1000).toFixed(2)}s`;
50
+ }
51
+
52
+ function formatPackageStats(pkgStats: PackageStats[]) {
53
+ const statLines = pkgStats.map((pkgStat) => {
54
+ const duration = pkgStat.duration;
55
+ const avg = duration / pkgStat.files;
56
+ return [pkgStat.pkg, `${pkgStat.files}`, humanDuration(duration), humanDuration(avg)];
57
+ });
58
+ statLines.unshift(['package', 'files', 'time', 'avg']);
59
+ const columnWidths = statLines.reduce(
60
+ (widths: number[], row) => {
61
+ for (let i = 0; i < row.length; i++) {
62
+ const cell = row[i];
63
+ if (widths[i] < cell.length) {
64
+ widths[i] = cell.length;
65
+ }
66
+ }
67
+ return widths;
68
+ },
69
+ statLines[0].map(() => 0)
70
+ );
71
+
72
+ const table = statLines
73
+ .map((row: string[]) =>
74
+ row
75
+ .map((cell: string, i: number) => {
76
+ if (i === 0) {
77
+ return cell.padEnd(columnWidths[i], ' ');
78
+ } else {
79
+ return cell.padStart(columnWidths[i], ' ');
80
+ }
81
+ })
82
+ .join('\t')
83
+ )
84
+ .join('\n');
85
+ return table;
86
+ }
87
+
88
+ export class VitePluginSvelteStats {
89
+ // package directory -> package name
90
+ private _packages: { path: string; name: string }[] = [];
91
+ private _collections: StatCollection[] = [];
92
+ startCollection(name: string, opts?: Partial<CollectionOptions>) {
93
+ const options = {
94
+ ...defaultCollectionOptions,
95
+ ...opts
96
+ };
97
+ const stats: Stat[] = [];
98
+ const collectionStart = performance.now();
99
+ const _this = this;
100
+ let hasLoggedProgress = false;
101
+ const collection: StatCollection = {
102
+ name,
103
+ options,
104
+ stats,
105
+ collectionStart,
106
+ finished: false,
107
+ start(file) {
108
+ if (collection.finished) {
109
+ throw new Error('called after finish() has been used');
110
+ }
111
+ const start = performance.now();
112
+ const stat: Stat = { file, start, end: start };
113
+ return () => {
114
+ const now = performance.now();
115
+ stat.end = now;
116
+ stats.push(stat);
117
+ if (!hasLoggedProgress && options.logInProgress(collection, now)) {
118
+ hasLoggedProgress = true;
119
+ log.info(`${name} in progress ...`);
120
+ }
121
+ };
122
+ },
123
+ async finish() {
124
+ await _this._finish(collection);
125
+ }
126
+ };
127
+ _this._collections.push(collection);
128
+ return collection;
129
+ }
130
+
131
+ public async finishAll() {
132
+ await Promise.all(this._collections.map((c) => c.finish()));
133
+ }
134
+
135
+ private async _finish(collection: StatCollection) {
136
+ collection.finished = true;
137
+ const now = performance.now();
138
+ collection.duration = now - collection.collectionStart;
139
+ const logResult = collection.options.logResult(collection);
140
+ if (logResult) {
141
+ await this._aggregateStatsResult(collection);
142
+ log.info(`${collection.name} done.`, formatPackageStats(collection.packageStats!));
143
+ }
144
+ // cut some ties to free it for garbage collection
145
+ const index = this._collections.indexOf(collection);
146
+ this._collections.splice(index, 1);
147
+ collection.stats.length = 0;
148
+ collection.stats = [];
149
+ if (collection.packageStats) {
150
+ collection.packageStats.length = 0;
151
+ collection.packageStats = [];
152
+ }
153
+ collection.start = () => () => {};
154
+ collection.finish = () => {};
155
+ }
156
+
157
+ private async _aggregateStatsResult(collection: StatCollection) {
158
+ const stats = collection.stats;
159
+ for (const stat of stats) {
160
+ let pkg = this._packages.find((p) => stat.file.startsWith(p.path));
161
+ if (!pkg) {
162
+ // check for package.json first
163
+ let pkgPath = await findClosestPkgJsonPath(stat.file);
164
+ if (pkgPath) {
165
+ let path = pkgPath?.replace(/package.json$/, '');
166
+ let name = JSON.parse(readFileSync(pkgPath, 'utf-8')).name;
167
+ if (!name) {
168
+ // some packages have nameless nested package.json
169
+ pkgPath = await findClosestPkgJsonPath(path);
170
+ if (pkgPath) {
171
+ path = pkgPath?.replace(/package.json$/, '');
172
+ name = JSON.parse(readFileSync(pkgPath, 'utf-8')).name;
173
+ }
174
+ }
175
+ if (path && name) {
176
+ pkg = { path, name };
177
+ this._packages.push(pkg);
178
+ }
179
+ }
180
+ }
181
+ // TODO is it possible that we want to track files where there is no named packge.json as parent?
182
+ // what do we want to do for that, try to find common root paths for different stats?
183
+ stat.pkg = pkg?.name ?? '$unknown';
184
+ }
185
+
186
+ // group stats
187
+ const grouped: { [key: string]: PackageStats } = {};
188
+ stats.forEach((stat) => {
189
+ const pkg = stat.pkg!;
190
+ let group = grouped[pkg];
191
+ if (!group) {
192
+ group = grouped[pkg] = {
193
+ files: 0,
194
+ duration: 0,
195
+ pkg
196
+ };
197
+ }
198
+ group.files += 1;
199
+ group.duration += stat.end - stat.start;
200
+ });
201
+
202
+ const groups = Object.values(grouped);
203
+ groups.sort((a, b) => b.duration - a.duration);
204
+ collection.packageStats = groups;
205
+ }
206
+ }