UNPKG

@sveltejs/vite-plugin-svelte

Version:

The official [Svelte](https://svelte.dev) plugin for [Vite](https://vitejs.dev).

249 lines (232 loc) 6.11 kB
/** @import { CollectionOptions, PackageStats, Stat, StatCollection } from '../types/vite-plugin-svelte-stats.js' */ import { log } from './log.js'; import { performance } from 'node:perf_hooks'; import { normalizePath } from 'vite'; import { findClosestPkgJsonPath } from 'vitefu'; import { readFileSync } from 'node:fs'; import { dirname } from 'node:path'; /** @type {CollectionOptions} */ const defaultCollectionOptions = { // log after 500ms and more than one file processed logInProgress: (c, now) => now - c.collectionStart > 500 && c.stats.length > 1, // always log results logResult: () => true }; /** * @param {number} n * @returns */ function humanDuration(n) { // 99.9ms 0.10s return n < 100 ? `${n.toFixed(1)}ms` : `${(n / 1000).toFixed(2)}s`; } /** * @param {PackageStats[]} pkgStats * @returns {string} */ function formatPackageStats(pkgStats) { const statLines = pkgStats.map((pkgStat) => { const duration = pkgStat.duration; const avg = duration / pkgStat.files; return [pkgStat.pkg, `${pkgStat.files}`, humanDuration(duration), humanDuration(avg)]; }); statLines.unshift(['package', 'files', 'time', 'avg']); const columnWidths = statLines.reduce( (widths, row) => { for (let i = 0; i < row.length; i++) { const cell = row[i]; if (widths[i] < cell.length) { widths[i] = cell.length; } } return widths; }, statLines[0].map(() => 0) ); const table = statLines .map((row) => row .map((cell, i) => { if (i === 0) { return cell.padEnd(columnWidths[i], ' '); } else { return cell.padStart(columnWidths[i], ' '); } }) .join('\t') ) .join('\n'); return table; } /** * @class */ export class VitePluginSvelteStats { /** @type {PackageInfo[]} */ #packageInfos = []; /** @type {StatCollection[]} */ #collections = []; /** * @param {string} name * @param {Partial<CollectionOptions>} [opts] * @returns {StatCollection} */ startCollection(name, opts) { const options = { ...defaultCollectionOptions, ...opts }; /** @type {Stat[]} */ const stats = []; const collectionStart = performance.now(); const _this = this; let hasLoggedProgress = false; /** @type {StatCollection} */ const collection = { name, options, stats, collectionStart, finished: false, start(file) { if (collection.finished) { throw new Error('called after finish() has been used'); } file = normalizePath(file); const start = performance.now(); /** @type {Stat} */ const stat = { file, start, end: start }; return () => { const now = performance.now(); stat.end = now; stats.push(stat); if (!hasLoggedProgress && options.logInProgress(collection, now)) { hasLoggedProgress = true; log.debug(`${name} in progress ...`, undefined, 'stats'); } }; }, async finish() { await _this.#finish(collection); } }; _this.#collections.push(collection); return collection; } async finishAll() { await Promise.all(this.#collections.map((c) => c.finish())); } /** * @param {StatCollection} collection */ async #finish(collection) { try { collection.finished = true; const now = performance.now(); collection.duration = now - collection.collectionStart; const logResult = collection.options.logResult(collection); if (logResult) { await this.#aggregateStatsResult(collection); log.debug( `${collection.name} done.\n${formatPackageStats( /** @type {PackageStats[]}*/ (collection.packageStats) )}`, undefined, 'stats' ); } // cut some ties to free it for garbage collection const index = this.#collections.indexOf(collection); this.#collections.splice(index, 1); collection.stats.length = 0; collection.stats = []; if (collection.packageStats) { collection.packageStats.length = 0; collection.packageStats = []; } collection.start = () => () => {}; collection.finish = () => {}; } catch (e) { // this should not happen, but stats taking also should not break the process log.debug.once(`failed to finish stats for ${collection.name}\n`, e, 'stats'); } } /** * @param {StatCollection} collection */ async #aggregateStatsResult(collection) { const stats = collection.stats; for (const stat of stats) { stat.pkg = (await this.#getPackageInfo(stat.file)).name; } // group stats /** @type {Record<string, PackageStats>} */ const grouped = {}; stats.forEach((stat) => { const pkg = /** @type {string} */ (stat.pkg); let group = grouped[pkg]; if (!group) { group = grouped[pkg] = { files: 0, duration: 0, pkg }; } group.files += 1; group.duration += stat.end - stat.start; }); const groups = Object.values(grouped); groups.sort((a, b) => b.duration - a.duration); collection.packageStats = groups; } /** * @param {string} file * @returns {Promise<PackageInfo>} */ async #getPackageInfo(file) { let info = this.#packageInfos.find((pi) => file.startsWith(pi.path)); if (!info) { info = await findPackageInfo(file); this.#packageInfos.push(info); } return info; } } /** * @typedef {{ * name: string; * version: string; * svelte?: string; * path: string; * }} PackageInfo */ /** * utility to get some info from the closest package.json with a "name" set * * @param {string} file to find info for * @returns {Promise<PackageInfo>} */ async function findPackageInfo(file) { /** @type {PackageInfo} */ const info = { name: '$unknown', version: '0.0.0-unknown', path: '$unknown' }; let path = await findClosestPkgJsonPath(file, (pkgPath) => { const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); if (pkg.name != null) { info.name = pkg.name; if (pkg.version != null) { info.version = pkg.version; } info.svelte = pkg.svelte; return true; } return false; }); // return normalized path with appended '/' so .startsWith works for future file checks path = normalizePath(dirname(path ?? file)) + '/'; info.path = path; return info; }