From 520ba8da095dd879bb694ced1d9d388a3a8ac1ee Mon Sep 17 00:00:00 2001 From: Anton Evzhakov Date: Sun, 23 Jul 2023 18:36:46 +0300 Subject: [PATCH] feat: debug mode for CLI, Vite, and Webpack (#1298) --- .changeset/stale-brooms-kick.md | 11 + .gitignore | 1 + packages/babel/src/plugins/preeval.ts | 26 ++- .../transform-stages/1-prepare-for-eval.ts | 195 ++++++++++++------ packages/babel/src/transform.ts | 30 +-- packages/cli/src/linaria.ts | 57 +---- packages/shaker/src/plugins/shaker-plugin.ts | 24 ++- packages/testkit/src/prepareCode.test.ts | 7 +- packages/utils/src/EventEmitter.ts | 22 ++ packages/utils/src/debug/perfMetter.ts | 144 +++++++++++++ packages/utils/src/index.ts | 3 + packages/vite/src/index.ts | 13 +- packages/webpack5-loader/package.json | 1 + .../webpack5-loader/src/LinariaDebugPlugin.ts | 24 +++ packages/webpack5-loader/src/index.ts | 6 +- pnpm-lock.yaml | 5 + website/webpack.config.js | 5 + 17 files changed, 435 insertions(+), 139 deletions(-) create mode 100644 .changeset/stale-brooms-kick.md create mode 100644 packages/utils/src/EventEmitter.ts create mode 100644 packages/utils/src/debug/perfMetter.ts create mode 100644 packages/webpack5-loader/src/LinariaDebugPlugin.ts diff --git a/.changeset/stale-brooms-kick.md b/.changeset/stale-brooms-kick.md new file mode 100644 index 000000000..519840c1e --- /dev/null +++ b/.changeset/stale-brooms-kick.md @@ -0,0 +1,11 @@ +--- +'@linaria/babel-preset': patch +'@linaria/cli': patch +'@linaria/testkit': patch +'@linaria/utils': patch +'@linaria/vite': patch +'@linaria/webpack5-loader': patch +'linaria-website': patch +--- + +Debug mode for CLI, Webpack 5 and Vite. When enabled, prints brief perf report to console and information about processed dependency tree to the specified file. diff --git a/.gitignore b/.gitignore index 10c18c2b0..18e1c86e4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ logs npm-debug.log* yarn-debug.log* yarn-error.log* +**/linaria-debug.json # Runtime data pids diff --git a/packages/babel/src/plugins/preeval.ts b/packages/babel/src/plugins/preeval.ts index f167d21be..81bc2f735 100644 --- a/packages/babel/src/plugins/preeval.ts +++ b/packages/babel/src/plugins/preeval.ts @@ -7,6 +7,7 @@ import type { BabelFile, PluginObj } from '@babel/core'; import { createCustomDebug } from '@linaria/logger'; import type { StrictOptions } from '@linaria/utils'; import { + EventEmitter, getFileIdx, addIdentifierToLinariaPreval, removeDangerousCode, @@ -20,12 +21,14 @@ import { processTemplateExpression } from '../utils/processTemplateExpression'; export type PreevalOptions = Pick< StrictOptions, 'classNameSlug' | 'displayName' | 'evaluate' | 'features' ->; +> & { eventEmitter: EventEmitter }; + +const onFinishCallbacks = new WeakMap void>(); export default function preeval( babel: Core, - options: PreevalOptions -): PluginObj { + { eventEmitter = EventEmitter.dummy, ...options }: PreevalOptions +): PluginObj void }> { const { types: t } = babel; return { name: '@linaria/babel/preeval', @@ -38,6 +41,10 @@ export default function preeval( const rootScope = file.scope; this.processors = []; + const onProcessTemplateFinished = eventEmitter.pair({ + method: 'preeval:processTemplate', + }); + file.path.traverse({ Identifier: (p) => { processTemplateExpression(p, file.opts, options, (processor) => { @@ -53,15 +60,28 @@ export default function preeval( }, }); + onProcessTemplateFinished(); + if ( isFeatureEnabled(options.features, 'dangerousCodeRemover', filename) ) { log('start', 'Strip all JSX and browser related stuff'); + const onCodeRemovingFinished = eventEmitter.pair({ + method: 'preeval:removeDangerousCode', + }); removeDangerousCode(file.path); + onCodeRemovingFinished(); } + + onFinishCallbacks.set( + this, + eventEmitter.pair({ method: 'preeval:rest-transformations' }) + ); }, visitor: {}, post(file: BabelFile) { + onFinishCallbacks.get(this)?.(); + const log = createCustomDebug('preeval', getFileIdx(file.opts.filename!)); if (this.processors.length === 0) { diff --git a/packages/babel/src/transform-stages/1-prepare-for-eval.ts b/packages/babel/src/transform-stages/1-prepare-for-eval.ts index 5c1ae5915..f8312c688 100644 --- a/packages/babel/src/transform-stages/1-prepare-for-eval.ts +++ b/packages/babel/src/transform-stages/1-prepare-for-eval.ts @@ -12,7 +12,12 @@ import type { File } from '@babel/types'; import type { CustomDebug } from '@linaria/logger'; import { createCustomDebug } from '@linaria/logger'; import type { Evaluator, EvaluatorConfig, StrictOptions } from '@linaria/utils'; -import { buildOptions, getFileIdx, loadBabelOptions } from '@linaria/utils'; +import { + buildOptions, + EventEmitter, + getFileIdx, + loadBabelOptions, +} from '@linaria/utils'; import type { Core } from '../babel'; import type { TransformCacheCollection } from '../cache'; @@ -49,7 +54,8 @@ function runPreevalStage( babel: Core, item: IEntrypoint, originalAst: File, - pluginOptions: StrictOptions + pluginOptions: StrictOptions, + eventEmitter: EventEmitter ): BabelFileResult { const babelOptions = item.parseConfig; @@ -60,7 +66,7 @@ function runPreevalStage( const plugins = [ ...preShakePlugins, - [require.resolve('../plugins/preeval'), pluginOptions], + [require.resolve('../plugins/preeval'), { ...pluginOptions, eventEmitter }], ...(babelOptions.plugins ?? []).filter( (i) => !hasKeyInList(i, pluginOptions.highPriorityPlugins) ), @@ -89,18 +95,22 @@ export function prepareCode( babel: Core, item: IEntrypoint, originalAst: File, - pluginOptions: StrictOptions + pluginOptions: StrictOptions, + eventEmitter: EventEmitter ): [code: string, imports: Module['imports'], metadata?: BabelFileMetadata] { const { evaluator, name: filename, parseConfig, only } = item; const log = createCustomDebug('transform', getFileIdx(filename)); + const onPreevalFinished = eventEmitter.pair({ method: 'preeval' }); const preevalStageResult = runPreevalStage( babel, item, originalAst, - pluginOptions + pluginOptions, + eventEmitter ); + onPreevalFinished(); if ( only.length === 1 && @@ -120,6 +130,7 @@ export function prepareCode( features: pluginOptions.features, }; + const onEvaluatorFinished = eventEmitter.pair({ method: 'evaluator' }); const [, code, imports] = evaluator( parseConfig, preevalStageResult.ast!, @@ -127,6 +138,7 @@ export function prepareCode( evaluatorConfig, babel ); + onEvaluatorFinished(); log('stage-1:evaluator:end', ''); @@ -137,7 +149,8 @@ function processQueueItem( babel: Core, item: IEntrypoint | null, cache: TransformCacheCollection, - pluginOptions: StrictOptions + pluginOptions: StrictOptions, + eventEmitter: EventEmitter ): | { imports: Map | null; @@ -151,9 +164,11 @@ function processQueueItem( const { parseConfig, name, only, code } = item; + const onParseFinished = eventEmitter.pair({ method: 'parseFile' }); const ast: File = cache.originalASTCache.get(name) ?? parseFile(babel, name, code, parseConfig); + onParseFinished(); const log = createCustomDebug('transform', getFileIdx(name)); @@ -166,7 +181,8 @@ function processQueueItem( babel, item, ast, - pluginOptions + pluginOptions, + eventEmitter ); if (code === preparedCode) { @@ -199,10 +215,13 @@ export function createEntrypoint( babel: Core, name: string, only: string[], - code: string, + maybeCode: string | undefined, pluginOptions: StrictOptions, - options: Pick + options: Pick, + eventEmitter: EventEmitter ): IEntrypoint | 'ignored' { + const finishEvent = eventEmitter.pair({ method: 'createEntrypoint' }); + const log = createCustomDebug('transform', getFileIdx(name)); const extension = extname(name); @@ -212,9 +231,12 @@ export function createEntrypoint( `${name} is ignored. If you want it to be processed, you should add '${extension}' to the "extensions" option.` ); + finishEvent(); return 'ignored'; } + const code = maybeCode ?? readFileSync(name, 'utf-8'); + const { action, babelOptions } = getMatchedRule( pluginOptions.rules, name, @@ -223,6 +245,7 @@ export function createEntrypoint( if (action === 'ignore') { log('createEntrypoint', `${name} is ignored by rule`); + finishEvent(); return 'ignored'; } @@ -246,6 +269,7 @@ export function createEntrypoint( log('createEntrypoint', `${name} (${only.join(', ')})\n${code}`); + finishEvent(); return { code, evaluator, @@ -267,7 +291,8 @@ function processImports( importsOnly: string[]; importedFile: string; resolved: string | null; - }[] + }[], + eventEmitter: EventEmitter ) { for (const { importedFile, importsOnly, resolved } of resolvedImports) { if (resolved === null) { @@ -295,14 +320,14 @@ function processImports( `${resolved}\0${[...importsOnlySet].join(',')}` ); - const fileContent = readFileSync(resolved, 'utf8'); const next = createEntrypoint( babel, resolved, - importsOnly, - fileContent, + [...importsOnlySet], + undefined, pluginOptions, - options + options, + eventEmitter ); if (next === 'ignored') { continue; @@ -319,7 +344,8 @@ function processEntrypoint( cache: TransformCacheCollection, pluginOptions: StrictOptions, options: Pick, - nextItem: NextItem + nextItem: NextItem, + eventEmitter: EventEmitter ): | { imports: Map | null; @@ -370,7 +396,8 @@ function processEntrypoint( only: mergedOnly, }, cache, - pluginOptions + pluginOptions, + eventEmitter ); if (!processed) { @@ -395,7 +422,8 @@ export function prepareForEvalSync( resolve: (what: string, importer: string, stack: string[]) => string, partialEntrypoint: Pick, pluginOptions: StrictOptions, - options: Pick + options: Pick, + eventEmitter = EventEmitter.dummy ): ITransformFileResult | undefined { const log = createCustomDebug( 'transform', @@ -408,7 +436,8 @@ export function prepareForEvalSync( partialEntrypoint.only, partialEntrypoint.code, pluginOptions, - options + options, + eventEmitter ); if (entrypoint === 'ignored') { @@ -429,16 +458,19 @@ export function prepareForEvalSync( cache, pluginOptions, options, - item + item, + eventEmitter ); if (processResult === 'skip') { continue; } const { imports, result, only: mergedOnly } = processResult; + const listOfImports = Array.from(imports?.entries() ?? []); - if (imports) { - const resolvedImports = Array.from(imports?.entries() ?? []).map( + if (listOfImports.length > 0) { + const onResolveFinished = eventEmitter.pair({ method: 'resolve' }); + const resolvedImports = listOfImports.map( ([importedFile, importsOnly]) => { let resolved: string | null = null; try { @@ -463,6 +495,17 @@ export function prepareForEvalSync( }; } ); + onResolveFinished(); + + eventEmitter.single({ + type: 'dependency', + file: item.entrypoint.name, + only: item.entrypoint.only, + imports: resolvedImports.map(({ resolved, importsOnly }) => ({ + from: resolved, + what: importsOnly, + })), + }); processImports( babel, @@ -472,9 +515,17 @@ export function prepareForEvalSync( pluginOptions, options, item, - resolvedImports + resolvedImports, + eventEmitter ); } else { + eventEmitter.single({ + type: 'dependency', + file: item.entrypoint.name, + only: item.entrypoint.only, + imports: [], + }); + log('stage-1', '%s has no imports', item.entrypoint.name); } @@ -502,7 +553,8 @@ export default async function prepareForEval( ) => Promise, partialEntrypoint: Pick, pluginOptions: StrictOptions, - options: Pick + options: Pick, + eventEmitter = EventEmitter.dummy ): Promise { /* * This method can be run simultaneously for multiple files. @@ -523,7 +575,8 @@ export default async function prepareForEval( partialEntrypoint.only, partialEntrypoint.code, pluginOptions, - options + options, + eventEmitter ); if (entrypoint === 'ignored') { @@ -544,7 +597,8 @@ export default async function prepareForEval( cache, pluginOptions, options, - item + item, + eventEmitter ); if (processResult === 'skip') { continue; @@ -552,46 +606,57 @@ export default async function prepareForEval( const { imports, result, only: mergedOnly } = processResult; - if (imports && imports.size > 0) { + const listOfImports = Array.from(imports?.entries() ?? []); + if (listOfImports.length > 0) { + const onResolveFinished = eventEmitter.pair({ method: 'resolve' }); const resolvedImports = await Promise.all( - Array.from(imports?.entries() ?? []).map( - async ([importedFile, importsOnly]) => { - let resolved: string | null = null; - try { - resolved = await resolve( - importedFile, - item.entrypoint.name, - item.stack - ); - } catch (err) { - log( - 'stage-1:async-resolve', - `❌ cannot resolve %s in %s: %O`, - importedFile, - item.entrypoint.name, - err - ); - } - - if (resolved !== null) { - log( - 'stage-1:async-resolve', - `✅ %s (%o) in %s -> %s`, - importedFile, - importsOnly, - item.entrypoint.name, - resolved - ); - } - - return { + listOfImports.map(async ([importedFile, importsOnly]) => { + let resolved: string | null = null; + try { + resolved = await resolve( + importedFile, + item.entrypoint.name, + item.stack + ); + } catch (err) { + log( + 'stage-1:async-resolve', + `❌ cannot resolve %s in %s: %O`, + importedFile, + item.entrypoint.name, + err + ); + } + + if (resolved !== null) { + log( + 'stage-1:async-resolve', + `✅ %s (%o) in %s -> %s`, importedFile, importsOnly, - resolved, - }; + item.entrypoint.name, + resolved + ); } - ) + + return { + importedFile, + importsOnly, + resolved, + }; + }) ); + onResolveFinished(); + + eventEmitter.single({ + type: 'dependency', + file: item.entrypoint.name, + only: item.entrypoint.only, + imports: resolvedImports.map(({ resolved, importsOnly }) => ({ + from: resolved, + what: importsOnly, + })), + }); processImports( babel, @@ -601,9 +666,17 @@ export default async function prepareForEval( pluginOptions, options, item, - resolvedImports + resolvedImports, + eventEmitter ); } else { + eventEmitter.single({ + type: 'dependency', + file: item.entrypoint.name, + only: item.entrypoint.only, + imports: [], + }); + log('stage-1', '%s has no imports', item.entrypoint.name); } diff --git a/packages/babel/src/transform.ts b/packages/babel/src/transform.ts index 2e845baf8..448c0b196 100644 --- a/packages/babel/src/transform.ts +++ b/packages/babel/src/transform.ts @@ -10,6 +10,7 @@ import type { TransformOptions } from '@babel/core'; import * as babel from '@babel/core'; +import { EventEmitter } from '@linaria/utils'; import type { StrictOptions } from '@linaria/utils'; import { TransformCacheCollection } from './cache'; @@ -33,7 +34,7 @@ function syncStages( prepareStageResult: ITransformFileResult | undefined, babelConfig: TransformOptions, cache: TransformCacheCollection, - eventEmitter?: (ev: unknown) => void + eventEmitter = EventEmitter.dummy ) { const { filename } = options; const ast = cache.originalASTCache.get(filename) ?? 'ignored'; @@ -52,7 +53,7 @@ function syncStages( // *** 2nd stage *** - eventEmitter?.({ type: 'transform:stage-2:start', filename }); + const finishStage2Event = eventEmitter.pair({ stage: 'stage-2', filename }); const evalStageResult = evalStage( cache, @@ -61,7 +62,7 @@ function syncStages( filename ); - eventEmitter?.({ type: 'transform:stage-2:finish', filename }); + finishStage2Event(); if (evalStageResult === null) { return { @@ -74,7 +75,7 @@ function syncStages( // *** 3rd stage *** - eventEmitter?.({ type: 'transform:stage-3:start', filename }); + const finishStage3Event = eventEmitter.pair({ stage: 'stage-3', filename }); const collectStageResult = prepareForRuntime( babel, @@ -86,7 +87,7 @@ function syncStages( babelConfig ); - eventEmitter?.({ type: 'transform:stage-3:finish', filename }); + finishStage3Event(); if (!withLinariaMetadata(collectStageResult.metadata)) { return { @@ -97,7 +98,7 @@ function syncStages( // *** 4th stage - eventEmitter?.({ type: 'transform:stage-4:start', filename }); + const finishStage4Event = eventEmitter.pair({ stage: 'stage-4', filename }); const extractStageResult = extractStage( collectStageResult.metadata.linaria.processors, @@ -105,7 +106,7 @@ function syncStages( options ); - eventEmitter?.({ type: 'transform:stage-4:finish', filename }); + finishStage4Event(); return { ...extractStageResult, @@ -125,12 +126,12 @@ export function transformSync( syncResolve: (what: string, importer: string, stack: string[]) => string, babelConfig: TransformOptions = {}, cache = new TransformCacheCollection(), - eventEmitter?: (ev: unknown) => void + eventEmitter = EventEmitter.dummy ): Result { const { filename } = options; // *** 1st stage *** - eventEmitter?.({ type: 'transform:stage-1:start', filename }); + const finishEvent = eventEmitter.pair({ stage: 'stage-1', filename }); const entrypoint = { name: options.filename, @@ -148,7 +149,7 @@ export function transformSync( options ); - eventEmitter?.({ type: 'transform:stage-1:finish', filename }); + finishEvent(); // *** The rest of the stages are synchronous *** @@ -173,7 +174,7 @@ export default async function transform( ) => Promise, babelConfig: TransformOptions = {}, cache = new TransformCacheCollection(), - eventEmitter?: (ev: unknown) => void + eventEmitter = EventEmitter.dummy ): Promise { const { filename } = options; @@ -183,7 +184,7 @@ export default async function transform( // *** 1st stage *** - eventEmitter?.({ type: 'transform:stage-1:start', filename }); + const finishEvent = eventEmitter.pair({ stage: 'stage-1', filename }); const entrypoint = { name: filename, @@ -198,10 +199,11 @@ export default async function transform( asyncResolve, entrypoint, pluginOptions, - options + options, + eventEmitter ); - eventEmitter?.({ type: 'transform:stage-1:finish', filename }); + finishEvent(); // *** The rest of the stages are synchronous *** diff --git a/packages/cli/src/linaria.ts b/packages/cli/src/linaria.ts index 55e6befa6..060728036 100644 --- a/packages/cli/src/linaria.ts +++ b/packages/cli/src/linaria.ts @@ -12,7 +12,7 @@ import normalize from 'normalize-path'; import yargs from 'yargs'; import { TransformCacheCollection, transform } from '@linaria/babel-preset'; -import { asyncResolveFallback } from '@linaria/utils'; +import { asyncResolveFallback, createPerfMeter } from '@linaria/utils'; const modulesOptions = [ 'commonjs', @@ -29,6 +29,7 @@ const argv = yargs type: 'string', description: 'Path to a config file', requiresArg: true, + coerce: path.resolve, }) .option('out-dir', { alias: 'o', @@ -36,6 +37,7 @@ const argv = yargs description: 'Output directory for the extracted CSS files', demandOption: true, requiresArg: true, + coerce: path.resolve, }) .option('source-maps', { alias: 's', @@ -49,6 +51,7 @@ const argv = yargs description: 'Directory containing the source JS files', demandOption: true, requiresArg: true, + coerce: path.resolve, }) .option('insert-css-requires', { alias: 'i', @@ -56,6 +59,7 @@ const argv = yargs description: 'Directory containing JS files to insert require statements for the CSS files', requiresArg: true, + coerce: path.resolve, }) .option('transform', { alias: 't', @@ -110,7 +114,8 @@ function resolveOutputFilename( } async function processFiles(files: (number | string)[], options: Options) { - const startedAt = performance.now(); + const { emitter, onDone } = createPerfMeter(); + let count = 0; const resolvedFiles = files.reduce( @@ -125,30 +130,6 @@ async function processFiles(files: (number | string)[], options: Options) { ); const cache = new TransformCacheCollection(); - const timings = new Map(); - const addTiming = (key: string, value: number) => { - timings.set(key, Math.round((timings.get(key) || 0) + value)); - }; - - const startTimes = new Map(); - const onEvent = (unknownEvent: unknown) => { - const ev = unknownEvent as { type: string; filename: string }; - const [, stage, type] = ev.type.split(':'); - if (type === 'start') { - startTimes.set(ev.filename, performance.now()); - startTimes.set(stage, performance.now()); - } else { - const startTime = startTimes.get(ev.filename); - if (startTime) { - addTiming(ev.filename, performance.now() - startTime); - } - const stageStartTime = startTimes.get(stage); - if (stageStartTime) { - addTiming(stage, performance.now() - stageStartTime); - } - } - }; - const modifiedFiles: { name: string; content: string }[] = []; // eslint-disable-next-line no-restricted-syntax @@ -177,7 +158,7 @@ async function processFiles(files: (number | string)[], options: Options) { asyncResolveFallback, {}, cache, - onEvent + emitter ); if (cssText) { @@ -242,27 +223,7 @@ async function processFiles(files: (number | string)[], options: Options) { console.log(`Successfully extracted ${count} CSS files.`); - console.log(`\nTimings:`); - console.log(` Total: ${(performance.now() - startedAt).toFixed()}ms`); - console.log(`\n By stages:`); - let stage = 1; - while (timings.has(`stage-${stage}`)) { - console.log(` Stage ${stage}: ${timings.get(`stage-${stage}`)}ms`); - timings.delete(`stage-${stage}`); - stage += 1; - } - - console.log('\n By files:'); - - const byFiles = Array.from(timings.entries()); - byFiles.sort(([, a], [, b]) => b - a); - byFiles.forEach(([filename, time]) => { - const relativeFilename = path.relative( - options.sourceRoot ?? process.cwd(), - filename - ); - console.log(` ${relativeFilename}: ${time}ms`); - }); + onDone(options.sourceRoot ?? process.cwd()); } processFiles(argv._, { diff --git a/packages/shaker/src/plugins/shaker-plugin.ts b/packages/shaker/src/plugins/shaker-plugin.ts index e7164acd0..c193ddd93 100644 --- a/packages/shaker/src/plugins/shaker-plugin.ts +++ b/packages/shaker/src/plugins/shaker-plugin.ts @@ -337,8 +337,19 @@ export default function shakerPlugin( post(file: BabelFile) { const log = createCustomDebug('shaker', getFileIdx(file.opts.filename!)); + const processedImports = new Set(); const imports = new Map(); - this.imports.forEach(({ imported, source }) => { + const addImport = ({ + imported, + source, + }: { + imported: string; + source: string; + }) => { + if (processedImports.has(`${source}:${imported}`)) { + return; + } + if (!imports.has(source)) { imports.set(source, []); } @@ -346,15 +357,12 @@ export default function shakerPlugin( if (imported) { imports.get(source)!.push(imported); } - }); - this.reexports.forEach(({ imported, source }) => { - if (!imports.has(source)) { - imports.set(source, []); - } + processedImports.add(`${source}:${imported}`); + }; - imports.get(source)!.push(imported); - }); + this.imports.forEach(addImport); + this.reexports.forEach(addImport); log('end', `remaining imports: %O`, imports); diff --git a/packages/testkit/src/prepareCode.test.ts b/packages/testkit/src/prepareCode.test.ts index 2c8b6737d..55aa613b2 100644 --- a/packages/testkit/src/prepareCode.test.ts +++ b/packages/testkit/src/prepareCode.test.ts @@ -9,6 +9,7 @@ import { parseFile, prepareCode, } from '@linaria/babel-preset'; +import { EventEmitter } from '@linaria/utils'; const testCasesDir = join(__dirname, '__fixtures__', 'prepare-code-test-cases'); @@ -64,7 +65,8 @@ describe('prepareCode', () => { pluginOptions, { root, - } + }, + EventEmitter.dummy ); if (entrypoint === 'ignored') { @@ -78,7 +80,8 @@ describe('prepareCode', () => { babel, entrypoint, ast, - pluginOptions + pluginOptions, + EventEmitter.dummy ); expect(transformedCode).toMatchSnapshot('code'); diff --git a/packages/utils/src/EventEmitter.ts b/packages/utils/src/EventEmitter.ts new file mode 100644 index 000000000..6f81b6f61 --- /dev/null +++ b/packages/utils/src/EventEmitter.ts @@ -0,0 +1,22 @@ +export class EventEmitter { + static dummy = new EventEmitter(() => {}); + + constructor( + protected onEvent: ( + labels: Record, + type: 'start' | 'finish' | 'single', + event?: unknown + ) => void + ) {} + + public pair(labels: Record) { + this.onEvent(labels, 'start'); + return () => { + this.onEvent(labels, 'finish'); + }; + } + + public single(labels: Record) { + this.onEvent(labels, 'single'); + } +} diff --git a/packages/utils/src/debug/perfMetter.ts b/packages/utils/src/debug/perfMetter.ts new file mode 100644 index 000000000..4e52af44b --- /dev/null +++ b/packages/utils/src/debug/perfMetter.ts @@ -0,0 +1,144 @@ +/* eslint-disable no-console */ +import path from 'path'; + +import { EventEmitter } from '../EventEmitter'; + +type Timings = Map>; + +export interface IPerfMeterOptions { + filename?: string; + print?: boolean; +} + +interface IProcessedFile { + exports: string[]; + imports: { from: string; what: string[] }[]; + passes: number; +} + +export interface IProcessedEvent { + type: 'dependency'; + file: string; + only: string[]; + imports: { from: string; what: string[] }[]; +} + +function replacer(key: string, value: unknown) { + if (value instanceof Map) { + return Array.from(value.entries()).reduce( + (obj, [k, v]) => ({ + ...obj, + [k]: v, + }), + {} + ); + } + + return value; +} + +function printTimings(timings: Timings, startedAt: number, sourceRoot: string) { + if (timings.size === 0) { + return; + } + + console.log(`\nTimings:`); + console.log(` Total: ${(performance.now() - startedAt).toFixed()}ms`); + + Array.from(timings.entries()).forEach(([label, byLabel]) => { + console.log(`\n By ${label}:`); + + const array = Array.from(byLabel.entries()); + // array.sort(([, a], [, b]) => b - a); + array.forEach(([value, time]) => { + const name = value.startsWith(sourceRoot) + ? path.relative(sourceRoot, value) + : value; + console.log(` ${name}: ${time}ms`); + }); + }); +} + +export const createPerfMeter = ( + options: IPerfMeterOptions | boolean = true +) => { + if (!options) { + return { + emitter: EventEmitter.dummy, + onDone: () => {}, + }; + } + + const startedAt = performance.now(); + const timings: Timings = new Map(); + const addTiming = (label: string, key: string, value: number) => { + if (!timings.has(label)) { + timings.set(label, new Map()); + } + + const forLabel = timings.get(label)!; + forLabel.set(key, Math.round((forLabel.get(key) || 0) + value)); + }; + + const processedFiles = new Map(); + const processDependencyEvent = ({ file, only, imports }: IProcessedEvent) => { + if (!processedFiles.has(file)) { + processedFiles.set(file, { + exports: [], + imports: [], + passes: 0, + }); + } + + const processed = processedFiles.get(file)!; + processed.passes += 1; + processed.exports = only; + processed.imports = imports; + }; + + const processSingleEvent = ( + meta: Record | IProcessedEvent + ) => { + if (meta.type === 'dependency') { + processDependencyEvent(meta as IProcessedEvent); + } + }; + + const startTimes = new Map(); + const emitter = new EventEmitter((meta, type) => { + if (type === 'single') { + processSingleEvent(meta); + return; + } + + if (type === 'start') { + Object.entries(meta).forEach(([label, value]) => { + startTimes.set(`${label}\0${value}`, performance.now()); + }); + } else { + Object.entries(meta).forEach(([label, value]) => { + const startTime = startTimes.get(`${label}\0${value}`); + if (startTime) { + addTiming(label, String(value), performance.now() - startTime); + } + }); + } + }); + + return { + emitter, + onDone: (sourceRoot: string) => { + if (options === true || options.print) { + printTimings(timings, startedAt, sourceRoot); + } + + if (options !== true && options.filename) { + const fs = require('fs'); + fs.writeFileSync( + options.filename, + JSON.stringify({ processedFiles, timings }, replacer, 2) + ); + } + }, + }; +}; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index cdb3added..7f5f4d811 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -16,6 +16,8 @@ export { extractExpression, } from './collectTemplateDependencies'; export { createId } from './createId'; +export { createPerfMeter } from './debug/perfMetter'; +export { EventEmitter } from './EventEmitter'; export { default as findIdentifiers, nonType } from './findIdentifiers'; export { findPackageJSON } from './findPackageJSON'; export { hasEvaluatorMetadata } from './hasEvaluatorMetadata'; @@ -53,6 +55,7 @@ export type { ISideEffectImport, IState, } from './collectExportsAndImports'; +export type { IPerfMeterOptions } from './debug/perfMetter'; export type { IVariableContext } from './IVariableContext'; export type { Artifact, diff --git a/packages/vite/src/index.ts b/packages/vite/src/index.ts index 9fd537b85..8535c51c7 100644 --- a/packages/vite/src/index.ts +++ b/packages/vite/src/index.ts @@ -17,9 +17,11 @@ import { } from '@linaria/babel-preset'; import type { PluginOptions, Preprocessor } from '@linaria/babel-preset'; import { createCustomDebug } from '@linaria/logger'; -import { getFileIdx, syncResolve } from '@linaria/utils'; +import type { IPerfMeterOptions } from '@linaria/utils'; +import { createPerfMeter, getFileIdx, syncResolve } from '@linaria/utils'; type VitePluginOptions = { + debug?: IPerfMeterOptions | false | null | undefined; include?: FilterPattern; exclude?: FilterPattern; sourceMap?: boolean; @@ -31,6 +33,7 @@ export { Plugin }; const emptyConfig = {}; export default function linaria({ + debug, include, exclude, sourceMap, @@ -43,12 +46,17 @@ export default function linaria({ let config: ResolvedConfig; let devServer: ViteDevServer; + const { emitter, onDone } = createPerfMeter(debug ?? false); + // const targets: { id: string; dependencies: string[] }[] = []; const cache = new TransformCacheCollection(); return { name: 'linaria', enforce: 'post', + buildEnd() { + onDone(process.cwd()); + }, configResolved(resolvedConfig: ResolvedConfig) { config = resolvedConfig; }, @@ -143,7 +151,8 @@ export default function linaria({ }, asyncResolve, emptyConfig, - cache + cache, + emitter ); let { cssText, dependencies } = result; diff --git a/packages/webpack5-loader/package.json b/packages/webpack5-loader/package.json index 6d4919672..f86b42f7e 100644 --- a/packages/webpack5-loader/package.json +++ b/packages/webpack5-loader/package.json @@ -34,6 +34,7 @@ "dependencies": { "@linaria/babel-preset": "workspace:^", "@linaria/logger": "workspace:^", + "@linaria/utils": "workspace:^", "enhanced-resolve": "^5.3.1", "mkdirp": "^0.5.1" }, diff --git a/packages/webpack5-loader/src/LinariaDebugPlugin.ts b/packages/webpack5-loader/src/LinariaDebugPlugin.ts new file mode 100644 index 000000000..c80c09792 --- /dev/null +++ b/packages/webpack5-loader/src/LinariaDebugPlugin.ts @@ -0,0 +1,24 @@ +import type { Compiler } from 'webpack'; + +import type { EventEmitter, IPerfMeterOptions } from '@linaria/utils'; +import { createPerfMeter } from '@linaria/utils'; + +export const sharedState: { + emitter?: EventEmitter; +} = {}; + +export class LinariaDebugPlugin { + private readonly onDone: (root: string) => void; + + constructor(options?: IPerfMeterOptions) { + const { emitter, onDone } = createPerfMeter(options ?? true); + sharedState.emitter = emitter; + this.onDone = onDone; + } + + apply(compiler: Compiler) { + compiler.hooks.shutdown.tap('LinariaDebug', () => { + this.onDone(process.cwd()); + }); + } +} diff --git a/packages/webpack5-loader/src/index.ts b/packages/webpack5-loader/src/index.ts index dd578877c..25b82b3fe 100644 --- a/packages/webpack5-loader/src/index.ts +++ b/packages/webpack5-loader/src/index.ts @@ -13,9 +13,12 @@ import type { Result, Preprocessor } from '@linaria/babel-preset'; import { transform, TransformCacheCollection } from '@linaria/babel-preset'; import { debug } from '@linaria/logger'; +import { sharedState } from './LinariaDebugPlugin'; import type { ICache } from './cache'; import { getCacheInstance } from './cache'; +export { LinariaDebugPlugin } from './LinariaDebugPlugin'; + const outputCssLoader = require.resolve('./outputCssLoader'); type Loader = RawLoaderDefinitionFunction<{ @@ -93,7 +96,8 @@ const webpack5Loader: Loader = function webpack5LoaderPlugin( }, asyncResolve, emptyConfig, - cache + cache, + sharedState.emitter ).then( async (result: Result) => { if (result.cssText) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2aceb2cec..e1afe1aef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -260,6 +260,8 @@ importers: specifier: ^0.4.54 version: 0.4.54(vite@3.2.4) + examples/vpssr-linaria-solid/dist/server: {} + examples/webpack5: dependencies: linaria-website: @@ -1072,6 +1074,9 @@ importers: '@linaria/logger': specifier: workspace:^ version: link:../logger + '@linaria/utils': + specifier: workspace:^ + version: link:../utils enhanced-resolve: specifier: ^5.3.1 version: 5.9.3 diff --git a/website/webpack.config.js b/website/webpack.config.js index 19d422847..a1ee6fa7c 100644 --- a/website/webpack.config.js +++ b/website/webpack.config.js @@ -1,6 +1,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const { join } = require('path'); // eslint-disable-line import/no-extraneous-dependencies const HtmlWebpackPlugin = require('html-webpack-plugin'); +const { LinariaDebugPlugin } = require('@linaria/webpack5-loader'); const dev = process.env.NODE_ENV !== 'production'; @@ -18,6 +19,10 @@ module.exports = { emitOnErrors: false, }, plugins: [ + new LinariaDebugPlugin({ + filename: 'linaria-debug.json', + print: true, + }), new MiniCssExtractPlugin({ filename: 'styles.css' }), new HtmlWebpackPlugin({ title: 'Linaria – zero-runtime CSS in JS library',