From db09696b0fce09f2f204b99cd8de5034e394fe19 Mon Sep 17 00:00:00 2001 From: Anton Evzhakov Date: Wed, 9 Aug 2023 21:12:08 +0300 Subject: [PATCH] test: a bunch of tests for ActionQueue and related code --- .../__tests__/queue/AsyncActionQueue.test.ts | 98 + .../__tests__/queue/GenericQueue.test.ts | 180 ++ .../__tests__/queue/PriorityQueue.test.ts | 74 + .../queue/actions/processEntrypoint.test.ts | 73 + .../__tests__/queue/createEntrypoint.test.ts | 97 + .../__tests__/queue/entrypoint-helpers.ts | 42 + packages/babel/src/cache.ts | 8 +- .../transform-stages/1-prepare-for-eval.ts | 26 +- .../src/transform-stages/queue/ActionQueue.ts | 379 +--- .../queue/GenericActionQueue.ts | 182 ++ .../transform-stages/queue/PriorityQueue.ts | 53 +- .../transform-stages/queue/actions/action.ts | 306 +++ .../queue/actions/addToCodeCache.ts | 6 +- .../queue/actions/explodeReexports.ts | 35 +- .../queue/actions/getExports.ts | 44 +- .../queue/actions/processEntrypoint.ts | 89 +- .../queue/actions/processImports.ts | 64 +- .../queue/actions/resolveImports.ts | 136 +- .../queue/actions/transform.ts | 57 +- .../queue/createEntrypoint.ts | 414 ++-- .../babel/src/transform-stages/queue/types.ts | 123 +- packages/babel/tsconfig.spec.json | 9 + packages/logger/package.json | 4 +- packages/logger/src/index.ts | 26 +- packages/rollup/src/index.ts | 3 +- packages/testkit/package.json | 2 + .../src/__snapshots__/babel.test.ts.snap | 1681 +++++++++++++++++ packages/testkit/src/babel.test.ts | 60 +- packages/testkit/src/prepareCode.test.ts | 17 +- packages/utils/src/EventEmitter.ts | 8 +- .../utils/src/collectExportsAndImports.ts | 24 +- packages/utils/src/debug/perfMetter.ts | 33 +- pnpm-lock.yaml | 18 +- 33 files changed, 3486 insertions(+), 885 deletions(-) create mode 100644 packages/babel/__tests__/queue/AsyncActionQueue.test.ts create mode 100644 packages/babel/__tests__/queue/GenericQueue.test.ts create mode 100644 packages/babel/__tests__/queue/PriorityQueue.test.ts create mode 100644 packages/babel/__tests__/queue/actions/processEntrypoint.test.ts create mode 100644 packages/babel/__tests__/queue/createEntrypoint.test.ts create mode 100644 packages/babel/__tests__/queue/entrypoint-helpers.ts create mode 100644 packages/babel/src/transform-stages/queue/GenericActionQueue.ts create mode 100644 packages/babel/src/transform-stages/queue/actions/action.ts create mode 100644 packages/babel/tsconfig.spec.json diff --git a/packages/babel/__tests__/queue/AsyncActionQueue.test.ts b/packages/babel/__tests__/queue/AsyncActionQueue.test.ts new file mode 100644 index 000000000..01fde99ea --- /dev/null +++ b/packages/babel/__tests__/queue/AsyncActionQueue.test.ts @@ -0,0 +1,98 @@ +/* eslint-disable no-await-in-loop */ +import { EventEmitter, getFileIdx } from '@linaria/utils'; + +import { AsyncActionQueue } from '../../src/transform-stages/queue/ActionQueue'; +import type { + IBaseServices, + Handlers, +} from '../../src/transform-stages/queue/GenericActionQueue'; +import { rootLog } from '../../src/transform-stages/queue/rootLog'; +import type { + IBaseEntrypoint, + IProcessEntrypointAction, +} from '../../src/transform-stages/queue/types'; + +const createEntrypoint = (name: string): IBaseEntrypoint => ({ + name, + idx: getFileIdx(name).toString().padStart(5, '0'), + only: ['default'], + log: rootLog, + parent: null, +}); + +type AsyncHandlers = Handlers | void, IBaseServices>; + +describe('AsyncActionQueue', () => { + let services: IBaseServices; + let handlers: AsyncHandlers; + beforeEach(() => { + handlers = { + addToCodeCache: jest.fn(), + transform: jest.fn(), + explodeReexports: jest.fn(), + processEntrypoint: jest.fn(), + processImports: jest.fn(), + getExports: jest.fn(), + resolveImports: jest.fn(), + }; + + services = { + eventEmitter: EventEmitter.dummy, + }; + }); + + const createQueueFor = ( + name: string, + customHandlers: Partial = {} + ) => { + const entrypoint = createEntrypoint(name); + return new AsyncActionQueue( + services, + { ...handlers, ...customHandlers }, + entrypoint + ); + }; + + it('should call actions according to its weight', async () => { + const processEntrypoint = jest.fn( + (_services: IBaseServices, action: IProcessEntrypointAction) => { + action.next('transform', action.entrypoint, {}); + action.next('addToCodeCache', action.entrypoint, { + data: { + imports: null, + result: { + code: '', + metadata: undefined, + }, + only: [], + }, + }); + action.next('explodeReexports', action.entrypoint, {}); + action.next('processImports', action.entrypoint, { + resolved: [], + }); + action.next('getExports', action.entrypoint, {}); + action.next('resolveImports', action.entrypoint, { + imports: null, + }); + } + ); + + const queue = createQueueFor('/foo/bar.js', { processEntrypoint }); + await queue.runNext(); // processEntrypoint + + const rightOrder: (keyof AsyncHandlers)[] = [ + 'resolveImports', + 'getExports', + 'processImports', + 'explodeReexports', + 'transform', + 'addToCodeCache', + ]; + + for (let i = 0; i < rightOrder.length; i++) { + await queue.runNext(); + expect(handlers[rightOrder[i]]).toHaveBeenCalledTimes(1); + } + }); +}); diff --git a/packages/babel/__tests__/queue/GenericQueue.test.ts b/packages/babel/__tests__/queue/GenericQueue.test.ts new file mode 100644 index 000000000..a77c52e09 --- /dev/null +++ b/packages/babel/__tests__/queue/GenericQueue.test.ts @@ -0,0 +1,180 @@ +/* eslint-disable no-await-in-loop */ +import { EventEmitter, getFileIdx } from '@linaria/utils'; + +import { + AsyncActionQueue, + SyncActionQueue, +} from '../../src/transform-stages/queue/ActionQueue'; +import type { + IBaseServices, + Handlers, + Handler, +} from '../../src/transform-stages/queue/GenericActionQueue'; +import { rootLog } from '../../src/transform-stages/queue/rootLog'; +import type { + IBaseEntrypoint, + IGetExportsAction, + IProcessEntrypointAction, + IBaseAction, +} from '../../src/transform-stages/queue/types'; + +const createEntrypoint = (name: string): IBaseEntrypoint => ({ + name, + idx: getFileIdx(name).toString().padStart(5, '0'), + only: ['default'], + log: rootLog, + parent: null, +}); + +type Res = Promise | void; +type UniversalHandlers = Handlers; + +type GetHandler = Handler; + +type Queues = typeof AsyncActionQueue | typeof SyncActionQueue; + +describe.each<[string, Queues]>([ + ['AsyncActionQueue', AsyncActionQueue], + ['SyncActionQueue', SyncActionQueue], +])('%s', (_name, Queue) => { + let services: IBaseServices; + let handlers: UniversalHandlers; + beforeEach(() => { + handlers = { + addToCodeCache: jest.fn(), + transform: jest.fn(), + explodeReexports: jest.fn(), + processEntrypoint: jest.fn(), + processImports: jest.fn(), + getExports: jest.fn(), + resolveImports: jest.fn(), + }; + + services = { + eventEmitter: EventEmitter.dummy, + }; + }); + + const createQueueFor = ( + name: string, + customHandlers: Partial = {} + ) => { + const entrypoint = createEntrypoint(name); + return new Queue(services, { ...handlers, ...customHandlers }, entrypoint); + }; + + describe('base', () => { + it('should be defined', () => { + expect(Queue).toBeDefined(); + }); + + it('should create queue', () => { + const queue = createQueueFor('/foo/bar.js'); + expect(queue).toBeDefined(); + expect(queue.isEmpty()).toBe(false); + Object.values(handlers).forEach((handler) => { + expect(handler).not.toHaveBeenCalled(); + }); + }); + + it('should run processEntrypoint', () => { + const queue = createQueueFor('/foo/bar.js'); + queue.runNext(); + expect(handlers.processEntrypoint).toHaveBeenCalledTimes(1); + expect(queue.isEmpty()).toBe(true); + }); + + it('should process next calls', async () => { + const processEntrypoint: GetHandler = ( + _services, + action + ) => { + action.next('transform', action.entrypoint, {}); + }; + + const queue = createQueueFor('/foo/bar.js', { processEntrypoint }); + await queue.runNext(); + expect(queue.isEmpty()).toBe(false); + await queue.runNext(); + expect(queue.isEmpty()).toBe(true); + expect(handlers.transform).toHaveBeenCalledTimes(1); + }); + + it('should call actions according to its weight', async () => { + const processEntrypoint: GetHandler = ( + _services, + action + ) => { + action.next('transform', action.entrypoint, {}); + action.next('addToCodeCache', action.entrypoint, { + data: { + imports: null, + result: { + code: '', + metadata: undefined, + }, + only: [], + }, + }); + action.next('explodeReexports', action.entrypoint, {}); + action.next('processImports', action.entrypoint, { + resolved: [], + }); + action.next('getExports', action.entrypoint, {}); + action.next('resolveImports', action.entrypoint, { + imports: null, + }); + }; + + const queue = createQueueFor('/foo/bar.js', { processEntrypoint }); + await queue.runNext(); // processEntrypoint + + const rightOrder: (keyof UniversalHandlers)[] = [ + 'resolveImports', + 'getExports', + 'processImports', + 'explodeReexports', + 'transform', + 'addToCodeCache', + ]; + + for (let i = 0; i < rightOrder.length; i++) { + await queue.runNext(); + expect(handlers[rightOrder[i]]).toHaveBeenCalledTimes(1); + } + }); + }); + + it('should work with events', async () => { + const exports: string[] = ['resolved']; + const onGetExports = jest.fn(); + + const processEntrypoint: GetHandler = ( + _services, + action + ) => { + action + .next('getExports', action.entrypoint, {}) + .on('resolve', onGetExports); + }; + + const getExports: GetHandler = ( + _services, + _action, + callbacks + ) => { + callbacks.resolve(exports); + }; + + const queue = createQueueFor('/foo/bar.js', { + processEntrypoint, + getExports, + }); + + while (!queue.isEmpty()) { + await queue.runNext(); + } + + expect(onGetExports).toHaveBeenCalledWith(exports); + }); +}); diff --git a/packages/babel/__tests__/queue/PriorityQueue.test.ts b/packages/babel/__tests__/queue/PriorityQueue.test.ts new file mode 100644 index 000000000..83e42e9f6 --- /dev/null +++ b/packages/babel/__tests__/queue/PriorityQueue.test.ts @@ -0,0 +1,74 @@ +import { PriorityQueue } from '../../src/transform-stages/queue/PriorityQueue'; +import { rootLog } from '../../src/transform-stages/queue/rootLog'; + +class NumberQueue extends PriorityQueue { + constructor() { + super( + rootLog, + (i) => i.toString(), + (a, b) => a < b + ); + } + + public delete(item: string) { + super.delete(item); + } + + public dequeue() { + return super.dequeue(); + } + + public enqueue(item: number) { + super.enqueue(item); + } + + public dump() { + const result = []; + while (!this.isEmpty()) { + result.push(this.dequeue()); + } + + return result; + } +} + +describe('PriorityQueue', () => { + it('should be defined', () => { + expect(PriorityQueue).toBeDefined(); + }); + + describe('Simple queue of numbers', () => { + describe('emptiness', () => { + it('should be empty', () => { + const queue = new NumberQueue(); + expect(queue.isEmpty()).toBe(true); + }); + + it('should not be empty', () => { + const queue = new NumberQueue(); + queue.enqueue(1); + expect(queue.isEmpty()).toBe(false); + }); + + it('should be empty after dequeue', () => { + const queue = new NumberQueue(); + queue.enqueue(1); + queue.dequeue(); + expect(queue.isEmpty()).toBe(true); + }); + }); + + it('should dequeue in order', () => { + const queue = new NumberQueue(); + [2, 1, 3].forEach((i) => queue.enqueue(i)); + expect(queue.dump()).toEqual([3, 2, 1]); + }); + + it('should dequeue in order after delete', () => { + const queue = new NumberQueue(); + [2, 1, 4, 3, 5].forEach((i) => queue.enqueue(i)); + queue.delete('3'); + expect(queue.dump()).toEqual([5, 4, 2, 1]); + }); + }); +}); diff --git a/packages/babel/__tests__/queue/actions/processEntrypoint.test.ts b/packages/babel/__tests__/queue/actions/processEntrypoint.test.ts new file mode 100644 index 000000000..579653b05 --- /dev/null +++ b/packages/babel/__tests__/queue/actions/processEntrypoint.test.ts @@ -0,0 +1,73 @@ +import { EventEmitter } from '@linaria/utils'; + +import { TransformCacheCollection } from '../../../src'; +import { createAction } from '../../../src/transform-stages/queue/actions/action'; +import { processEntrypoint } from '../../../src/transform-stages/queue/actions/processEntrypoint'; +import type { Next, Services } from '../../../src/transform-stages/queue/types'; +import { createEntrypoint, fakeLoadAndParse } from '../entrypoint-helpers'; + +describe('processEntrypoint', () => { + let services: Pick; + const nextNext = jest.fn(); + const next = jest.fn, Parameters>((type, ep, data) => + createAction(type, ep, data, nextNext) + ); + + beforeEach(() => { + services = { + cache: new TransformCacheCollection(), + eventEmitter: EventEmitter.dummy, + }; + + fakeLoadAndParse.mockClear(); + next.mockClear(); + nextNext.mockClear(); + }); + + it('should emit explodeReexports and transform actions', async () => { + const fooBarDefault = createEntrypoint(services, '/foo/bar.js', [ + 'default', + ]); + + const action = createAction( + 'processEntrypoint', + fooBarDefault, + {}, + next as Next + ); + + processEntrypoint(services, action); + + expect(next).toHaveBeenCalledTimes(2); + expect(next).toHaveBeenNthCalledWith( + 1, + 'explodeReexports', + fooBarDefault, + {} + ); + + expect(next).toHaveBeenNthCalledWith(2, 'transform', fooBarDefault, {}); + }); + + it('should emit processEntrypoint if entrypoint was superseded', async () => { + const fooBarDefault = createEntrypoint(services, '/foo/bar.js', [ + 'default', + ]); + + const action = createAction( + 'processEntrypoint', + fooBarDefault, + {}, + next as Next + ); + + processEntrypoint(services, action); + + expect(next).toHaveBeenCalledTimes(2); + + const fooBarNamed = createEntrypoint(services, '/foo/bar.js', ['named']); + + expect(next).toHaveBeenCalledTimes(3); + expect(next).toHaveBeenLastCalledWith('processEntrypoint', fooBarNamed, {}); + }); +}); diff --git a/packages/babel/__tests__/queue/createEntrypoint.test.ts b/packages/babel/__tests__/queue/createEntrypoint.test.ts new file mode 100644 index 000000000..41e7fbc61 --- /dev/null +++ b/packages/babel/__tests__/queue/createEntrypoint.test.ts @@ -0,0 +1,97 @@ +import { EventEmitter } from '@linaria/utils'; + +import { TransformCacheCollection } from '../../src'; +import { onSupersede } from '../../src/transform-stages/queue/createEntrypoint'; +import type { Services } from '../../src/transform-stages/queue/types'; + +import { createEntrypoint, fakeLoadAndParse } from './entrypoint-helpers'; + +describe('createEntrypoint', () => { + let services: Pick; + + beforeEach(() => { + services = { + cache: new TransformCacheCollection(), + eventEmitter: EventEmitter.dummy, + }; + + fakeLoadAndParse.mockClear(); + }); + + it('should create a new entrypoint', () => { + const entrypoint = createEntrypoint(services, '/foo/bar.js', ['default']); + expect(entrypoint).toMatchObject({ + name: '/foo/bar.js', + only: ['default'], + parent: null, + }); + }); + + it('should take from cache', () => { + const entrypoint1 = createEntrypoint(services, '/foo/bar.js', ['default']); + const entrypoint2 = createEntrypoint(services, '/foo/bar.js', ['default']); + expect(entrypoint1).toBe(entrypoint2); + }); + + it('should not take from cache if path differs', () => { + const entrypoint1 = createEntrypoint(services, '/foo/bar.js', ['default']); + const entrypoint2 = createEntrypoint(services, '/foo/baz.js', ['default']); + expect(entrypoint1).not.toBe(entrypoint2); + expect(entrypoint1).toMatchObject({ + name: '/foo/bar.js', + only: ['default'], + }); + expect(entrypoint2).toMatchObject({ + name: '/foo/baz.js', + only: ['default'], + }); + }); + + it('should not take from cache if only differs', () => { + const entrypoint1 = createEntrypoint(services, '/foo/bar.js', ['default']); + const entrypoint2 = createEntrypoint(services, '/foo/bar.js', ['named']); + expect(entrypoint1).not.toBe(entrypoint2); + expect(entrypoint2).toMatchObject({ + name: '/foo/bar.js', + only: ['default', 'named'], + }); + }); + + it('should take from cache if only is subset of cached', () => { + const entrypoint1 = createEntrypoint(services, '/foo/bar.js', [ + 'default', + 'named', + ]); + const entrypoint2 = createEntrypoint(services, '/foo/bar.js', ['default']); + expect(entrypoint1).toBe(entrypoint2); + }); + + it('should take from cache if wildcard is cached', () => { + const entrypoint1 = createEntrypoint(services, '/foo/bar.js', ['*']); + const entrypoint2 = createEntrypoint(services, '/foo/bar.js', ['default']); + expect(entrypoint1).toBe(entrypoint2); + }); + + it('should call callback if entrypoint was superseded', () => { + const callback = jest.fn(); + const entrypoint1 = createEntrypoint(services, '/foo/bar.js', ['default']); + + onSupersede(entrypoint1, callback); + + const entrypoint2 = createEntrypoint(services, '/foo/bar.js', ['named']); + expect(entrypoint1).not.toBe(entrypoint2); + expect(callback).toBeCalledWith(entrypoint2); + }); + + it('should not call supersede callback if it was unsubscribed', () => { + const callback = jest.fn(); + const entrypoint1 = createEntrypoint(services, '/foo/bar.js', ['default']); + + const unsubscribe = onSupersede(entrypoint1, callback); + unsubscribe(); + + const entrypoint2 = createEntrypoint(services, '/foo/bar.js', ['named']); + expect(entrypoint1).not.toBe(entrypoint2); + expect(callback).not.toBeCalled(); + }); +}); diff --git a/packages/babel/__tests__/queue/entrypoint-helpers.ts b/packages/babel/__tests__/queue/entrypoint-helpers.ts new file mode 100644 index 000000000..93413c123 --- /dev/null +++ b/packages/babel/__tests__/queue/entrypoint-helpers.ts @@ -0,0 +1,42 @@ +import type { File } from '@babel/types'; + +import type { LoadAndParseFn } from '../../src/transform-stages/queue/createEntrypoint'; +import { genericCreateEntrypoint } from '../../src/transform-stages/queue/createEntrypoint'; +import { rootLog } from '../../src/transform-stages/queue/rootLog'; +import type { + IEntrypoint, + Services, +} from '../../src/transform-stages/queue/types'; + +export const fakeLoadAndParse = jest.fn< + ReturnType>, + [] +>(() => ({ + ast: {} as File, + code: '', + evaluator: jest.fn(), + evalConfig: {}, +})); + +export const createEntrypoint = ( + services: Pick, + name: string, + only: string[], + parent: IEntrypoint | null = null +) => { + const entrypoint = genericCreateEntrypoint( + fakeLoadAndParse, + services, + parent ?? { log: rootLog }, + name, + only, + undefined, + null + ); + + if (entrypoint === 'ignored') { + throw new Error('entrypoint was ignored'); + } + + return entrypoint; +}; diff --git a/packages/babel/src/cache.ts b/packages/babel/src/cache.ts index 32ce0c9bd..7288f37a7 100644 --- a/packages/babel/src/cache.ts +++ b/packages/babel/src/cache.ts @@ -13,7 +13,7 @@ function hashContent(content: string) { } interface ICaches { - entrypoints: Map; + entrypoints: Map>; ignored: Map; resolve: Map; resolveTask: Map< @@ -146,8 +146,12 @@ export class TransformCacheCollection { } public invalidate(cacheName: CacheNames, key: string): void { - loggers[cacheName]('invalidate', key); const cache = this[cacheName] as Map; + if (!cache.has(key)) { + return; + } + + loggers[cacheName]('invalidate', key); cache.delete(key); } 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 8da87b1e7..7e4b5db85 100644 --- a/packages/babel/src/transform-stages/1-prepare-for-eval.ts +++ b/packages/babel/src/transform-stages/1-prepare-for-eval.ts @@ -29,16 +29,15 @@ export function prepareForEvalSync( options: Pick, eventEmitter = EventEmitter.dummy ): ITransformFileResult | undefined { + const services = { babel, cache, options, eventEmitter }; + const entrypoint = createEntrypoint( - babel, - rootLog, - cache, + services, + { log: rootLog }, partialEntrypoint.name, partialEntrypoint.only, partialEntrypoint.code, - pluginOptions, - options, - eventEmitter + pluginOptions ); if (entrypoint === 'ignored') { @@ -46,7 +45,7 @@ export function prepareForEvalSync( } const queue = new SyncActionQueue( - { babel, cache, options, eventEmitter }, + services, { addToCodeCache, explodeReexports, @@ -83,6 +82,8 @@ export default async function prepareForEval( options: Pick, eventEmitter = EventEmitter.dummy ): Promise { + const services = { babel, cache, options, eventEmitter }; + /* * This method can be run simultaneously for multiple files. * A shared cache is accessible for all runs, but each run has its own queue @@ -92,15 +93,12 @@ export default async function prepareForEval( * the combined "only" option. */ const entrypoint = createEntrypoint( - babel, - rootLog, - cache, + services, + { log: rootLog }, partialEntrypoint.name, partialEntrypoint.only, partialEntrypoint.code, - pluginOptions, - options, - eventEmitter + pluginOptions ); if (entrypoint === 'ignored') { @@ -108,7 +106,7 @@ export default async function prepareForEval( } const queue = new AsyncActionQueue( - { babel, cache, options, eventEmitter }, + services, { addToCodeCache, explodeReexports, diff --git a/packages/babel/src/transform-stages/queue/ActionQueue.ts b/packages/babel/src/transform-stages/queue/ActionQueue.ts index b10ed3b8b..768e9b579 100644 --- a/packages/babel/src/transform-stages/queue/ActionQueue.ts +++ b/packages/babel/src/transform-stages/queue/ActionQueue.ts @@ -1,230 +1,12 @@ -// eslint-disable-next-line max-classes-per-file -import { relative, sep } from 'path'; - import type { EventEmitter } from '@linaria/utils'; import type { Core } from '../../babel'; import type { TransformCacheCollection } from '../../cache'; import type { Options } from '../../types'; -import type { - EventsHandlers, - IBaseNode, - IBaseEntrypoint, -} from './PriorityQueue'; -import { PriorityQueue } from './PriorityQueue'; -import type { - ActionQueueItem, - IEntrypoint, - IResolvedImport, - IResolveImportsAction, - BaseAction, -} from './types'; - -const peek = (arr: T[]) => - arr.length > 0 ? arr[arr.length - 1] : undefined; - -export type OnNext> = { - on: ( - type: K, - callback: (...args: TEvents[K]) => void - ) => void; -}; - -export type Next< - TEntrypoint extends IBaseEntrypoint, - TNode extends IBaseNode -> = ( - item: TNode & { type: TType }, - refCount?: number -) => Extract extends IBaseNode< - TEntrypoint, - infer TEvents -> - ? OnNext - : { on: (type: never, callback: never) => void }; - -type Merger = ( - a: T, - b: T, - next: Next -) => void; - -const reprocessEntrypoint: Merger = (a, b, next) => { - const entrypoint: IEntrypoint = { - ...a.entrypoint, - only: Array.from( - new Set([...a.entrypoint.only, ...b.entrypoint.only]) - ).sort(), - }; - - b.entrypoint.log('Superseded by %s', a.entrypoint.log.namespace); - - next({ - type: 'processEntrypoint', - entrypoint, - stack: a.stack, - refCount: (a.refCount ?? 1) + (b.refCount ?? 1), - }); -}; - -const mergers: { - [K in ActionQueueItem['type']]: Merger>; -} = { - processEntrypoint: reprocessEntrypoint, - resolveImports: (a, b, next) => { - const mergedImports = new Map(); - const addOrMerge = (v: string[], k: string) => { - const prev = mergedImports.get(k); - if (prev) { - mergedImports.set(k, Array.from(new Set([...prev, ...v])).sort()); - } else { - mergedImports.set(k, v); - } - }; - - a.imports?.forEach(addOrMerge); - b.imports?.forEach(addOrMerge); - - const merged: IResolveImportsAction = { - ...a, - imports: mergedImports, - }; - - next(merged); - }, - processImports: (a, b, next) => { - const mergedResolved: IResolvedImport[] = []; - const addOrMerge = (v: IResolvedImport) => { - const prev = mergedResolved.find( - (i) => i.importedFile === v.importedFile - ); - if (prev) { - prev.importsOnly = Array.from( - new Set([...prev.importsOnly, ...v.importsOnly]) - ).sort(); - } else { - mergedResolved.push(v); - } - }; - - a.resolved.forEach(addOrMerge); - b.resolved.forEach(addOrMerge); - const merged = { - ...a, - resolved: mergedResolved, - }; - - next(merged); - }, - transform: reprocessEntrypoint, - addToCodeCache: (a, b, next) => { - const aOnly = a.entrypoint.only; - const bOnly = b.entrypoint.only; - if (aOnly.includes('*') || bOnly.every((i) => aOnly.includes(i))) { - next(a); - return; - } - - if (bOnly.includes('*') || aOnly.every((i) => bOnly.includes(i))) { - next(b); - return; - } - - reprocessEntrypoint(a, b, next); - }, - explodeReexports(a, b, next) { - next(a); - }, - getExports(a, b, next) { - next(a); - }, -}; - -const weights: Record = { - addToCodeCache: 0, - explodeReexports: 10, - getExports: 25, - processEntrypoint: 15, - processImports: 20, - resolveImports: 30, - transform: 5, -}; - -function hasLessPriority(a: ActionQueueItem, b: ActionQueueItem) { - if (a.type === b.type) { - const firstA = peek(a.stack); - const firstB = peek(b.stack); - if (a.refCount === b.refCount && firstA && firstB) { - const distanceA = relative(firstA, a.entrypoint.name).split(sep).length; - const distanceB = relative(firstB, b.entrypoint.name).split(sep).length; - return distanceA > distanceB; - } - - return (a.refCount ?? 1) > (b.refCount ?? 1); - } - - return weights[a.type] < weights[b.type]; -} - -const nameOf = (node: BaseAction): string => - `${node.type}:${node.entrypoint.name}`; - -const keyOf = (node: BaseAction): string => nameOf(node); - -function transferCallbacks>( - from: IBaseNode[], - to: IBaseNode -) { - const targetCallbacks = (to.callbacks as EventsHandlers) ?? {}; - - from.forEach((node) => { - if (!node.callbacks) return; - - const callbacks = node.callbacks as EventsHandlers; - Object.keys(callbacks).forEach((key) => { - const name = key as keyof typeof targetCallbacks; - const handlers = callbacks[name]; - if (!handlers) return; - if (!targetCallbacks[name]) { - targetCallbacks[name] = []; - } - - handlers.forEach((handler) => { - if (!handler) return; - targetCallbacks[name]!.push(handler); - }); - }); - }); - - // eslint-disable-next-line no-param-reassign - to.callbacks = targetCallbacks; -} - -function merge( - a: ActionQueueItem, - b: ActionQueueItem, - insert: (item: ActionQueueItem) => void -): void { - if (a.type === b.type) { - ( - mergers[a.type] as ( - a: ActionQueueItem, - b: ActionQueueItem, - next: (item: ActionQueueItem) => void - ) => void - )(a, b, (item) => { - // Merge callbacks - transferCallbacks([a, b], item); - - insert(item); - }); - - return; - } - - throw new Error(`Cannot merge ${nameOf(a)} with ${nameOf(b)}`); -} +import type { IBaseServices } from './GenericActionQueue'; +import { GenericActionQueue } from './GenericActionQueue'; +import type { ActionQueueItem, IEntrypoint } from './types'; type Services = { babel: Core; @@ -233,152 +15,45 @@ type Services = { eventEmitter: EventEmitter; }; -type GetCallbacks = TAction extends IBaseNode< - IEntrypoint, - infer TEvents -> - ? { - [K in keyof TEvents]: (...args: TEvents[K]) => void; - } - : never; - -type Handler = ( - services: Services, - action: TAction, - next: Next, - callbacks: GetCallbacks -) => TRes; - -class GenericActionQueue extends PriorityQueue< - IEntrypoint, - ActionQueueItem -> { - constructor( - protected services: Services, - protected handlers: { - [K in ActionQueueItem['type']]: Handler< - Extract, - TRes - >; - }, - entrypoint: IEntrypoint - ) { - const log = entrypoint.log.extend('queue'); - - super(log, keyOf, merge, hasLessPriority); - - log('Created for entrypoint %s', entrypoint.name); - - this.enqueue({ - type: 'processEntrypoint', - entrypoint, - stack: [], - refCount: 1, - }); - } - - protected next( - item: ActionQueueItem & { type: TType }, - refCount = item.refCount ?? 1 - ) { - type ResultType = Extract< - ActionQueueItem, - { type: TType } - > extends BaseAction - ? OnNext - : { on: (type: never, callback: never) => void }; - - const callbacks: Record void)[]> = - item.callbacks ?? {}; - - this.enqueue({ - ...item, - refCount, - callbacks, - }); - - const on: ResultType['on'] = ( - type: string, - callback: (...args: unknown[]) => void - ) => { - if (!callbacks[type]) { - callbacks[type] = []; - } - - callbacks[type]!.push(callback); - }; - - return { - on, - } as ResultType; - } - - protected handle(action: TAction): TRes { - const { eventEmitter } = this.services; - const handler = this.handlers[action.type] as Handler; - - eventEmitter.single({ - type: 'queue-action', - action: action.type, - file: action.entrypoint.name, - args: action.entrypoint.only, - }); - - const next = this.next.bind(this) as Next; - - type Callbacks = GetCallbacks; - const allCallbacks = action.callbacks as Record< - keyof Callbacks, - ((...args: unknown[]) => void)[] | undefined - >; - - const callbacks = new Proxy({} as Callbacks, { - get: (target, prop) => { - const callbackName = prop.toString() as keyof Callbacks; - return (...args: unknown[]) => { - if (!action.callbacks) { - return; - } - - eventEmitter.single({ - type: 'queue-action', - action: `${action.type}:${callbackName.toString()}`, - file: action.entrypoint.name, - args, - }); - - allCallbacks[callbackName]?.forEach((cb) => cb(...args)); - }; - }, - }); - - return eventEmitter.pair( - { - method: `queue:${action.type}`, - }, - () => handler(this.services, action, next, callbacks) - ); - } -} - -export class SyncActionQueue extends GenericActionQueue { +export class SyncActionQueue< + TServices extends IBaseServices +> extends GenericActionQueue { public runNext() { const next = this.dequeue(); if (!next) { return; } + next.entrypoint.log('Start %s from %r', next.type, this.logRef); this.handle(next); + next.entrypoint.log('Finish %s from %r', next.type, this.logRef); } } -export class AsyncActionQueue extends GenericActionQueue | void> { +export class AsyncActionQueue< + TServices extends IBaseServices +> extends GenericActionQueue | void, TServices> { + private static taskCache = new WeakMap< + ActionQueueItem, + Promise | void + >(); + public async runNext() { const next = this.dequeue(); if (!next) { return; } - await this.handle(next); + next.entrypoint.log('Start %s from %r', next.type, this.logRef); + + // Do not run same task twice + if (!AsyncActionQueue.taskCache.has(next)) { + AsyncActionQueue.taskCache.set(next, this.handle(next)); + } else { + next.entrypoint.log('Reuse %s from another queue', next.type); + } + + await AsyncActionQueue.taskCache.get(next); + next.entrypoint.log('Finish %s from %r', next.type, this.logRef); } } diff --git a/packages/babel/src/transform-stages/queue/GenericActionQueue.ts b/packages/babel/src/transform-stages/queue/GenericActionQueue.ts new file mode 100644 index 000000000..7a82d760b --- /dev/null +++ b/packages/babel/src/transform-stages/queue/GenericActionQueue.ts @@ -0,0 +1,182 @@ +import { relative, sep } from 'path'; + +import type { EventEmitter } from '@linaria/utils'; + +import { PriorityQueue } from './PriorityQueue'; +import { createAction, getRefsCount, keyOf } from './actions/action'; +import type { + IBaseEntrypoint, + DataOf, + ActionByType, + IBaseAction, + ActionQueueItem, + EventEmitters, +} from './types'; + +export interface IBaseServices { + eventEmitter: EventEmitter; +} + +const weights: Record = { + addToCodeCache: 0, + transform: 5, + explodeReexports: 10, + processEntrypoint: 15, + processImports: 20, + getExports: 25, + resolveImports: 30, +}; + +function hasLessPriority(a: IBaseAction, b: IBaseAction) { + if (a.type === b.type) { + const parentA = a.entrypoint.parent?.name; + const parentB = b.entrypoint.parent?.name; + const refCountA = getRefsCount(a.entrypoint); + const refCountB = getRefsCount(b.entrypoint); + if (refCountA === refCountB && parentA && parentB) { + const distanceA = relative(parentA, a.entrypoint.name).split(sep).length; + const distanceB = relative(parentB, b.entrypoint.name).split(sep).length; + return distanceA > distanceB; + } + + return refCountA > refCountB; + } + + return weights[a.type] < weights[b.type]; +} + +export type Handler< + TServices extends IBaseServices, + TAction extends IBaseAction, + TRes +> = ( + services: TServices, + action: TAction, + callbacks: EventEmitters< + TAction extends IBaseAction + ? TEvents + : Record + > +) => TRes; + +export type Handlers = { + [K in ActionQueueItem['type']]: Handler, TRes>; +}; + +export class GenericActionQueue< + TRes, + TServices extends IBaseServices +> extends PriorityQueue { + protected readonly queueIdx: string; + + public get logRef() { + return { + namespace: this.log.namespace, + text: `queue:${this.queueIdx}`, + }; + } + + constructor( + protected services: TServices, + protected handlers: Handlers, + entrypoint: IBaseEntrypoint + ) { + const log = entrypoint.log.extend('queue'); + + super(log, keyOf, hasLessPriority); + + log('Created for entrypoint %s', entrypoint.name); + this.queueIdx = entrypoint.idx; + + this.next('processEntrypoint', entrypoint, {}); + } + + protected override enqueue(newAction: ActionQueueItem) { + const key = keyOf(newAction); + // const idx = this.keys.get(key); + // if (idx !== undefined) { + // // Merge with existing entry + // const oldAction = this.data[idx]; + // const mergeAction = onCollide(oldAction, newAction); + // console.log('mergeAction', mergeAction); + // debugger; + // + // return; + // } + // + super.enqueue(newAction); + + // addQueue(newAction.entrypoint, this); + // newAction.onAbort((reason) => { + // this.log('Aborting %s, reason: %s', key, reason ?? 'unknown'); + // this.delete(key); + // }); + } + + protected next = ( + actionType: TType, + entrypoint: IBaseEntrypoint, + data: DataOf> + ): ActionByType => { + const action = createAction(actionType, entrypoint, data, this.next); + + this.enqueue(action); + + return action; + }; + + protected handle(action: TAction): TRes { + const { eventEmitter } = this.services; + const handler = this.handlers[action.type as TAction['type']] as Handler< + TServices, + TAction, + TRes + >; + + eventEmitter.single({ + type: 'queue-action', + queueIdx: this.queueIdx, + action: action.type, + file: action.entrypoint.name, + args: action.entrypoint.only, + }); + + type Callbacks = EventEmitters< + TAction extends IBaseAction + ? TEvents + : Record + >; + const allCallbacks = action.callbacks as Record< + keyof Callbacks, + ((...args: unknown[]) => void)[] | undefined + >; + + const callbacks = new Proxy({} as Callbacks, { + get: (target, prop) => { + const callbackName = prop.toString() as keyof Callbacks; + return (...args: unknown[]) => { + if (!action.callbacks) { + return; + } + + eventEmitter.single({ + type: 'queue-action', + queueIdx: this.queueIdx, + action: `${action.type}:${callbackName.toString()}`, + file: action.entrypoint.name, + args, + }); + + allCallbacks[callbackName]?.forEach((cb) => cb(...args)); + }; + }, + }); + + return eventEmitter.pair( + { + method: `queue:${action.type}`, + }, + () => handler(this.services, action, callbacks) + ); + } +} diff --git a/packages/babel/src/transform-stages/queue/PriorityQueue.ts b/packages/babel/src/transform-stages/queue/PriorityQueue.ts index 1c9c52c7c..f042044f8 100644 --- a/packages/babel/src/transform-stages/queue/PriorityQueue.ts +++ b/packages/babel/src/transform-stages/queue/PriorityQueue.ts @@ -1,42 +1,13 @@ import type { Debugger } from '@linaria/logger'; -export interface IBaseEntrypoint { - abortSignal?: AbortSignal; - onAbort(fn: () => void): () => void; -} - -export type EventsHandlers> = { - [K in keyof TEvents]?: Array<(...args: TEvents[K]) => void>; -}; - -export interface IBaseNode< - TEntrypoint extends IBaseEntrypoint, - TEvents extends Record = Record, - TCallbacks extends EventsHandlers = EventsHandlers -> { - callbacks?: TCallbacks; - entrypoint: TEntrypoint; - refCount?: number; - stack: string[]; - type: string; -} +export abstract class PriorityQueue { + protected data: Array = []; -export abstract class PriorityQueue< - TEntrypoint extends IBaseEntrypoint, - TNode extends IBaseNode -> { - private data: Array = []; - - private keys: Map = new Map(); + protected keys: Map = new Map(); protected constructor( - private readonly log: Debugger, + protected readonly log: Debugger, private readonly keyOf: (node: TNode) => string, - private readonly merge: ( - a: TNode, - b: TNode, - insert: (node: TNode) => void - ) => void, private readonly hasLessPriority: (a: TNode, b: TNode) => boolean ) {} @@ -44,7 +15,7 @@ export abstract class PriorityQueue< return this.data.length; } - private delete(key: string) { + protected delete(key: string) { const idx = this.keys.get(key); if (idx === undefined) return; @@ -150,22 +121,8 @@ export abstract class PriorityQueue< protected enqueue(newNode: TNode) { const key = this.keyOf(newNode); - const idx = this.keys.get(key); - if (idx !== undefined) { - // Merge with existing entry - const oldNode = this.data[idx]; - this.delete(key); - this.merge(oldNode, newNode, this.enqueue.bind(this)); - - return; - } - this.increaseKey(this.size + 1, newNode); this.log('Enqueued %s: %o', key, this.data.map(this.keyOf)); - newNode.entrypoint.onAbort(() => { - this.log('Aborting %s', key); - this.delete(key); - }); } public isEmpty() { diff --git a/packages/babel/src/transform-stages/queue/actions/action.ts b/packages/babel/src/transform-stages/queue/actions/action.ts new file mode 100644 index 000000000..ba8f8a8f2 --- /dev/null +++ b/packages/babel/src/transform-stages/queue/actions/action.ts @@ -0,0 +1,306 @@ +import type { + ActionQueueItem, + IActionControls, + IBaseAction, + IEntrypoint, + Next, + DataOf, + IResolvedImport, + EventsHandlers, + ActionByType, + IBaseEntrypoint, +} from '../types'; + +type MergeAction = 'reprocess' | 'keep existing' | 'replace existing'; + +// type DriedAction = DataOf & { +// entrypoint: IEntrypoint; +// type: ActionQueueItem['type']; +// }; + +// export type GetCallbacksByAction = +// EventsHandlers< +// TAction extends IBaseAction +// ? TEvents +// : Record +// >; + +// export type GetCallbacksByType = +// GetCallbacksByAction>; + +// export type Merger = ( +// existAction: DriedAction, +// newAction: DriedAction +// ) => MergeAction | DataOf; + +export const nameOf = (node: { + type: string; + entrypoint: { + name: string; + }; +}): string => `${node.type}:${node.entrypoint.name}`; + +export const keyOf = (node: { + type: string; + entrypoint: { + name: string; + }; +}): string => nameOf(node); + +// const defaultMerger = ( +// a: DriedAction, +// b: DriedAction +// ): MergeAction => { +// const aOnly = a.entrypoint.only; +// const bOnly = b.entrypoint.only; +// if (aOnly.includes('*') || bOnly.every((i) => aOnly.includes(i))) { +// return 'keep existing'; +// } +// +// if (bOnly.includes('*') || aOnly.every((i) => bOnly.includes(i))) { +// return 'replace existing'; +// } +// +// return 'reprocess'; +// }; + +// function transferCallbacks>( +// from: { callbacks?: IBaseAction['callbacks'] }[], +// to: { callbacks?: IBaseAction['callbacks'] } +// ) { +// const targetCallbacks = (to.callbacks as EventsHandlers) ?? {}; +// +// from.forEach((node) => { +// if (!node.callbacks) return; +// +// const callbacks = node.callbacks as EventsHandlers; +// Object.keys(callbacks).forEach((key) => { +// const name = key as keyof typeof targetCallbacks; +// const handlers = callbacks[name]; +// if (!handlers) return; +// if (!targetCallbacks[name]) { +// targetCallbacks[name] = []; +// } +// +// handlers.forEach((handler) => { +// if (!handler) return; +// targetCallbacks[name]!.push(handler); +// }); +// }); +// }); +// +// // eslint-disable-next-line no-param-reassign +// to.callbacks = targetCallbacks; +// } + +// const mergers: { +// [K in ActionQueueItem['type']]: Merger>; +// } = { +// processEntrypoint: defaultMerger, +// resolveImports: (a, b) => { +// const mergedImports = new Map(); +// const addOrMerge = (v: string[], k: string) => { +// const prev = mergedImports.get(k); +// if (prev) { +// mergedImports.set(k, Array.from(new Set([...prev, ...v])).sort()); +// } else { +// mergedImports.set(k, v); +// } +// }; +// +// a.imports?.forEach(addOrMerge); +// b.imports?.forEach(addOrMerge); +// +// return { +// imports: mergedImports, +// }; +// }, +// processImports: (a, b) => { +// const mergedResolved: IResolvedImport[] = []; +// const addOrMerge = (v: IResolvedImport) => { +// const prev = mergedResolved.find( +// (i) => i.importedFile === v.importedFile +// ); +// if (prev) { +// prev.importsOnly = Array.from( +// new Set([...prev.importsOnly, ...v.importsOnly]) +// ).sort(); +// } else { +// mergedResolved.push(v); +// } +// }; +// +// a.resolved.forEach(addOrMerge); +// b.resolved.forEach(addOrMerge); +// +// return { +// resolved: mergedResolved, +// }; +// }, +// transform: defaultMerger, +// addToCodeCache: defaultMerger, +// explodeReexports: () => 'keep existing', +// getExports: () => 'keep existing', +// }; + +// export const onCollide: Merger = (existAction, newAction) => { +// if (existAction.type === newAction.type) { +// return (mergers[existAction.type] as Merger)( +// existAction, +// newAction +// ); +// } +// +// throw new Error( +// `Cannot merge ${nameOf(existAction)} with ${nameOf(newAction)}` +// ); +// }; + +// Only one action of each type can be queued per entrypoint +const actionsCache = new WeakMap< + IBaseEntrypoint, + Map +>(); + +const nexts = new WeakMap>(); + +// export function createCombinedAction( +// target: ActionByType, +// source: { +// next: Next; +// callbacks?: Callbacks; +// } +// ) { +// transferCallbacks([source], target); +// +// return target; +// } + +const actionsForEntrypoints = new WeakMap>(); +export const getRefsCount = (entrypoint: IBaseEntrypoint) => + actionsForEntrypoints.get(entrypoint)?.size ?? 0; + +export const addRef = (entrypoint: IBaseEntrypoint, action: IBaseAction) => { + if (!actionsForEntrypoints.has(entrypoint)) { + actionsForEntrypoints.set(entrypoint, new Set()); + } + + actionsForEntrypoints.get(entrypoint)?.add(action); +}; + +function innerCreateAction( + actionType: TType, + entrypoint: IBaseEntrypoint, + data: DataOf>, + next: Next +): ActionByType { + type Events = (ActionByType extends IBaseAction< + IBaseEntrypoint, + infer TEvents + > + ? TEvents + : Record) & + Record; + + if (!actionsCache.has(entrypoint)) { + actionsCache.set(entrypoint, new Map()); + } + + const cache = actionsCache.get(entrypoint)!; + const existing = cache.get(actionType) as ActionByType | undefined; + // if (existing) { + // const dryAction = { + // ...data, + // type: actionType, + // entrypoint, + // }; + // + // const result = onCollide(existing, dryAction); + // if (result === 'keep existing') { + // const combined = createCombinedAction(existing, { callbacks, next }); + // cache.set(actionType, combined); + // return combined; + // } + // + // if (result === 'replace existing') { + // const newAction = { + // ...dryAction, + // callbacks, + // next, + // onAbort: () => {}, + // abort: () => {}, + // } as ActionQueueItem; + // + // const combined = createCombinedAction(newAction, { + // callbacks: existing.callbacks, + // next: existing.next.bind(existing), + // }); + // cache.set(actionType, combined); + // return combined; + // } + // + // if (result === 'reprocess') { + // debugger; + // return createAction('processEntrypoint', entrypoint, {}, next, {}); + // } + // + // const newAction = { + // ...result, + // callbacks, + // type: actionType, + // entrypoint, + // abort: () => {}, + // next, + // onAbort: () => {}, + // } as ActionQueueItem; + // + // const combined = createCombinedAction(newAction, { + // callbacks: existing.callbacks, + // next: existing.next.bind(existing), + // }); + // cache.set(actionType, combined); + // return combined; + // } + type Callbacks = EventsHandlers; + const callbacks: Callbacks = {}; + // const callbacks: GetCallbacksByType = {}; + + const on = ( + type: T, + callback: (...args: Events[T]) => void + ) => { + if (!callback) { + return; + } + + if (!callbacks[type]) { + callbacks[type] = []; + } + + callbacks[type]!.push(callback); + }; + + const newAction = { + ...data, + callbacks, + type: actionType, + entrypoint, + next, + on, + } as ActionByType; + + cache.set(actionType, newAction); + + return newAction; +} + +export function createAction( + actionType: TType, + entrypoint: IBaseEntrypoint, + data: DataOf>, + next: Next +): ActionByType { + const action = innerCreateAction(actionType, entrypoint, data, next); + addRef(entrypoint, action); + return action; +} diff --git a/packages/babel/src/transform-stages/queue/actions/addToCodeCache.ts b/packages/babel/src/transform-stages/queue/actions/addToCodeCache.ts index 97ee45dbf..ac3342813 100644 --- a/packages/babel/src/transform-stages/queue/actions/addToCodeCache.ts +++ b/packages/babel/src/transform-stages/queue/actions/addToCodeCache.ts @@ -2,7 +2,11 @@ import type { IAddToCodeCacheAction, Services } from '../types'; export function addToCodeCache( { cache }: Services, - action: IAddToCodeCacheAction + action: IAddToCodeCacheAction, + callbacks: { + done: () => void; + } ) { cache.add('code', action.entrypoint.name, action.data); + callbacks.done(); } diff --git a/packages/babel/src/transform-stages/queue/actions/explodeReexports.ts b/packages/babel/src/transform-stages/queue/actions/explodeReexports.ts index ddbba5fb5..d400c2ed2 100644 --- a/packages/babel/src/transform-stages/queue/actions/explodeReexports.ts +++ b/packages/babel/src/transform-stages/queue/actions/explodeReexports.ts @@ -1,13 +1,7 @@ import type { ExportAllDeclaration, Node, File } from '@babel/types'; import type { Core } from '../../../babel'; -import type { Next } from '../ActionQueue'; -import type { - ActionQueueItem, - IEntrypoint, - IExplodeReexportsAction, - Services, -} from '../types'; +import type { IExplodeReexportsAction, Services } from '../types'; import { findExportsInImports } from './getExports'; @@ -29,10 +23,14 @@ const getWildcardReexport = (babel: Core, ast: File) => { return reexportsFrom; }; +/** + * Replaces wildcard reexports with named reexports. + * Recursively emits getExports for each reexported module, + * and replaces wildcard with resolved named. + */ export function explodeReexports( services: Services, - action: IExplodeReexportsAction, - next: Next + action: IExplodeReexportsAction ) { const { log, ast } = action.entrypoint; @@ -77,19 +75,16 @@ export function explodeReexports( } }, }); - - next(action); }; // Resolve modules - next({ - type: 'resolveImports', - entrypoint: action.entrypoint, - imports: new Map(reexportsFrom.map((i) => [i.source, []])), - stack: action.stack, - }).on('resolve', (resolvedImports) => { - findExportsInImports(services, action, next, resolvedImports, { - resolve: onResolved, + action + .next('resolveImports', action.entrypoint, { + imports: new Map(reexportsFrom.map((i) => [i.source, []])), + }) + .on('resolve', (resolvedImports) => { + findExportsInImports(services, action, resolvedImports, { + resolve: onResolved, + }); }); - }); } diff --git a/packages/babel/src/transform-stages/queue/actions/getExports.ts b/packages/babel/src/transform-stages/queue/actions/getExports.ts index 3493b0f03..f856d48fc 100644 --- a/packages/babel/src/transform-stages/queue/actions/getExports.ts +++ b/packages/babel/src/transform-stages/queue/actions/getExports.ts @@ -1,21 +1,17 @@ import type { IReexport } from '@linaria/utils'; import { collectExportsAndImports } from '@linaria/utils'; -import type { Next } from '../ActionQueue'; import { createEntrypoint } from '../createEntrypoint'; import type { + IExplodeReexportsAction, IGetExportsAction, IResolvedImport, Services, - BaseAction, - ActionQueueItem, - IEntrypoint, } from '../types'; export function findExportsInImports( - { babel, cache, eventEmitter, options }: Services, - action: BaseAction, - next: Next, + services: Services, + action: IGetExportsAction | IExplodeReexportsAction, imports: IResolvedImport[], callbacks: { resolve: (replacements: Record) => void; @@ -49,16 +45,12 @@ export function findExportsInImports( } const newEntrypoint = createEntrypoint( - babel, - action.entrypoint.log, - cache, + services, + action.entrypoint, resolved, [], undefined, - action.entrypoint.pluginOptions, - options, - eventEmitter, - action.entrypoint.abortSignal + action.entrypoint.pluginOptions ); if (newEntrypoint === 'ignored') { @@ -66,11 +58,7 @@ export function findExportsInImports( return; } - next({ - type: 'getExports', - entrypoint: newEntrypoint, - stack: [newEntrypoint.name, ...action.stack], - }).on('resolve', (exports) => { + action.next('getExports', newEntrypoint, {}).on('resolve', (exports) => { onResolve({ [imp.importedFile]: exports, }); @@ -81,7 +69,6 @@ export function findExportsInImports( export function getExports( services: Services, action: IGetExportsAction, - next: Next, callbacks: { resolve: (result: string[]) => void } ) { const { entrypoint } = action; @@ -117,16 +104,15 @@ export function getExports( callbacks.resolve(result); }; - next({ - type: 'resolveImports', - entrypoint: action.entrypoint, - imports: new Map(withWildcardReexport.map((i) => [i.source, []])), - stack: action.stack, - }).on('resolve', (resolvedImports) => { - findExportsInImports(services, action, next, resolvedImports, { - resolve: onResolved, + action + .next('resolveImports', action.entrypoint, { + imports: new Map(withWildcardReexport.map((i) => [i.source, []])), + }) + .on('resolve', (resolvedImports) => { + findExportsInImports(services, action, resolvedImports, { + resolve: onResolved, + }); }); - }); } else { callbacks.resolve(result); } diff --git a/packages/babel/src/transform-stages/queue/actions/processEntrypoint.ts b/packages/babel/src/transform-stages/queue/actions/processEntrypoint.ts index ee9e6c5d5..447961734 100644 --- a/packages/babel/src/transform-stages/queue/actions/processEntrypoint.ts +++ b/packages/babel/src/transform-stages/queue/actions/processEntrypoint.ts @@ -1,90 +1,31 @@ -import type { Next } from '../ActionQueue'; -import { addOnAbort } from '../createEntrypoint'; -import type { - IEntrypoint, - IProcessEntrypointAction, - Services, - ActionQueueItem, -} from '../types'; +import { onSupersede } from '../createEntrypoint'; +import type { IProcessEntrypointAction, IBaseEntrypoint } from '../types'; -const includes = (a: string[], b: string[]) => { - if (a.includes('*')) return true; - if (a.length !== b.length) return false; - return a.every((item, index) => item === b[index]); -}; - -const abortControllers = new WeakMap(); +import { getRefsCount } from './action'; /** * The first stage of processing an entrypoint. - * It checks if the file is already processed and if it is, it checks if the `only` option is the same. - * If it is not, it emits a transformation action for the file with the merged `only` option. + * This stage is responsible for: + * - scheduling the explodeReexports action + * - scheduling the transform action + * - rescheduling itself if the entrypoint is superseded */ -export function processEntrypoint( - { cache }: Services, - action: IProcessEntrypointAction, - next: Next +export function processEntrypoint( + _services: unknown, + action: IProcessEntrypointAction ): void { const { name, only, log } = action.entrypoint; log( 'start processing %s (only: %s, refs: %d)', name, only, - action.refCount ?? 0 + getRefsCount(action.entrypoint) ); - const cached = cache.get('entrypoints', name); - // If we already have a result for this file, we should get a result for merged `only` - const mergedOnly = cached?.only - ? Array.from(new Set([...cached.only, ...only])).sort() - : only; - - if (cached) { - if (includes(cached.only, mergedOnly)) { - log('%s is already processed', name); - return; - } - - log( - '%s is already processed, but with different `only` %o (the cached one %o)', - name, - only, - cached?.only - ); - - // If we already have a result for this file, we should invalidate it - cache.invalidate('eval', name); - - const abortController = abortControllers.get(cached); - if (abortController) { - abortController.abort(); - } - } - - const abortController = new AbortController(); - action.entrypoint.onAbort(() => { - abortController.abort(); + const unsubscribed = onSupersede(action.entrypoint, (newEntrypoint) => { + action.next('processEntrypoint', newEntrypoint, {}); }); - const entrypoint: IEntrypoint = addOnAbort({ - ...action.entrypoint, - only: mergedOnly, - abortSignal: abortController.signal, - }); - - abortControllers.set(entrypoint, abortController); - - cache.add('entrypoints', name, entrypoint); - - next({ - type: 'explodeReexports', - entrypoint: action.entrypoint, - stack: action.stack, - }); - - next({ - type: 'transform', - entrypoint, - stack: action.stack, - }); + action.next('explodeReexports', action.entrypoint, {}); + action.next('transform', action.entrypoint, {}).on('done', unsubscribed); } diff --git a/packages/babel/src/transform-stages/queue/actions/processImports.ts b/packages/babel/src/transform-stages/queue/actions/processImports.ts index 9522028a5..6893ee49a 100644 --- a/packages/babel/src/transform-stages/queue/actions/processImports.ts +++ b/packages/babel/src/transform-stages/queue/actions/processImports.ts @@ -1,67 +1,29 @@ /* eslint-disable no-restricted-syntax,no-continue,no-await-in-loop */ -import type { Next } from '../ActionQueue'; import { createEntrypoint } from '../createEntrypoint'; -import type { - IProcessImportsAction, - Services, - ActionQueueItem, - IEntrypoint, -} from '../types'; +import type { IProcessImportsAction, Services } from '../types'; +/** + * Creates new entrypoints and emits processEntrypoint for each resolved import + */ export function processImports( - { babel, cache, options, eventEmitter }: Services, - action: IProcessImportsAction, - next: Next + services: Services, + action: IProcessImportsAction ) { - const { resolved: resolvedImports, entrypoint, stack } = action; - - for (const { importedFile, importsOnly, resolved } of resolvedImports) { - if (resolved === null) { - entrypoint.log( - `[resolve] ✅ %s in %s is ignored`, - importedFile, - entrypoint.name - ); - continue; - } - - const resolveCacheKey = `${entrypoint.name} -> ${importedFile}`; - const resolveCached = cache.get('resolve', resolveCacheKey); - const importsOnlySet = new Set(importsOnly); - if (resolveCached) { - const [, cachedOnly] = resolveCached.split('\0'); - cachedOnly?.split(',').forEach((token) => { - if (token) { - importsOnlySet.add(token); - } - }); - } - - cache.add( - 'resolve', - resolveCacheKey, - `${resolved}\0${[...importsOnlySet].join(',')}` - ); + const { resolved: resolvedImports, entrypoint } = action; + for (const { importsOnly, resolved } of resolvedImports) { const nextEntrypoint = createEntrypoint( - babel, - entrypoint.log, - cache, + services, + entrypoint, resolved, - [...importsOnlySet], + importsOnly, undefined, - entrypoint.pluginOptions, - options, - eventEmitter + entrypoint.pluginOptions ); if (nextEntrypoint === 'ignored') { continue; } - next({ - type: 'processEntrypoint', - entrypoint: nextEntrypoint, - stack: [entrypoint.name, ...stack], - }); + action.next('processEntrypoint', nextEntrypoint, {}); } } diff --git a/packages/babel/src/transform-stages/queue/actions/resolveImports.ts b/packages/babel/src/transform-stages/queue/actions/resolveImports.ts index 367736bfe..9e4e81cee 100644 --- a/packages/babel/src/transform-stages/queue/actions/resolveImports.ts +++ b/packages/babel/src/transform-stages/queue/actions/resolveImports.ts @@ -1,8 +1,12 @@ /* eslint-disable no-restricted-syntax,no-continue,no-await-in-loop */ +import { getFileIdx } from '@linaria/utils'; + +import { getStack } from '../createEntrypoint'; import type { IResolveImportsAction, Services, IResolvedImport, + IBaseEntrypoint, } from '../types'; const includes = (a: string[], b: string[]) => { @@ -17,11 +21,79 @@ const mergeImports = (a: string[], b: string[]) => { return [...result].filter((i) => i).sort(); }; +function emitDependency( + emitter: Services['eventEmitter'], + entrypoint: IResolveImportsAction['entrypoint'], + imports: IResolvedImport[] +) { + emitter.single({ + type: 'dependency', + file: entrypoint.name, + only: entrypoint.only, + imports: imports.map(({ resolved, importsOnly }) => ({ + from: resolved, + what: importsOnly, + })), + fileIdx: getFileIdx(entrypoint.name).toString().padStart(5, '0'), + }); +} + +function addToCache( + cache: Services['cache'], + entrypoint: IBaseEntrypoint, + resolvedImports: { + importedFile: string; + importsOnly: string[]; + resolved: string | null; + }[] +) { + const filteredImports = resolvedImports.filter((i): i is IResolvedImport => { + if (i.resolved === null) { + entrypoint.log( + `[resolve] ✅ %s in %s is ignored`, + i.importedFile, + entrypoint.name + ); + return false; + } + + return true; + }); + + return filteredImports.map(({ importedFile, importsOnly, resolved }) => { + const resolveCacheKey = `${entrypoint.name} -> ${importedFile}`; + const resolveCached = cache.get('resolve', resolveCacheKey); + const importsOnlySet = new Set(importsOnly); + if (resolveCached) { + const [, cachedOnly] = resolveCached.split('\0'); + cachedOnly?.split(',').forEach((token) => { + if (token) { + importsOnlySet.add(token); + } + }); + } + + cache.add( + 'resolve', + resolveCacheKey, + `${resolved}\0${[...importsOnlySet].join(',')}` + ); + + return { + importedFile, + importsOnly: [...importsOnlySet], + resolved, + }; + }); +} + +/** + * Synchronously resolves specified imports with a provided resolver. + */ export function syncResolveImports( resolve: (what: string, importer: string, stack: string[]) => string, - { eventEmitter }: Services, + { cache, eventEmitter }: Services, action: IResolveImportsAction, - next: unknown, callbacks: { resolve: (result: IResolvedImport[]) => void } ) { const { imports, entrypoint } = action; @@ -29,12 +101,7 @@ export function syncResolveImports( const { log } = entrypoint; if (listOfImports.length === 0) { - eventEmitter.single({ - type: 'dependency', - file: entrypoint.name, - only: entrypoint.only, - imports: [], - }); + emitDependency(eventEmitter, entrypoint, []); log('%s has no imports', entrypoint.name); callbacks.resolve([]); @@ -44,7 +111,11 @@ export function syncResolveImports( const resolvedImports = listOfImports.map(([importedFile, importsOnly]) => { let resolved: string | null = null; try { - resolved = resolve(importedFile, entrypoint.name, action.stack); + resolved = resolve( + importedFile, + entrypoint.name, + getStack(action.entrypoint) + ); log( '[sync-resolve] ✅ %s -> %s (only: %o)', importedFile, @@ -62,19 +133,14 @@ export function syncResolveImports( }; }); - eventEmitter.single({ - type: 'dependency', - file: entrypoint.name, - only: entrypoint.only, - imports: resolvedImports.map(({ resolved, importsOnly }) => ({ - from: resolved, - what: importsOnly, - })), - }); - - callbacks.resolve(resolvedImports); + const filteredImports = addToCache(cache, entrypoint, resolvedImports); + emitDependency(eventEmitter, entrypoint, filteredImports); + callbacks.resolve(filteredImports); } +/** + * Asynchronously resolves specified imports with a provided resolver. + */ export async function asyncResolveImports( resolve: ( what: string, @@ -83,7 +149,6 @@ export async function asyncResolveImports( ) => Promise, { cache, eventEmitter }: Services, action: IResolveImportsAction, - next: unknown, callbacks: { resolve: (result: IResolvedImport[]) => void } ) { const { imports, entrypoint } = action; @@ -91,12 +156,7 @@ export async function asyncResolveImports( const { log } = entrypoint; if (listOfImports.length === 0) { - eventEmitter.single({ - type: 'dependency', - file: entrypoint.name, - only: entrypoint.only, - imports: [], - }); + emitDependency(eventEmitter, entrypoint, []); log('%s has no imports', entrypoint.name); callbacks.resolve([]); @@ -111,7 +171,11 @@ export async function asyncResolveImports( ) => { let resolved: string | null = null; try { - resolved = await resolve(importedFile, entrypoint.name, action.stack); + resolved = await resolve( + importedFile, + entrypoint.name, + getStack(action.entrypoint) + ); } catch (err) { log( '[async-resolve] ❌ cannot resolve %s in %s: %O', @@ -138,7 +202,7 @@ export async function asyncResolveImports( }; }; - const resolvedImports: IResolvedImport[] = await Promise.all( + const resolvedImports = await Promise.all( listOfImports.map(([importedFile, importsOnly]) => { const resolveCacheKey = `${entrypoint.name} -> ${importedFile}`; @@ -203,15 +267,7 @@ export async function asyncResolveImports( log('resolved %d imports', resolvedImports.length); - eventEmitter.single({ - type: 'dependency', - file: entrypoint.name, - only: entrypoint.only, - imports: resolvedImports.map(({ resolved, importsOnly }) => ({ - from: resolved, - what: importsOnly, - })), - }); - - callbacks.resolve(resolvedImports); + const filteredImports = addToCache(cache, entrypoint, resolvedImports); + emitDependency(eventEmitter, entrypoint, filteredImports); + callbacks.resolve(filteredImports); } diff --git a/packages/babel/src/transform-stages/queue/actions/transform.ts b/packages/babel/src/transform-stages/queue/actions/transform.ts index b8f339e47..97a66e966 100644 --- a/packages/babel/src/transform-stages/queue/actions/transform.ts +++ b/packages/babel/src/transform-stages/queue/actions/transform.ts @@ -11,13 +11,7 @@ import { buildOptions, getPluginKey } from '@linaria/utils'; import type { Core } from '../../../babel'; import type Module from '../../../module'; import withLinariaMetadata from '../../../utils/withLinariaMetadata'; -import type { Next } from '../ActionQueue'; -import type { - IEntrypoint, - ITransformAction, - Services, - ActionQueueItem, -} from '../types'; +import type { IEntrypoint, ITransformAction, Services } from '../types'; const EMPTY_FILE = '=== empty file ==='; @@ -124,13 +118,12 @@ export function prepareCode( /** * Prepares the code for evaluation. This includes removing dead and potentially unsafe code. - * If prepared code has imports, they will be resolved in the next step. - * In the end, the prepared code is added to the cache. + * Emits resolveImports, processImports and addToCodeCache events. */ export function transform( services: Services, action: ITransformAction, - next: Next + callbacks: { done: () => void } ): void { if (!action) { return; @@ -155,34 +148,30 @@ export function transform( if (preparedCode === '') { log('%s is skipped', name); + callbacks.done(); return; } - next({ - type: 'resolveImports', - entrypoint: action.entrypoint, - imports, - stack: action.stack, - }).on('resolve', (resolvedImports) => { - next({ - type: 'processImports', - entrypoint: action.entrypoint, - resolved: resolvedImports, - stack: action.stack, + action + .next('resolveImports', action.entrypoint, { + imports, + }) + .on('resolve', (resolvedImports) => { + action.next('processImports', action.entrypoint, { + resolved: resolvedImports, + }); }); - }); - next({ - type: 'addToCodeCache', - entrypoint: action.entrypoint, - data: { - imports, - result: { - code: preparedCode, - metadata, + action + .next('addToCodeCache', action.entrypoint, { + data: { + imports, + result: { + code: preparedCode, + metadata, + }, + only, }, - only, - }, - stack: action.stack, - }); + }) + .on('done', callbacks.done); } diff --git a/packages/babel/src/transform-stages/queue/createEntrypoint.ts b/packages/babel/src/transform-stages/queue/createEntrypoint.ts index 8d566cf76..822b63e66 100644 --- a/packages/babel/src/transform-stages/queue/createEntrypoint.ts +++ b/packages/babel/src/transform-stages/queue/createEntrypoint.ts @@ -1,11 +1,11 @@ import { readFileSync } from 'fs'; import { dirname, extname } from 'path'; -import type { PluginItem } from '@babel/core'; +import type { PluginItem, TransformOptions } from '@babel/core'; import type { File } from '@babel/types'; import type { Debugger } from '@linaria/logger'; -import type { Evaluator, EventEmitter, StrictOptions } from '@linaria/utils'; +import type { Evaluator, StrictOptions } from '@linaria/utils'; import { buildOptions, getFileIdx, @@ -13,189 +13,319 @@ import { loadBabelOptions, } from '@linaria/utils'; -import type { Core } from '../../babel'; -import type { TransformCacheCollection } from '../../cache'; -import type { Options } from '../../types'; import { getMatchedRule, parseFile } from '../helpers/parseFile'; -import { rootLog } from './rootLog'; -import type { IEntrypoint } from './types'; +import type { + IEntrypoint, + Services, + IBaseEntrypoint, + IEntrypointCode, +} from './types'; const EMPTY_FILE = '=== empty file ==='; -const getLogIdx = (filename: string) => - getFileIdx(filename).toString().padStart(5, '0'); +const getIdx = (fn: string) => getFileIdx(fn).toString().padStart(5, '0'); -const onAbortHandlers = new WeakMap void)[]>(); -const knownSignals = new WeakSet(); +const includes = (a: string[], b: string[]) => { + if (a.includes('*')) return true; + if (a.length !== b.length) return false; + return a.every((item, index) => item === b[index]); +}; -export function addOnAbort( - entrypoint: Omit & { - onAbort?: IEntrypoint['onAbort']; +const isParent = ( + parent: IEntrypoint | { log: Debugger } +): parent is IEntrypoint => 'name' in parent; + +export function getStack(entrypoint: IBaseEntrypoint) { + const stack = [entrypoint.name]; + + let { parent } = entrypoint; + while (parent) { + stack.push(parent.name); + parent = parent.parent; } -): IEntrypoint { - const { abortSignal } = entrypoint; - const onAbort = (fn: () => void) => { - if (!abortSignal) { - return () => {}; - } + return stack; +} - let handlers = onAbortHandlers.get(abortSignal); +const isModuleResolver = (plugin: PluginItem) => + getPluginKey(plugin) === 'module-resolver'; - if (!handlers) { - handlers = []; - onAbortHandlers.set(abortSignal, handlers); - } +function buildConfigs( + services: Services, + name: string, + pluginOptions: StrictOptions, + babelOptions: TransformOptions | undefined +): { + evalConfig: TransformOptions; + parseConfig: TransformOptions; +} { + const { babel, options } = services; + + const commonOptions = { + ast: true, + filename: name, + inputSourceMap: options.inputSourceMap, + root: options.root, + sourceFileName: name, + sourceMaps: true, + }; - handlers.push(fn); + const rawConfig = buildOptions( + pluginOptions?.babelOptions, + babelOptions, + commonOptions + ); - return () => { - const idx = handlers!.indexOf(fn); - if (idx !== -1) { - handlers!.splice(idx, 1); - } - }; + const parseConfig = loadBabelOptions(babel, name, { + babelrc: true, + ...rawConfig, + }); + + const parseHasModuleResolver = parseConfig.plugins?.some(isModuleResolver); + const rawHasModuleResolver = rawConfig.plugins?.some(isModuleResolver); + + if (parseHasModuleResolver && !rawHasModuleResolver) { + // eslint-disable-next-line no-console + console.warn( + `[linaria] ${name} has a module-resolver plugin in its babelrc, but it is not present` + + `in the babelOptions for the linaria plugin. This works for now but will be an error in the future.` + + `Please add the module-resolver plugin to the babelOptions for the linaria plugin.` + ); + + rawConfig.plugins = [ + ...(parseConfig.plugins?.filter((plugin) => isModuleResolver(plugin)) ?? + []), + ...(rawConfig.plugins ?? []), + ]; + } + + const evalConfig = loadBabelOptions(babel, name, { + babelrc: false, + ...rawConfig, + }); + + return { + evalConfig, + parseConfig, }; +} - // AbortSignal has a limit of listeners, so we need to reuse the same one - if (abortSignal && !knownSignals.has(abortSignal)) { - abortSignal.addEventListener('abort', () => { - const handlers = onAbortHandlers.get(abortSignal); - if (handlers) { - handlers.forEach((fn) => fn()); - } - }); +function loadAndParse( + services: Services, + name: string, + loadedCode: string | undefined, + log: Debugger, + pluginOptions: StrictOptions +) { + const { babel, cache, eventEmitter } = services; + + const extension = extname(name); + + if (!pluginOptions.extensions.includes(extension)) { + log( + '[createEntrypoint] %s is ignored. If you want it to be processed, you should add \'%s\' to the "extensions" option.', + name, + extension + ); + + return 'ignored'; + } + + const code = loadedCode ?? readFileSync(name, 'utf-8'); + + const { action, babelOptions } = getMatchedRule( + pluginOptions.rules, + name, + code + ); - knownSignals.add(abortSignal); + if (action === 'ignore') { + log('[createEntrypoint] %s is ignored by rule', name); + cache.add('ignored', name, true); + return 'ignored'; } + const evaluator: Evaluator = + typeof action === 'function' + ? action + : require(require.resolve(action, { + paths: [dirname(name)], + })).default; + + const { evalConfig, parseConfig } = buildConfigs( + services, + name, + pluginOptions, + babelOptions + ); + + const ast: File = eventEmitter.pair( + { method: 'parseFile' }, + () => + cache.get('originalAST', name) ?? + parseFile(babel, name, code, parseConfig) + ); + return { - ...entrypoint, - onAbort, + ast, + code, + evaluator, + evalConfig, }; } -export function createEntrypoint( - babel: Core, - parentLog: Debugger, - cache: TransformCacheCollection, +const supersedeHandlers = new WeakMap< + IBaseEntrypoint, + Array<(newEntrypoint: IEntrypoint) => void> +>(); + +export function onSupersede( + entrypoint: T, + callback: (newEntrypoint: T) => void +) { + if (!supersedeHandlers.has(entrypoint)) { + supersedeHandlers.set(entrypoint, []); + } + + const handlers = supersedeHandlers.get(entrypoint)!; + handlers.push(callback as (newEntrypoint: IBaseEntrypoint) => void); + supersedeHandlers.set(entrypoint, handlers); + + return () => { + const index = handlers.indexOf( + callback as (newEntrypoint: IBaseEntrypoint) => void + ); + if (index >= 0) { + handlers.splice(index, 1); + } + }; +} + +export function supersedeEntrypoint( + services: Pick, + oldEntrypoint: IEntrypoint, + newEntrypoint: IEntrypoint +) { + // If we already have a result for this file, we should invalidate it + services.cache.invalidate('eval', oldEntrypoint.name); + + supersedeHandlers + .get(oldEntrypoint) + ?.forEach((handler) => handler(newEntrypoint)); +} + +export type LoadAndParseFn = ( + services: TServices, + name: string, + loadedCode: string | undefined, + log: Debugger, + pluginOptions: TPluginOptions +) => IEntrypointCode | 'ignored'; + +export function genericCreateEntrypoint< + TServices extends Pick, + TPluginOptions +>( + loadAndParseFn: LoadAndParseFn, + services: TServices, + parent: IEntrypoint | { log: Debugger }, name: string, only: string[], - maybeCode: string | undefined, - pluginOptions: StrictOptions, - options: Pick, - eventEmitter: EventEmitter, - abortSignal?: AbortSignal -): IEntrypoint | 'ignored' { + loadedCode: string | undefined, + pluginOptions: TPluginOptions +): IEntrypoint | 'ignored' { + const { cache, eventEmitter } = services; + return eventEmitter.pair({ method: 'createEntrypoint' }, () => { - const log = parentLog.extend( - getLogIdx(name), - parentLog === rootLog ? ':' : '->' - ); - const extension = extname(name); + const idx = getIdx(name); + const log = parent.log.extend(idx, isParent(parent) ? '->' : ':'); + + let onCreate: ( + newEntrypoint: IEntrypoint + ) => void = () => {}; + + const cached = cache.get('entrypoints', name) as + | IEntrypoint + | undefined; + const mergedOnly = cached?.only + ? Array.from(new Set([...cached.only, ...only])) + .filter((i) => i) + .sort() + : only; + + if (cached) { + if (includes(cached.only, mergedOnly)) { + log('%s is cached', name); + return cached; + } - if (!pluginOptions.extensions.includes(extension)) { log( - '[createEntrypoint] %s is ignored. If you want it to be processed, you should add \'%s\' to the "extensions" option.', + '%s is cached, but with different `only` %o (the cached one %o)', name, - extension + only, + cached?.only ); - return 'ignored'; + onCreate = (newEntrypoint) => { + supersedeEntrypoint(services, cached, newEntrypoint); + }; } - const code = maybeCode ?? readFileSync(name, 'utf-8'); - - const { action, babelOptions } = getMatchedRule( - pluginOptions.rules, + const loadedAndParsed = loadAndParseFn( + services, name, - code + loadedCode, + log, + pluginOptions ); - if (action === 'ignore') { - log('[createEntrypoint] %s is ignored by rule', name); - cache.add('ignored', name, true); + if (loadedAndParsed === 'ignored') { return 'ignored'; } - const evaluator: Evaluator = - typeof action === 'function' - ? action - : require(require.resolve(action, { - paths: [dirname(name)], - })).default; - - // FIXME: All those configs should be memoized - - const commonOptions = { - ast: true, - filename: name, - inputSourceMap: options.inputSourceMap, - root: options.root, - sourceFileName: name, - sourceMaps: true, - }; - - const rawConfig = buildOptions( - pluginOptions?.babelOptions, - babelOptions, - commonOptions - ); - - const parseConfig = loadBabelOptions(babel, name, { - babelrc: true, - ...rawConfig, - }); - - const isModuleResolver = (plugin: PluginItem) => - getPluginKey(plugin) === 'module-resolver'; - const parseHasModuleResolver = parseConfig.plugins?.some(isModuleResolver); - const rawHasModuleResolver = rawConfig.plugins?.some(isModuleResolver); - - if (parseHasModuleResolver && !rawHasModuleResolver) { - // eslint-disable-next-line no-console - console.warn( - `[linaria] ${name} has a module-resolver plugin in its babelrc, but it is not present` + - `in the babelOptions for the linaria plugin. This works for now but will be an error in the future.` + - `Please add the module-resolver plugin to the babelOptions for the linaria plugin.` - ); - - rawConfig.plugins = [ - ...(parseConfig.plugins?.filter((plugin) => isModuleResolver(plugin)) ?? - []), - ...(rawConfig.plugins ?? []), - ]; - } - - cache.invalidateIfChanged(name, code); + cache.invalidateIfChanged(name, loadedAndParsed.code); + cache.add('originalAST', name, loadedAndParsed.ast); - const evalConfig = loadBabelOptions(babel, name, { - babelrc: false, - ...rawConfig, - }); - - const ast: File = eventEmitter.pair( - { method: 'parseFile' }, - () => - cache.get('originalAST', name) ?? - parseFile(babel, name, code, parseConfig) + log( + '[createEntrypoint] %s (%o)\n%s', + name, + mergedOnly, + loadedAndParsed.code || EMPTY_FILE ); - cache.add('originalAST', name, ast); - - log('[createEntrypoint] %s (%o)\n%s', name, only, code || EMPTY_FILE); - - return addOnAbort({ - abortSignal, - ast, - code, - evalConfig, - evaluator, + const newEntrypoint: IEntrypoint = { + ...loadedAndParsed, + idx, log, name, - only: [...only].filter((i) => i).sort(), + only: mergedOnly, + parent: isParent(parent) ? parent : null, pluginOptions, - }); + }; + + cache.add('entrypoints', name, newEntrypoint); + onCreate(newEntrypoint); + + return newEntrypoint; }); } + +export function createEntrypoint( + services: Services, + parent: IEntrypoint | { log: Debugger }, + name: string, + only: string[], + loadedCode: string | undefined, + pluginOptions: StrictOptions +): IEntrypoint | 'ignored' { + return genericCreateEntrypoint( + loadAndParse, + services, + parent, + name, + only, + loadedCode, + pluginOptions + ); +} diff --git a/packages/babel/src/transform-stages/queue/types.ts b/packages/babel/src/transform-stages/queue/types.ts index d3ceafa68..8be4927cd 100644 --- a/packages/babel/src/transform-stages/queue/types.ts +++ b/packages/babel/src/transform-stages/queue/types.ts @@ -8,8 +8,6 @@ import type { Core } from '../../babel'; import type { TransformCacheCollection } from '../../cache'; import type { ITransformFileResult, Options } from '../../types'; -import type { IBaseNode, IBaseEntrypoint } from './PriorityQueue'; - export type Services = { babel: Core; cache: TransformCacheCollection; @@ -17,34 +15,107 @@ export type Services = { eventEmitter: EventEmitter; }; -export interface IEntrypoint extends IBaseEntrypoint { +export interface IBaseNode { + type: ActionQueueItem['type']; +} + +export interface IBaseEntrypoint { + idx: string; + log: Debugger; + name: string; + only: string[]; + parent: IEntrypoint | null; +} + +export interface IEntrypointCode { ast: File; code: string; evalConfig: TransformOptions; evaluator: Evaluator; - log: Debugger; - name: string; - only: string[]; - pluginOptions: StrictOptions; } -export type BaseAction< +export interface IEntrypoint + extends IBaseEntrypoint, + IEntrypointCode { + pluginOptions: TPluginOptions; +} + +export type EventEmitters> = { + [K in keyof TEvents]: (...args: TEvents[K]) => void; +}; + +export type EventsHandlers> = { + [K in keyof TEvents]?: Array<(...args: TEvents[K]) => void>; +}; + +export type ActionOn> = < + K extends keyof TEvents +>( + type: K, + callback: (...args: TEvents[K]) => void +) => void; + +export interface IActionControls { + // abort: (reason?: string) => void; + next: Next; + // onAbort: (callback: (reason?: string) => void) => void; +} + +export type ActionByType = Extract< + ActionQueueItem, + { + type: TType; + } +>; + +export interface IBaseAction< + TEntrypoint extends IBaseEntrypoint = IBaseEntrypoint, TEvents extends Record = Record -> = IBaseNode; +> extends IBaseNode, + IActionControls { + callbacks?: EventsHandlers; + entrypoint: TEntrypoint; + on: ActionOn; +} + +export type DataOf = Omit< + TNode, + keyof IActionControls | keyof IBaseAction | 'entrypoint' +>; -export interface IExplodeReexportsAction extends BaseAction { +export type Next = ( + type: TType, + entrypoint: IBaseEntrypoint, + data: DataOf> +) => Extract; + +export interface IExplodeReexportsAction extends IBaseAction { type: 'explodeReexports'; } -export interface IProcessEntrypointAction extends BaseAction { +export interface IProcessEntrypointAction< + TEntrypoint extends IBaseEntrypoint = IBaseEntrypoint +> extends IBaseAction { type: 'processEntrypoint'; } -export interface ITransformAction extends BaseAction { +export interface ITransformAction + extends IBaseAction< + IEntrypoint, + { + done: []; + } + > { type: 'transform'; } -export interface IAddToCodeCacheAction extends BaseAction { +export interface IAddToCodeCacheAction + extends IBaseAction< + IBaseEntrypoint, + { + done: []; + } + > { type: 'addToCodeCache'; data: { imports: Map | null; @@ -56,26 +127,32 @@ export interface IAddToCodeCacheAction extends BaseAction { export interface IResolvedImport { importedFile: string; importsOnly: string[]; - resolved: string | null; + resolved: string; } export interface IResolveImportsAction - extends BaseAction<{ - resolve: [exports: IResolvedImport[]]; - }> { - imports: Map | null; + extends IBaseAction< + IBaseEntrypoint, + { + resolve: [result: IResolvedImport[]]; + } + > { type: 'resolveImports'; + imports: Map | null; } -export interface IProcessImportsAction extends BaseAction { - resolved: IResolvedImport[]; +export interface IProcessImportsAction extends IBaseAction { type: 'processImports'; + resolved: IResolvedImport[]; } export interface IGetExportsAction - extends BaseAction<{ - resolve: [exports: string[]]; - }> { + extends IBaseAction< + IEntrypoint, + { + resolve: [exports: string[]]; + } + > { type: 'getExports'; } diff --git a/packages/babel/tsconfig.spec.json b/packages/babel/tsconfig.spec.json new file mode 100644 index 000000000..268d7808c --- /dev/null +++ b/packages/babel/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "types": ["jest", "node"] + }, + "include": ["src/**/*.ts", "__tests__/**/*.ts"], + "exclude": [] +} diff --git a/packages/logger/package.json b/packages/logger/package.json index 7a9ebb718..2802bd623 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -30,11 +30,11 @@ "watch": "pnpm build:lib --watch & pnpm build:esm --watch & pnpm build:declarations --watch" }, "dependencies": { - "debug": "^4.1.1", + "debug": "^4.3.4", "picocolors": "^1.0.0" }, "devDependencies": { - "@types/debug": "^4.1.5", + "@types/debug": "^4.1.8", "@types/node": "^17.0.39" }, "engines": { diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index 4b97bd90d..8278b5e25 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -2,11 +2,6 @@ import genericDebug from 'debug'; import type { Debugger } from 'debug'; import pc from 'picocolors'; -type LogLevel = 'error' | 'warn' | 'info' | 'debug'; - -const levels = ['error', 'warn', 'info', 'debug']; -const currentLevel = levels.indexOf(process.env.LINARIA_LOG || 'warn'); - export type { Debugger }; export const linariaLogger = genericDebug('linaria'); @@ -28,10 +23,13 @@ function gerOrCreate(namespace: string | null | undefined): Debugger { return loggers.get(namespace)!; } -genericDebug.formatters.r = (ref: { namespace: string; text?: string }) => { - const color = parseInt(gerOrCreate(ref.namespace).color, 10); +genericDebug.formatters.r = ( + ref: string | { namespace: string; text?: string } +) => { + const namespace = typeof ref === 'string' ? ref : ref.namespace; + const text = typeof ref === 'string' ? namespace : ref.text ?? namespace; + const color = parseInt(gerOrCreate(namespace).color, 10); const colorCode = `\u001B[3${color < 8 ? color : `8;5;${color}`}`; - const text = ref.text ?? ref.namespace; return `${colorCode};1m${text}\u001B[0m`; }; @@ -47,16 +45,11 @@ const format = (text: T) => { return text; }; -function log( - level: LogLevel, +export function debug( namespaces: string, template: unknown | (() => void), ...restArgs: unknown[] ) { - if (currentLevel < levels.indexOf(level)) { - return; - } - const logger = gerOrCreate(namespaces); if (!logger.enabled) return; @@ -71,11 +64,6 @@ function log( logger(format(template), ...restArgs); } -export const debug = log.bind(null, 'debug'); -export const info = log.bind(null, 'info'); -export const warn = log.bind(null, 'warn'); -export const error = log.bind(null, 'error'); - export const notify = (message: string) => { // eslint-disable-next-line no-console console.log( diff --git a/packages/rollup/src/index.ts b/packages/rollup/src/index.ts index 399228383..24f2ee688 100644 --- a/packages/rollup/src/index.ts +++ b/packages/rollup/src/index.ts @@ -154,7 +154,8 @@ export default function linaria({ vite = { ...vite, buildStart() { - this.warn( + // eslint-disable-next-line no-console + console.warn( 'You are trying to use @linaria/rollup with Vite. The support for Vite in @linaria/rollup is deprecated and will be removed in the next major release. Please use @linaria/vite instead.' ); }, diff --git a/packages/testkit/package.json b/packages/testkit/package.json index 43750f2ea..3ec73878a 100644 --- a/packages/testkit/package.json +++ b/packages/testkit/package.json @@ -30,6 +30,7 @@ "@linaria/shaker": "workspace:^", "@linaria/tags": "workspace:^", "@swc/core": "^1.3.20", + "debug": "^4.3.4", "dedent": "^0.7.0", "esbuild": "^0.15.16", "strip-ansi": "^5.2.0", @@ -50,6 +51,7 @@ "@types/babel__core": "^7.1.19", "@types/babel__generator": "^7.6.4", "@types/babel__traverse": "^7.17.1", + "@types/debug": "^4.1.8", "@types/dedent": "^0.7.0", "@types/jest": "^28.1.0", "@types/node": "^17.0.39", diff --git a/packages/testkit/src/__snapshots__/babel.test.ts.snap b/packages/testkit/src/__snapshots__/babel.test.ts.snap index 654462dba..d3ed87391 100644 --- a/packages/testkit/src/__snapshots__/babel.test.ts.snap +++ b/packages/testkit/src/__snapshots__/babel.test.ts.snap @@ -411,6 +411,1687 @@ Dependencies: NA `; +exports[`strategy shaker concurrent two parallel chains of reexports 1`] = ` +"import { styled } from '@linaria/react'; +export const H1 = /*#__PURE__*/styled('h1')({ + name: \\"H1\\", + class: \\"H1_hpsacp3\\", + propsAsIs: false +});" +`; + +exports[`strategy shaker concurrent two parallel chains of reexports 2`] = ` + +CSS: + +.H1_hpsacp3 { + color: foo; +} + +Dependencies: ./__fixtures__/re-exports + +`; + +exports[`strategy shaker concurrent two parallel chains of reexports 3`] = ` +"import { styled } from '@linaria/react'; +export const H1 = /*#__PURE__*/styled('h1')({ + name: \\"H1\\", + class: \\"H1_htinbxh\\", + propsAsIs: false +});" +`; + +exports[`strategy shaker concurrent two parallel chains of reexports 4`] = ` + +CSS: + +.H1_htinbxh { + color: bar2; +} + +Dependencies: ./__fixtures__/re-exports + +`; + +exports[`strategy shaker concurrent two parallel chains of reexports 5`] = ` +Array [ + Array [ + Object { + "action": "processEntrypoint", + "args": Array [ + "__linariaPreval", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/source-1.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "processEntrypoint", + "args": Array [ + "__linariaPreval", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/source-2.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "explodeReexports", + "args": Array [ + "__linariaPreval", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/source-1.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "explodeReexports", + "args": Array [ + "__linariaPreval", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/source-2.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "transform", + "args": Array [ + "__linariaPreval", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/source-1.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "transform", + "args": Array [ + "__linariaPreval", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/source-2.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports", + "args": Array [ + "__linariaPreval", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/source-1.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports", + "args": Array [ + "__linariaPreval", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/source-2.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "file": "/Users/anber/Sources/linaria/packages/testkit/src/source-1.js", + "imports": Array [ + Object { + "from": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "what": Array [ + "fooStyles", + ], + }, + ], + "only": Array [ + "__linariaPreval", + ], + "type": "dependency", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports:resolve", + "args": Array [ + Array [ + Object { + "importedFile": "./__fixtures__/re-exports", + "importsOnly": Array [ + "fooStyles", + ], + "resolved": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + }, + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/source-1.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "file": "/Users/anber/Sources/linaria/packages/testkit/src/source-2.js", + "imports": Array [ + Object { + "from": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "what": Array [ + "bar2", + ], + }, + ], + "only": Array [ + "__linariaPreval", + ], + "type": "dependency", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports:resolve", + "args": Array [ + Array [ + Object { + "importedFile": "./__fixtures__/re-exports", + "importsOnly": Array [ + "bar2", + ], + "resolved": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + }, + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/source-2.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "processImports", + "args": Array [ + "__linariaPreval", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/source-1.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "processImports", + "args": Array [ + "__linariaPreval", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/source-2.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "processEntrypoint", + "args": Array [ + "fooStyles", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "processEntrypoint", + "args": Array [ + "bar2", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "explodeReexports", + "args": Array [ + "fooStyles", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "explodeReexports", + "args": Array [ + "bar2", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports", + "args": Array [ + "fooStyles", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports", + "args": Array [ + "bar2", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "imports": Array [ + Object { + "from": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/empty.js", + "what": Array [], + }, + Object { + "from": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "what": Array [], + }, + ], + "only": Array [ + "fooStyles", + ], + "type": "dependency", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports:resolve", + "args": Array [ + Array [ + Object { + "importedFile": "./empty", + "importsOnly": Array [], + "resolved": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/empty.js", + }, + Object { + "importedFile": "./foo", + "importsOnly": Array [], + "resolved": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + }, + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "imports": Array [ + Object { + "from": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/empty.js", + "what": Array [], + }, + Object { + "from": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "what": Array [], + }, + ], + "only": Array [ + "bar2", + ], + "type": "dependency", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports:resolve", + "args": Array [ + Array [ + Object { + "importedFile": "./empty", + "importsOnly": Array [], + "resolved": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/empty.js", + }, + Object { + "importedFile": "./foo", + "importsOnly": Array [], + "resolved": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + }, + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports", + "args": Array [], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/empty.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports:resolve", + "args": Array [ + Array [], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/empty.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports", + "args": Array [], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/empty.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports:resolve", + "args": Array [ + Array [], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/empty.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports", + "args": Array [], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports", + "args": Array [], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports", + "args": Array [], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports", + "args": Array [], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "imports": Array [ + Object { + "from": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "what": Array [], + }, + ], + "only": Array [], + "type": "dependency", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports:resolve", + "args": Array [ + Array [ + Object { + "importedFile": "../bar", + "importsOnly": Array [], + "resolved": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + }, + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "imports": Array [ + Object { + "from": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "what": Array [], + }, + ], + "only": Array [], + "type": "dependency", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports:resolve", + "args": Array [ + Array [ + Object { + "importedFile": "../bar", + "importsOnly": Array [], + "resolved": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + }, + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports", + "args": Array [], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports", + "args": Array [], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports", + "args": Array [], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports", + "args": Array [], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "imports": Array [ + Object { + "from": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + "what": Array [], + }, + ], + "only": Array [], + "type": "dependency", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports:resolve", + "args": Array [ + Array [ + Object { + "importedFile": "./re-exports/constants", + "importsOnly": Array [], + "resolved": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + }, + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "imports": Array [ + Object { + "from": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + "what": Array [], + }, + ], + "only": Array [], + "type": "dependency", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports:resolve", + "args": Array [ + Array [ + Object { + "importedFile": "./re-exports/constants", + "importsOnly": Array [], + "resolved": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + }, + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports", + "args": Array [], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports:resolve", + "args": Array [ + Array [ + "foo", + "bar", + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports:resolve", + "args": Array [ + Array [ + "bar1", + "bar2", + "foo", + "bar", + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports:resolve", + "args": Array [ + Array [ + "fooStyles", + "bar1", + "bar2", + "foo", + "bar", + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports", + "args": Array [], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports:resolve", + "args": Array [ + Array [ + "foo", + "bar", + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports:resolve", + "args": Array [ + Array [ + "bar1", + "bar2", + "foo", + "bar", + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports:resolve", + "args": Array [ + Array [ + "fooStyles", + "bar1", + "bar2", + "foo", + "bar", + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "explodeReexports", + "args": Array [ + "fooStyles", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "explodeReexports", + "args": Array [ + "bar2", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "transform", + "args": Array [ + "fooStyles", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "transform", + "args": Array [ + "bar2", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports", + "args": Array [ + "fooStyles", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports", + "args": Array [ + "bar2", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "imports": Array [ + Object { + "from": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "what": Array [ + "fooStyles", + ], + }, + ], + "only": Array [ + "fooStyles", + ], + "type": "dependency", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports:resolve", + "args": Array [ + Array [ + Object { + "importedFile": "./foo", + "importsOnly": Array [ + "fooStyles", + ], + "resolved": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + }, + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "imports": Array [ + Object { + "from": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "what": Array [ + "bar2", + ], + }, + ], + "only": Array [ + "bar2", + ], + "type": "dependency", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports:resolve", + "args": Array [ + Array [ + Object { + "importedFile": "./foo", + "importsOnly": Array [ + "bar2", + ], + "resolved": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + }, + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "processImports", + "args": Array [ + "fooStyles", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "processImports", + "args": Array [ + "bar2", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "processEntrypoint", + "args": Array [ + "fooStyles", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "processEntrypoint", + "args": Array [ + "bar2", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "explodeReexports", + "args": Array [ + "fooStyles", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "explodeReexports", + "args": Array [ + "bar2", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports", + "args": Array [ + "fooStyles", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports", + "args": Array [ + "bar2", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "imports": Array [ + Object { + "from": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "what": Array [], + }, + ], + "only": Array [ + "fooStyles", + ], + "type": "dependency", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports:resolve", + "args": Array [ + Array [ + Object { + "importedFile": "../bar", + "importsOnly": Array [], + "resolved": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + }, + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "imports": Array [ + Object { + "from": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "what": Array [], + }, + ], + "only": Array [ + "bar2", + ], + "type": "dependency", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports:resolve", + "args": Array [ + Array [ + Object { + "importedFile": "../bar", + "importsOnly": Array [], + "resolved": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + }, + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports", + "args": Array [], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports", + "args": Array [], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports", + "args": Array [], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports", + "args": Array [], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "imports": Array [ + Object { + "from": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + "what": Array [], + }, + ], + "only": Array [], + "type": "dependency", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports:resolve", + "args": Array [ + Array [ + Object { + "importedFile": "./re-exports/constants", + "importsOnly": Array [], + "resolved": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + }, + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "imports": Array [ + Object { + "from": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + "what": Array [], + }, + ], + "only": Array [], + "type": "dependency", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports:resolve", + "args": Array [ + Array [ + Object { + "importedFile": "./re-exports/constants", + "importsOnly": Array [], + "resolved": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + }, + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports", + "args": Array [], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports:resolve", + "args": Array [ + Array [ + "foo", + "bar", + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports:resolve", + "args": Array [ + Array [ + "bar1", + "bar2", + "foo", + "bar", + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports", + "args": Array [], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports:resolve", + "args": Array [ + Array [ + "foo", + "bar", + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports:resolve", + "args": Array [ + Array [ + "bar1", + "bar2", + "foo", + "bar", + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "explodeReexports", + "args": Array [ + "fooStyles", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "explodeReexports", + "args": Array [ + "bar2", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "transform", + "args": Array [ + "fooStyles", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "transform", + "args": Array [ + "bar2", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports", + "args": Array [ + "fooStyles", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports", + "args": Array [ + "bar2", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "imports": Array [ + Object { + "from": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "what": Array [ + "bar2", + ], + }, + ], + "only": Array [ + "bar2", + ], + "type": "dependency", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports:resolve", + "args": Array [ + Array [ + Object { + "importedFile": "../bar", + "importsOnly": Array [ + "bar2", + ], + "resolved": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + }, + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "imports": Array [ + Object { + "from": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + "what": Array [ + "*", + ], + }, + ], + "only": Array [ + "fooStyles", + ], + "type": "dependency", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports:resolve", + "args": Array [ + Array [ + Object { + "importedFile": "./constants", + "importsOnly": Array [ + "*", + ], + "resolved": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + }, + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "processImports", + "args": Array [ + "bar2", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "processImports", + "args": Array [ + "fooStyles", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "processEntrypoint", + "args": Array [ + "bar2", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "processEntrypoint", + "args": Array [ + "*", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "explodeReexports", + "args": Array [ + "bar2", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "explodeReexports", + "args": Array [ + "*", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports", + "args": Array [ + "bar2", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "transform", + "args": Array [ + "*", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "imports": Array [ + Object { + "from": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + "what": Array [], + }, + ], + "only": Array [ + "bar2", + ], + "type": "dependency", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports:resolve", + "args": Array [ + Array [ + Object { + "importedFile": "./re-exports/constants", + "importsOnly": Array [], + "resolved": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + }, + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports", + "args": Array [ + "*", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + "imports": Array [], + "only": Array [ + "*", + ], + "type": "dependency", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports:resolve", + "args": Array [ + Array [], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports", + "args": Array [], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "getExports:resolve", + "args": Array [ + Array [ + "foo", + "bar", + ], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "processImports", + "args": Array [ + "*", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "explodeReexports", + "args": Array [ + "bar2", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "addToCodeCache", + "args": Array [ + "__linariaPreval", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/source-1.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "transform", + "args": Array [ + "bar2", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "addToCodeCache", + "args": Array [ + "fooStyles", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports", + "args": Array [ + "bar2", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "imports": Array [], + "only": Array [ + "bar2", + ], + "type": "dependency", + }, + "single", + ], + Array [ + Object { + "action": "resolveImports:resolve", + "args": Array [ + Array [], + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "addToCodeCache", + "args": Array [ + "fooStyles", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "processImports", + "args": Array [ + "bar2", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "addToCodeCache", + "args": Array [ + "*", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/constants.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "addToCodeCache", + "args": Array [ + "__linariaPreval", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/source-2.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "addToCodeCache", + "args": Array [ + "bar2", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/bar.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "addToCodeCache", + "args": Array [ + "bar2", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/index.js", + "type": "queue-action", + }, + "single", + ], + Array [ + Object { + "action": "addToCodeCache", + "args": Array [ + "bar2", + ], + "file": "/Users/anber/Sources/linaria/packages/testkit/src/__fixtures__/re-exports/foo.js", + "type": "queue-action", + }, + "single", + ], +] +`; + exports[`strategy shaker derives display name from filename 1`] = ` "import { styled } from '@linaria/react'; export default /*#__PURE__*/styled('h1')({ diff --git a/packages/testkit/src/babel.test.ts b/packages/testkit/src/babel.test.ts index 93950a5a5..410e619c2 100644 --- a/packages/testkit/src/babel.test.ts +++ b/packages/testkit/src/babel.test.ts @@ -1,8 +1,10 @@ +/* eslint-disable no-restricted-syntax */ import { readFileSync } from 'fs'; import { dirname, join, resolve } from 'path'; import * as babel from '@babel/core'; import type { PluginItem } from '@babel/core'; +import debug from 'debug'; import dedent from 'dedent'; import stripAnsi from 'strip-ansi'; @@ -84,7 +86,8 @@ async function transform( originalCode: string, opts: Options, cache?: TransformCacheCollection, - eventEmitter?: EventEmitter + eventEmitter?: EventEmitter, + filename = 'source' ) { const [ evaluator, @@ -93,7 +96,7 @@ async function transform( babelPartialConfig = {}, ] = opts; - const filename = join(dirName, `source.${extension}`); + const fullFilename = join(dirName, `${filename}.${extension}`); const presets = getPresets(extension); const linariaConfig = getLinariaConfig( @@ -106,7 +109,7 @@ async function transform( const result = await linariaTransform( originalCode, { - filename: babelPartialConfig.filename ?? filename, + filename: babelPartialConfig.filename ?? fullFilename, pluginOptions: linariaConfig, }, asyncResolve, @@ -3087,4 +3090,55 @@ describe('strategy shaker', () => { expect(metadata).toMatchSnapshot(); }); }); + + describe('concurrent', () => { + debug.enable('linaria:*'); + + it('two parallel chains of reexports', async () => { + const cache = new TransformCacheCollection(); + + const onEvent = jest.fn>(); + const emitter = new EventEmitter(onEvent); + + const files = { + 'source-1': dedent` + import { styled } from '@linaria/react'; + import { fooStyles } from "./__fixtures__/re-exports"; + + const value = fooStyles.foo; + + export const H1 = styled.h1\` + color: ${'${value}'}; + \` + `, + 'source-2': dedent` + import { styled } from '@linaria/react'; + import { bar2 } from "./__fixtures__/re-exports"; + + export const H1 = styled.h1\` + color: ${'${bar2}'}; + \` + `, + }; + + const results = await Promise.all( + Object.entries(files).map(([filename, content]) => + transform(content, [evaluator], cache, emitter, filename) + ) + ); + + for (const { code, metadata } of results) { + expect(code).toMatchSnapshot(); + expect(metadata).toMatchSnapshot(); + } + + expect(onEvent).not.toHaveBeenCalledWith( + expect.objectContaining({ + file: resolve(__dirname, './__fixtures__/re-exports/empty.js'), + action: 'processImports', + }), + 'single' + ); + }); + }); }); diff --git a/packages/testkit/src/prepareCode.test.ts b/packages/testkit/src/prepareCode.test.ts index 4322e9558..c71845491 100644 --- a/packages/testkit/src/prepareCode.test.ts +++ b/packages/testkit/src/prepareCode.test.ts @@ -59,18 +59,19 @@ describe('prepareCode', () => { .map((s) => s.trim()); const sourceCode = restLines.join('\n'); - const entrypoint = createEntrypoint( + const services = { babel, - linariaLogger, - new TransformCacheCollection(), + cache: new TransformCacheCollection(), + options: { root }, + eventEmitter: EventEmitter.dummy, + }; + const entrypoint = createEntrypoint( + services, + null, inputFilePath, only, sourceCode, - pluginOptions, - { - root, - }, - EventEmitter.dummy + pluginOptions ); if (entrypoint === 'ignored') { diff --git a/packages/utils/src/EventEmitter.ts b/packages/utils/src/EventEmitter.ts index 7da173d09..d168b6cf9 100644 --- a/packages/utils/src/EventEmitter.ts +++ b/packages/utils/src/EventEmitter.ts @@ -34,6 +34,12 @@ export class EventEmitter { } public single(labels: Record) { - this.onEvent(labels, 'single'); + this.onEvent( + { + ...labels, + datetime: new Date(), + }, + 'single' + ); } } diff --git a/packages/utils/src/collectExportsAndImports.ts b/packages/utils/src/collectExportsAndImports.ts index ae7e18275..b7b172513 100644 --- a/packages/utils/src/collectExportsAndImports.ts +++ b/packages/utils/src/collectExportsAndImports.ts @@ -27,7 +27,7 @@ import type { VariableDeclarator, } from '@babel/types'; -import { warn } from '@linaria/logger'; +import { debug } from '@linaria/logger'; import { getScope } from './getScope'; import isExports from './isExports'; @@ -224,7 +224,7 @@ function importFromVariableDeclarator( if (!isSync) { // Something went wrong // Is it something like `const { … } = import(…)`? - warn('evaluator:collectExportsAndImports', '`import` should be awaited'); + debug('evaluator:collectExportsAndImports', '`import` should be awaited'); return []; } @@ -233,7 +233,7 @@ function importFromVariableDeclarator( } // What else it can be? - warn( + debug( 'evaluator:collectExportsAndImports:importFromVariableDeclarator', 'Unknown type of id', id.node.type @@ -270,7 +270,7 @@ function exportFromVariableDeclarator( } // What else it can be? - warn( + debug( 'evaluator:collectExportsAndImports:exportFromVariableDeclarator', 'Unknown type of id', id.node.type @@ -398,7 +398,7 @@ function collectFromRequire(path: NodePath, state: IState): void { if (!imported) { // It's not a transpiled import. // TODO: Can we guess that it's a namespace import? - warn( + debug( 'evaluator:collectExportsAndImports', 'Unknown wrapper of require', container.node.callee @@ -426,7 +426,7 @@ function collectFromRequire(path: NodePath, state: IState): void { if (!variableDeclarator.isVariableDeclarator()) { // TODO: Where else it can be? - warn( + debug( 'evaluator:collectExportsAndImports', 'Unexpected require inside', variableDeclarator.node.type @@ -436,7 +436,7 @@ function collectFromRequire(path: NodePath, state: IState): void { const id = variableDeclarator.get('id'); if (!id.isIdentifier()) { - warn( + debug( 'evaluator:collectExportsAndImports', 'Id should be Identifier', variableDeclarator.node.type @@ -466,7 +466,7 @@ function collectFromRequire(path: NodePath, state: IState): void { // It is `require('@linaria/shaker').dep` const property = container.get('property'); if (!property.isIdentifier() && !property.isStringLiteral()) { - warn( + debug( 'evaluator:collectExportsAndImports', 'Property should be Identifier or StringLiteral', property.node.type @@ -488,7 +488,7 @@ function collectFromRequire(path: NodePath, state: IState): void { type: 'cjs', }); } else { - warn( + debug( 'evaluator:collectExportsAndImports', 'Id should be Identifier', variableDeclarator.node.type @@ -799,7 +799,7 @@ function unfoldNamespaceImport( break; } - warn( + debug( 'evaluator:collectExportsAndImports:unfoldNamespaceImports', 'Unknown import type', importType @@ -820,7 +820,7 @@ function unfoldNamespaceImport( // Otherwise, we can't predict usage and import it as is // TODO: handle more cases - warn( + debug( 'evaluator:collectExportsAndImports:unfoldNamespaceImports', 'Unknown reference', referencePath.node.type @@ -897,7 +897,7 @@ function collectFromExportSpecifier( } // TODO: handle other cases - warn( + debug( 'evaluator:collectExportsAndImports:collectFromExportSpecifier', 'Unprocessed ExportSpecifier', path.node.type diff --git a/packages/utils/src/debug/perfMetter.ts b/packages/utils/src/debug/perfMetter.ts index b063933d9..44323e154 100644 --- a/packages/utils/src/debug/perfMetter.ts +++ b/packages/utils/src/debug/perfMetter.ts @@ -14,6 +14,7 @@ interface IProcessedDependency { exports: string[]; imports: { from: string; what: string[] }[]; passes: number; + fileIdx: string; } export interface IProcessedEvent { @@ -21,15 +22,25 @@ export interface IProcessedEvent { file: string; only: string[]; imports: { from: string; what: string[] }[]; + fileIdx: string; } export interface IQueueActionEvent { type: 'queue-action'; + datetime: Date; + queueIdx: string; action: string; file: string; args?: string[]; } +const formatTime = (date: Date) => { + return `${date.toLocaleTimeString()}.${date + .getMilliseconds() + .toString() + .padStart(3, '0')}`; +}; + const workingDir = process.cwd(); function replacer(_key: string, value: unknown): unknown { @@ -96,12 +107,18 @@ export const createPerfMeter = ( }; const processedDependencies = new Map(); - const processDependencyEvent = ({ file, only, imports }: IProcessedEvent) => { + const processDependencyEvent = ({ + file, + only, + imports, + fileIdx, + }: IProcessedEvent) => { if (!processedDependencies.has(file)) { processedDependencies.set(file, { exports: [], imports: [], passes: 0, + fileIdx, }); } @@ -112,14 +129,24 @@ export const createPerfMeter = ( }; const queueActions = new Map(); - const processQueueAction = ({ file, action, args }: IQueueActionEvent) => { + const processQueueAction = ({ + file, + action, + args, + queueIdx, + datetime, + }: IQueueActionEvent) => { if (!queueActions.has(file)) { queueActions.set(file, []); } const stringifiedArgs = args?.map((arg) => JSON.stringify(arg)).join(', ') ?? ''; - queueActions.get(file)!.push(`${action}(${stringifiedArgs})`); + queueActions + .get(file)! + .push( + `${queueIdx}:${action}(${stringifiedArgs})@${formatTime(datetime)}` + ); }; const processSingleEvent = ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1113da5ff..fc780050b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -586,15 +586,15 @@ importers: packages/logger: dependencies: debug: - specifier: ^4.1.1 + specifier: ^4.3.4 version: 4.3.4 picocolors: specifier: ^1.0.0 version: 1.0.0 devDependencies: '@types/debug': - specifier: ^4.1.5 - version: 4.1.7 + specifier: ^4.1.8 + version: 4.1.8 '@types/node': specifier: ^17.0.39 version: 17.0.39 @@ -863,6 +863,9 @@ importers: '@swc/core': specifier: ^1.3.20 version: 1.3.20 + debug: + specifier: ^4.3.4 + version: 4.3.4 dedent: specifier: ^0.7.0 version: 0.7.0 @@ -918,6 +921,9 @@ importers: '@types/babel__traverse': specifier: ^7.17.1 version: 7.17.1 + '@types/debug': + specifier: ^4.1.8 + version: 4.1.8 '@types/dedent': specifier: ^0.7.0 version: 0.7.0 @@ -4098,8 +4104,8 @@ packages: '@types/node': 17.0.39 dev: true - /@types/debug@4.1.7: - resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} + /@types/debug@4.1.8: + resolution: {integrity: sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==} dependencies: '@types/ms': 0.7.31 dev: true @@ -11902,7 +11908,7 @@ packages: /micromark@3.1.0: resolution: {integrity: sha512-6Mj0yHLdUZjHnOPgr5xfWIMqMWS12zDN6iws9SLuSz76W8jTtAv24MN4/CL7gJrl5vtxGInkkqDv/JIoRsQOvA==} dependencies: - '@types/debug': 4.1.7 + '@types/debug': 4.1.8 debug: 4.3.4 decode-named-character-reference: 1.0.2 micromark-core-commonmark: 1.0.6