diff --git a/examples/astro-solid/astro.config.js b/examples/astro-solid/astro.config.js index 0dba18965..e88b277bd 100644 --- a/examples/astro-solid/astro.config.js +++ b/examples/astro-solid/astro.config.js @@ -20,9 +20,6 @@ export default defineConfig({ vite_linaria({ displayName: true, classNameSlug: (hash, title, args) => `${args.dir}_${title}_${hash}`, - babelOptions: { - presets: ['solid'], - }, }), vite_inspect(), ], diff --git a/packages/babel/src/cache.ts b/packages/babel/src/cache.ts new file mode 100644 index 000000000..9cf53b44a --- /dev/null +++ b/packages/babel/src/cache.ts @@ -0,0 +1,13 @@ +import type Module from './module'; +import type { ITransformFileResult } from './types'; + +export class TransformCacheCollection { + constructor( + public readonly resolveCache: Map = new Map(), + public readonly codeCache: Map< + string, + Map + > = new Map(), + public readonly evalCache: Map = new Map() + ) {} +} diff --git a/packages/babel/src/evaluators/index.ts b/packages/babel/src/evaluators/index.ts index 10465cca0..57f7a0bd1 100644 --- a/packages/babel/src/evaluators/index.ts +++ b/packages/babel/src/evaluators/index.ts @@ -2,27 +2,20 @@ * This file is an entry point for module evaluation for getting lazy dependencies. */ +import type { TransformCacheCollection } from '../cache'; import Module from '../module'; import loadLinariaOptions from '../transform-stages/helpers/loadLinariaOptions'; -import type { Options, CodeCache } from '../types'; +import type { Options } from '../types'; export default function evaluate( - resolveCache: Map, - codeCache: CodeCache, - evalCache: Map, + cache: TransformCacheCollection, code: string[], options: Pick ) { const filename = options?.filename ?? 'unknown'; const pluginOptions = loadLinariaOptions(options.pluginOptions); - const m = new Module( - filename ?? 'unknown', - pluginOptions, - resolveCache, - codeCache, - evalCache - ); + const m = new Module(filename ?? 'unknown', pluginOptions, cache); m.dependencies = []; m.evaluate(code); diff --git a/packages/babel/src/index.ts b/packages/babel/src/index.ts index c8d7450ab..9e840c111 100644 --- a/packages/babel/src/index.ts +++ b/packages/babel/src/index.ts @@ -29,6 +29,7 @@ export { default as getVisitorKeys } from './utils/getVisitorKeys'; export type { VisitorKeys } from './utils/getVisitorKeys'; export { default as peek } from './utils/peek'; export { default as processTemplateExpression } from './utils/processTemplateExpression'; +export { TransformCacheCollection } from './cache'; function isEnabled(caller?: TransformCaller & { evaluate?: true }) { return caller?.name !== 'linaria' || caller.evaluate === true; diff --git a/packages/babel/src/module.ts b/packages/babel/src/module.ts index 22e75efb6..5b88fa9b7 100644 --- a/packages/babel/src/module.ts +++ b/packages/babel/src/module.ts @@ -24,8 +24,9 @@ import type { BaseProcessor } from '@linaria/tags'; import type { StrictOptions } from '@linaria/utils'; import { getFileIdx } from '@linaria/utils'; +import { TransformCacheCollection } from './cache'; import * as process from './process'; -import type { CodeCache } from './types'; +import type { ITransformFileResult } from './types'; // Supported node builtins based on the modules polyfilled by webpack // `true` means module is polyfilled, `false` means module is empty @@ -121,12 +122,16 @@ class Module { debug: CustomDebug; + resolveCache: Map; + + codeCache: Map>; + + evalCache: Map; + constructor( filename: string, options: StrictOptions, - private resolveCache: Map, - private codeCache: CodeCache, - private evalCache: Map, + { codeCache, evalCache, resolveCache } = new TransformCacheCollection(), private debuggerDepth = 0, private parentModule?: Module ) { @@ -140,6 +145,10 @@ class Module { this.transform = null; this.debug = createCustomDebug('module', this.idx); + this.resolveCache = resolveCache; + this.codeCache = codeCache; + this.evalCache = evalCache; + Object.defineProperties(this, { id: { value: filename, @@ -338,9 +347,11 @@ class Module { m = new Module( filename, this.options, - this.resolveCache, - this.codeCache, - this.evalCache, + { + codeCache: this.codeCache, + evalCache: this.evalCache, + resolveCache: this.resolveCache, + }, this.debuggerDepth + 1, this ); diff --git a/packages/babel/src/plugins/babel-transform.ts b/packages/babel/src/plugins/babel-transform.ts index 372edb202..166af77ef 100644 --- a/packages/babel/src/plugins/babel-transform.ts +++ b/packages/babel/src/plugins/babel-transform.ts @@ -3,13 +3,14 @@ import type { NodePath } from '@babel/traverse'; import { debug } from '@linaria/logger'; import type { StrictOptions } from '@linaria/utils'; -import { removeWithRelated, syncResolve } from '@linaria/utils'; +import { removeWithRelated, syncAcquire } from '@linaria/utils'; import type { Core } from '../babel'; -import type Module from '../module'; +import { TransformCacheCollection } from '../cache'; import { prepareForEvalSync } from '../transform-stages/1-prepare-for-eval'; import evalStage from '../transform-stages/2-eval'; -import type { CodeCache, IPluginState } from '../types'; +import type { IPluginState } from '../types'; +import { createEntryPoint } from '../utils/createEntryPoint'; import processTemplateExpression from '../utils/processTemplateExpression'; import withLinariaMetadata from '../utils/withLinariaMetadata'; @@ -17,26 +18,19 @@ export default function collector( babel: Core, options: StrictOptions ): PluginObj { - const codeCache: CodeCache = new Map(); - const resolveCache = new Map(); - const evalCache = new Map(); + const cache = new TransformCacheCollection(); return { name: '@linaria/babel/babel-transform', pre(file: BabelFile) { debug('babel-transform:start', file.opts.filename); - const entryPoint = { - name: file.opts.filename!, - code: file.code, - only: ['__linariaPreval'], - }; + const entryPoint = createEntryPoint(file.opts.filename!, file.code); const prepareStageResults = prepareForEvalSync( babel, - resolveCache, - codeCache, - syncResolve, + cache, + syncAcquire, entryPoint, { root: file.opts.root ?? undefined, @@ -52,9 +46,7 @@ export default function collector( } const evalStageResult = evalStage( - resolveCache, - codeCache, - evalCache, + cache, prepareStageResults.map((r) => r.code), { filename: file.opts.filename!, 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 550d18e81..423575f4b 100644 --- a/packages/babel/src/transform-stages/1-prepare-for-eval.ts +++ b/packages/babel/src/transform-stages/1-prepare-for-eval.ts @@ -1,4 +1,3 @@ -import { readFileSync } from 'fs'; import { dirname, extname } from 'path'; import type { BabelFileResult, TransformOptions } from '@babel/core'; @@ -8,8 +7,13 @@ import type { EvalRule, Evaluator } from '@linaria/utils'; import { buildOptions, getFileIdx, loadBabelOptions } from '@linaria/utils'; import type { Core } from '../babel'; +import type { TransformCacheCollection } from '../cache'; import type Module from '../module'; -import type { CodeCache, ITransformFileResult, Options } from '../types'; +import type { + ExternalAcquireResult, + ITransformFileResult, + Options, +} from '../types'; import withLinariaMetadata from '../utils/withLinariaMetadata'; import cachedParseSync from './helpers/cachedParseSync'; @@ -172,26 +176,22 @@ function prepareCode( return [...result, preevalStageResult.metadata]; } +type ProcessQueueItemResult = { + imports: Map | null; + name: string; + results: ITransformFileResult[]; +}; + function processQueueItem( babel: Core, - item: { - name: string; - code: string; - only: string[]; - } | null, - codeCache: CodeCache, + item: FileInQueue | null, + cache: TransformCacheCollection, options: Pick -): - | { - imports: Map | null; - name: string; - results: ITransformFileResult[]; - } - | undefined { +): ProcessQueueItemResult | undefined { if (!item) { return undefined; } - + const { codeCache } = cache; const pluginOptions = loadLinariaOptions(options.pluginOptions); const results = new Set(); @@ -272,14 +272,17 @@ function processQueueItem( export function prepareForEvalSync( babel: Core, - resolveCache: Map, - codeCache: CodeCache, - resolve: (what: string, importer: string, stack: string[]) => string, + cache: TransformCacheCollection, + acquire: ( + what: string, + importer: string, + stack: string[] + ) => ExternalAcquireResult, resolvedFile: FileInQueue, options: Pick, stack: string[] = [] ): ITransformFileResult[] | undefined { - const processed = processQueueItem(babel, resolvedFile, codeCache, options); + const processed = processQueueItem(babel, resolvedFile, cache, options); if (!processed) return undefined; const { imports, name, results } = processed; @@ -290,28 +293,24 @@ export function prepareForEvalSync( imports?.forEach((importsOnly, importedFile) => { try { - const resolved = resolve(importedFile, name, stack); - log('stage-1:sync-resolve', `✅ ${importedFile} -> ${resolved}`); - resolveCache.set( + const { id, code } = acquire(importedFile, name, stack); + log('stage-1:sync-acquire', `✅ ${importedFile} -> ${id}`); + cache.resolveCache.set( `${name} -> ${importedFile}`, - `${resolved}\0${importsOnly.join(',')}` + `${id}\0${importsOnly.join(',')}` ); - const fileContent = readFileSync(resolved, 'utf8'); queue.push({ - name: resolved, + name: id, only: importsOnly, - code: fileContent, + code, }); } catch (err) { - log('stage-1:sync-resolve', `❌ cannot resolve ${importedFile}: %O`, err); + log('stage-1:sync-acquire', `❌ cannot acquire ${importedFile}: %O`, err); } }); queue.forEach((item) => { - prepareForEvalSync(babel, resolveCache, codeCache, resolve, item, options, [ - name, - ...stack, - ]); + prepareForEvalSync(babel, cache, acquire, item, options, [name, ...stack]); }); return Array.from(results); @@ -325,13 +324,12 @@ const mutexes = new Map>(); */ export default async function prepareForEval( babel: Core, - resolveCache: Map, - codeCache: CodeCache, - resolve: ( + cache: TransformCacheCollection, + acquire: ( what: string, importer: string, stack: string[] - ) => Promise, + ) => Promise, file: Promise, options: Pick, stack: string[] = [] @@ -342,7 +340,7 @@ export default async function prepareForEval( await mutex; } - const processed = processQueueItem(babel, resolvedFile, codeCache, options); + const processed = processQueueItem(babel, resolvedFile, cache, options); if (!processed) return undefined; const { imports, name, results } = processed; @@ -352,16 +350,17 @@ export default async function prepareForEval( const promises: Promise[] = []; imports?.forEach((importsOnly, importedFile) => { - const promise = resolve(importedFile, name, stack).then( + const promise = acquire(importedFile, name, stack).then( (resolved) => { if (resolved === null) { - log('stage-1:resolve', `✅ ${importedFile} is ignored`); + log('stage-1:acquire', `✅ ${importedFile} is ignored`); return null; } + const { code, id } = resolved; - log('stage-1:async-resolve', `✅ ${importedFile} -> ${resolved}`); + log('stage-1:acquire', `✅ ${importedFile} -> ${id}`); const resolveCacheKey = `${name} -> ${importedFile}`; - const cached = resolveCache.get(resolveCacheKey); + const cached = cache.resolveCache.get(resolveCacheKey); const importsOnlySet = new Set(importsOnly); if (cached) { const [, cachedOnly] = cached.split('\0'); @@ -370,37 +369,24 @@ export default async function prepareForEval( }); } - resolveCache.set( + cache.resolveCache.set( resolveCacheKey, - `${resolved}\0${[...importsOnlySet].join(',')}` + `${id}\0${[...importsOnlySet].join(',')}` ); - const fileContent = readFileSync(resolved, 'utf8'); return { - name: resolved, + name: id, only: importsOnly, - code: fileContent, + code, }; }, (err: unknown) => { - log( - 'stage-1:async-resolve', - `❌ cannot resolve ${importedFile}: %O`, - err - ); + log('stage-1:acquire', `❌ cannot resolve ${importedFile}: %O`, err); return null; } ); promises.push( - prepareForEval( - babel, - resolveCache, - codeCache, - resolve, - promise, - options, - [name, ...stack] - ) + prepareForEval(babel, cache, acquire, promise, options, [name, ...stack]) ); }); diff --git a/packages/babel/src/transform-stages/2-eval.ts b/packages/babel/src/transform-stages/2-eval.ts index 9b0745613..4c084d5f9 100644 --- a/packages/babel/src/transform-stages/2-eval.ts +++ b/packages/babel/src/transform-stages/2-eval.ts @@ -2,9 +2,9 @@ import { createCustomDebug } from '@linaria/logger'; import type { ValueCache } from '@linaria/tags'; import { getFileIdx } from '@linaria/utils'; +import type { TransformCacheCollection } from '../cache'; import evaluate from '../evaluators'; -import type Module from '../module'; -import type { CodeCache, Options } from '../types'; +import type { Options } from '../types'; import hasLinariaPreval from '../utils/hasLinariaPreval'; const wrap = (fn: () => T): T | Error => { @@ -19,9 +19,7 @@ const wrap = (fn: () => T): T | Error => { * Evaluates template dependencies. */ export default function evalStage( - resolveCache: Map, - codeCache: CodeCache, - evalCache: Map, + cache: TransformCacheCollection, code: string[], options: Pick ): [ValueCache, string[]] | null { @@ -29,7 +27,7 @@ export default function evalStage( log('stage-2', `>> evaluate __linariaPreval`); - const evaluated = evaluate(resolveCache, codeCache, evalCache, code, options); + const evaluated = evaluate(cache, code, options); const linariaPreval = hasLinariaPreval(evaluated.value) ? evaluated.value.__linariaPreval diff --git a/packages/babel/src/transform.ts b/packages/babel/src/transform.ts index e19cffd3e..9b9a9a413 100644 --- a/packages/babel/src/transform.ts +++ b/packages/babel/src/transform.ts @@ -10,24 +10,28 @@ import type { TransformOptions } from '@babel/core'; import * as babel from '@babel/core'; -import type Module from './module'; +import { TransformCacheCollection } from './cache'; import prepareForEval, { prepareForEvalSync, } from './transform-stages/1-prepare-for-eval'; import evalStage from './transform-stages/2-eval'; import prepareForRuntime from './transform-stages/3-prepare-for-runtime'; import extractStage from './transform-stages/4-extract'; -import type { Options, Result, CodeCache, ITransformFileResult } from './types'; +import type { + Options, + Result, + ITransformFileResult, + ExternalAcquireResult, +} from './types'; +import { createEntryPoint } from './utils/createEntryPoint'; import withLinariaMetadata from './utils/withLinariaMetadata'; function syncStages( originalCode: string, options: Options, prepareStageResults: ITransformFileResult[] | undefined, - babelConfig: TransformOptions = {}, - resolveCache = new Map(), - codeCache: CodeCache = new Map(), - evalCache = new Map(), + babelConfig: TransformOptions, + cache: TransformCacheCollection, eventEmitter?: (ev: unknown) => void ) { const { filename } = options; @@ -48,9 +52,7 @@ function syncStages( eventEmitter?.({ type: 'transform:stage-2:start', filename }); const evalStageResult = evalStage( - resolveCache, - codeCache, - evalCache, + cache, prepareStageResults.map((r) => r.code), options ); @@ -114,30 +116,26 @@ function syncStages( export function transformSync( originalCode: string, options: Options, - syncResolve: (what: string, importer: string, stack: string[]) => string, + acquire: ( + what: string, + importer: string, + stack: string[] + ) => ExternalAcquireResult, babelConfig: TransformOptions = {}, - resolveCache = new Map(), - codeCache: CodeCache = new Map(), - evalCache = new Map(), + cache = new TransformCacheCollection(), eventEmitter?: (ev: unknown) => void ): Result { const { filename } = options; - // *** 1st stage *** eventEmitter?.({ type: 'transform:stage-1:start', filename }); - const entryPoint = { - name: options.filename, - code: originalCode, - only: ['__linariaPreval'], - }; + const entryPoint = createEntryPoint(filename, originalCode); const prepareStageResults = prepareForEvalSync( babel, - resolveCache, - codeCache, - syncResolve, + cache, + acquire, entryPoint, options ); @@ -151,9 +149,7 @@ export function transformSync( options, prepareStageResults, babelConfig, - resolveCache, - codeCache, - evalCache, + cache, eventEmitter ); } @@ -161,15 +157,13 @@ export function transformSync( export default async function transform( originalCode: string, options: Options, - asyncResolve: ( + aqcuire: ( what: string, importer: string, stack: string[] - ) => Promise, + ) => Promise, babelConfig: TransformOptions = {}, - resolveCache = new Map(), - codeCache: CodeCache = new Map(), - evalCache = new Map(), + cache = new TransformCacheCollection(), eventEmitter?: (ev: unknown) => void ): Promise { const { filename } = options; @@ -178,17 +172,12 @@ export default async function transform( eventEmitter?.({ type: 'transform:stage-1:start', filename }); - const entryPoint = Promise.resolve({ - name: filename, - code: originalCode, - only: ['__linariaPreval'], - }); + const entryPoint = Promise.resolve(createEntryPoint(filename, originalCode)); const prepareStageResults = await prepareForEval( babel, - resolveCache, - codeCache, - asyncResolve, + cache, + aqcuire, entryPoint, options ); @@ -202,9 +191,7 @@ export default async function transform( options, prepareStageResults, babelConfig, - resolveCache, - codeCache, - evalCache, + cache, eventEmitter ); } diff --git a/packages/babel/src/types.ts b/packages/babel/src/types.ts index 51038f043..00c5caa07 100644 --- a/packages/babel/src/types.ts +++ b/packages/babel/src/types.ts @@ -56,8 +56,6 @@ export interface ITransformFileResult { code: string; } -export type CodeCache = Map>; - export type Stage = 'preeval' | 'collect'; export type Location = { @@ -98,3 +96,8 @@ export type MissedBabelCoreTypes = { file: { code: string; ast: File } ) => { path: NodePath }; }; + +export type ExternalAcquireResult = { + id: string; + code: string; +}; diff --git a/packages/babel/src/utils/createEntryPoint.ts b/packages/babel/src/utils/createEntryPoint.ts new file mode 100644 index 000000000..fd4d73266 --- /dev/null +++ b/packages/babel/src/utils/createEntryPoint.ts @@ -0,0 +1,9 @@ +export const createEntryPoint = ( + name: string, + code: string, + only = ['__linariaPreval'] +) => ({ + name, + code, + only, +}); diff --git a/packages/cli/src/linaria.ts b/packages/cli/src/linaria.ts index 7c68a5359..42be884d3 100644 --- a/packages/cli/src/linaria.ts +++ b/packages/cli/src/linaria.ts @@ -11,9 +11,8 @@ import mkdirp from 'mkdirp'; import normalize from 'normalize-path'; import yargs from 'yargs'; -import type { CodeCache, Module } from '@linaria/babel-preset'; -import { transform } from '@linaria/babel-preset'; -import { asyncResolveFallback } from '@linaria/utils'; +import { TransformCacheCollection, transform } from '@linaria/babel-preset'; +import { asyncAcquire } from '@linaria/utils'; const modulesOptions = [ 'commonjs', @@ -124,10 +123,7 @@ async function processFiles(files: (number | string)[], options: Options) { ], [] as string[] ); - - const codeCache: CodeCache = new Map(); - const resolveCache = new Map(); - const evalCache = new Map(); + const cache = new TransformCacheCollection(); const timings = new Map(); const addTiming = (key: string, value: number) => { @@ -178,11 +174,9 @@ async function processFiles(files: (number | string)[], options: Options) { }, root: options.sourceRoot, }, - asyncResolveFallback, + asyncAcquire, {}, - resolveCache, - codeCache, - evalCache, + cache, onEvent ); diff --git a/packages/esbuild/src/index.ts b/packages/esbuild/src/index.ts index 6117baaf9..67ff00416 100644 --- a/packages/esbuild/src/index.ts +++ b/packages/esbuild/src/index.ts @@ -4,14 +4,18 @@ * returns transformed code without template literals and attaches generated source maps */ -import fs from 'fs'; +import fs from 'fs/promises'; import path from 'path'; import type { Plugin, TransformOptions, Loader } from 'esbuild'; -import { transformSync } from 'esbuild'; +import { transform as esbuildTransform } from 'esbuild'; -import type { PluginOptions, Preprocessor } from '@linaria/babel-preset'; -import { slugify, transform } from '@linaria/babel-preset'; +import type { + ExternalAcquireResult, + PluginOptions, + Preprocessor, +} from '@linaria/babel-preset'; +import { slugify, transform as linariaTransform } from '@linaria/babel-preset'; type EsbuildPluginOptions = { sourceMap?: boolean; @@ -33,10 +37,10 @@ export default function linaria({ setup(build) { const cssLookup = new Map(); - const asyncResolve = async ( + const acquire = async ( token: string, importer: string - ): Promise => { + ): Promise => { const context = path.isAbsolute(importer) ? path.dirname(importer) : path.join(process.cwd(), path.dirname(importer)); @@ -48,8 +52,8 @@ export default function linaria({ if (result.errors.length > 0) { throw new Error(`Cannot resolve ${token}`); } - - return result.path; + const code = await fs.readFile(result.path, 'utf8'); + return { id: result.path, code }; }; build.onResolve({ filter: /\.linaria\.css$/ }, (args) => { @@ -68,7 +72,7 @@ export default function linaria({ }); build.onLoad({ filter: /\.(js|jsx|ts|tsx)$/ }, async (args) => { - const rawCode = fs.readFileSync(args.path, 'utf8'); + const rawCode = await fs.readFile(args.path, 'utf8'); const { ext, name: filename } = path.parse(args.path); const loader = ext.replace(/^\./, '') as Loader; @@ -89,7 +93,7 @@ export default function linaria({ } } - const transformed = transformSync(rawCode, { + const transformed = await esbuildTransform(rawCode, { ...options, sourcefile: args.path, sourcemap: sourceMap, @@ -102,14 +106,14 @@ export default function linaria({ code += `/*# sourceMappingURL=data:application/json;base64,${esbuildMap}*/`; } - const result = await transform( + const result = await linariaTransform( code, { filename: args.path, preprocessor, pluginOptions: rest, }, - asyncResolve + acquire ); if (!result.cssText) { diff --git a/packages/rollup/src/index.ts b/packages/rollup/src/index.ts index 1703e50b8..fe9da1637 100644 --- a/packages/rollup/src/index.ts +++ b/packages/rollup/src/index.ts @@ -7,13 +7,16 @@ import { createFilter } from '@rollup/pluginutils'; import type { Plugin } from 'rollup'; -import { transform, slugify } from '@linaria/babel-preset'; +import { + transform, + slugify, + TransformCacheCollection, +} from '@linaria/babel-preset'; import type { PluginOptions, Preprocessor, Result, - CodeCache, - Module, + ExternalAcquireResult, } from '@linaria/babel-preset'; import { createCustomDebug } from '@linaria/logger'; import { getFileIdx } from '@linaria/utils'; @@ -36,9 +39,7 @@ export default function linaria({ }: RollupPluginOptions = {}): Plugin { const filter = createFilter(include, exclude); const cssLookup: { [key: string]: string } = {}; - const codeCache: CodeCache = new Map(); - const resolveCache = new Map(); - const evalCache = new Map(); + const cache = new TransformCacheCollection(); const plugin: Plugin = { name: 'linaria', @@ -60,20 +61,25 @@ export default function linaria({ log('rollup-init', id); - const asyncResolve = async (what: string, importer: string) => { + const acquire = async ( + what: string, + importer: string + ): Promise => { const resolved = await this.resolve(what, importer); if (resolved) { log('resolve', "✅ '%s'@'%s -> %O\n%s", what, importer, resolved); // Vite adds param like `?v=667939b3` to cached modules - const resolvedId = resolved.id.split('?')[0]; + const [acquiredId] = resolved.id.split('?'); - if (resolvedId.startsWith('\0')) { + if (acquiredId.startsWith('\0')) { // \0 is a special character in Rollup that tells Rollup to not include this in the bundle // https://rollupjs.org/guide/en/#outputexports return null; } - return resolvedId; + const module = await this.load({ id: acquiredId }); + if (module?.code == null) return null; + return { id: acquiredId, code: module.code }; } log('resolve', "❌ '%s'@'%s", what, importer); @@ -87,11 +93,9 @@ export default function linaria({ preprocessor, pluginOptions: rest, }, - asyncResolve, + acquire, {}, - resolveCache, - codeCache, - evalCache + cache ); if (!result.cssText) return; diff --git a/packages/stylelint/src/preprocessor.ts b/packages/stylelint/src/preprocessor.ts index 90c3a1b3b..9d2f031d1 100644 --- a/packages/stylelint/src/preprocessor.ts +++ b/packages/stylelint/src/preprocessor.ts @@ -2,7 +2,7 @@ import stripAnsi from 'strip-ansi'; import { transform } from '@linaria/babel-preset'; import type { Replacement } from '@linaria/babel-preset'; -import { asyncResolveFallback } from '@linaria/utils'; +import { asyncAcquire } from '@linaria/utils'; type Errors = { [key: string]: @@ -63,7 +63,7 @@ function preprocessor() { { filename, }, - asyncResolveFallback + asyncAcquire ); cache[filename] = undefined; diff --git a/packages/testkit/src/module.test.ts b/packages/testkit/src/module.test.ts index ee71c6f64..a98331515 100644 --- a/packages/testkit/src/module.test.ts +++ b/packages/testkit/src/module.test.ts @@ -3,7 +3,7 @@ import path from 'path'; import * as babel from '@babel/core'; import dedent from 'dedent'; -import { Module } from '@linaria/babel-preset'; +import { Module, TransformCacheCollection } from '@linaria/babel-preset'; import type { Evaluator, StrictOptions } from '@linaria/utils'; beforeEach(() => Module.invalidate()); @@ -38,17 +38,9 @@ const options: StrictOptions = { beforeEach(() => Module.invalidateEvalCache()); -function createModule( - filename: string, - opts: StrictOptions, - evalCache: Map = new Map() -) { - return new Module(filename, opts, new Map(), new Map(), evalCache); -} - it('creates module for JS files', () => { const filename = '/foo/bar/test.js'; - const mod = createModule(filename, options); + const mod = new Module(filename, options); mod.evaluate('module.exports = () => 42'); @@ -58,7 +50,7 @@ it('creates module for JS files', () => { }); it('requires JS files', () => { - const mod = createModule(getFileName(), options); + const mod = new Module(getFileName(), options); mod.evaluate(dedent` const answer = require('./sample-script'); @@ -70,7 +62,7 @@ it('requires JS files', () => { }); it('requires JSON files', () => { - const mod = createModule(getFileName(), options); + const mod = new Module(getFileName(), options); mod.evaluate(dedent` const data = require('./sample-data.json'); @@ -83,34 +75,34 @@ it('requires JSON files', () => { it('returns module from the cache', () => { const filename = getFileName(); - const cache = new Map(); - const mod = createModule(filename, options, cache); + const cache = new TransformCacheCollection(); + const mod = new Module(filename, options, cache); const id = './sample-data.json'; expect(mod.require(id)).toBe(mod.require(id)); - const res1 = createModule(filename, options, cache).require(id); - const res2 = createModule(filename, options, cache).require(id); + const res1 = new Module(filename, options, cache).require(id); + const res2 = new Module(filename, options, cache).require(id); expect(res1).toBe(res2); }); it('clears modules from the cache', () => { const filename = getFileName(); - const cache = new Map(); + const cache = new TransformCacheCollection(); const id = './sample-data.json'; - const result = createModule(filename, options, cache).require(id); + const result = new Module(filename, options, cache).require(id); - expect(createModule(filename, options, cache).require(id)).toBe(result); + expect(new Module(filename, options, cache).require(id)).toBe(result); - cache.clear(); + cache.evalCache.clear(); - expect(createModule(filename, options, cache).require(id)).not.toBe(result); + expect(new Module(filename, options, cache).require(id)).not.toBe(result); }); it('exports the path for non JS/JSON files', () => { - const mod = createModule(getFileName(), options); + const mod = new Module(getFileName(), options); expect(mod.require('./sample-asset.png')).toBe( path.join(__dirname, '__fixtures__', 'sample-asset.png') @@ -118,19 +110,19 @@ it('exports the path for non JS/JSON files', () => { }); it('returns module when requiring mocked builtin node modules', () => { - const mod = createModule(getFileName(), options); + const mod = new Module(getFileName(), options); expect(mod.require('path')).toBe(require('path')); }); it('returns null when requiring empty builtin node modules', () => { - const mod = createModule(getFileName(), options); + const mod = new Module(getFileName(), options); expect(mod.require('fs')).toBe(null); }); it('throws when requiring unmocked builtin node modules', () => { - const mod = createModule(getFileName(), options); + const mod = new Module(getFileName(), options); expect(() => mod.require('perf_hooks')).toThrow( 'Unable to import "perf_hooks". Importing Node builtins is not supported in the sandbox.' @@ -138,7 +130,7 @@ it('throws when requiring unmocked builtin node modules', () => { }); it('has access to the global object', () => { - const mod = createModule(getFileName(), options); + const mod = new Module(getFileName(), options); expect(() => mod.evaluate(dedent` @@ -148,7 +140,7 @@ it('has access to the global object', () => { }); it("doesn't have access to the process object", () => { - const mod = createModule(getFileName(), options); + const mod = new Module(getFileName(), options); expect(() => mod.evaluate(dedent` @@ -158,7 +150,7 @@ it("doesn't have access to the process object", () => { }); it('has access to NODE_ENV', () => { - const mod = createModule(getFileName(), options); + const mod = new Module(getFileName(), options); mod.evaluate(dedent` module.exports = process.env.NODE_ENV; @@ -168,7 +160,7 @@ it('has access to NODE_ENV', () => { }); it('has require.resolve available', () => { - const mod = createModule(getFileName(), options); + const mod = new Module(getFileName(), options); mod.evaluate(dedent` module.exports = require.resolve('./sample-script'); @@ -180,7 +172,7 @@ it('has require.resolve available', () => { }); it('has require.ensure available', () => { - const mod = createModule(getFileName(), options); + const mod = new Module(getFileName(), options); expect(() => mod.evaluate(dedent` @@ -190,7 +182,7 @@ it('has require.ensure available', () => { }); it('has __filename available', () => { - const mod = createModule(getFileName(), options); + const mod = new Module(getFileName(), options); mod.evaluate(dedent` module.exports = __filename; @@ -200,7 +192,7 @@ it('has __filename available', () => { }); it('has __dirname available', () => { - const mod = createModule(getFileName(), options); + const mod = new Module(getFileName(), options); mod.evaluate(dedent` module.exports = __dirname; @@ -210,7 +202,7 @@ it('has __dirname available', () => { }); it('has setTimeout, clearTimeout available', () => { - const mod = createModule(getFileName(), options); + const mod = new Module(getFileName(), options); expect(() => mod.evaluate(dedent` @@ -224,7 +216,7 @@ it('has setTimeout, clearTimeout available', () => { }); it('has setInterval, clearInterval available', () => { - const mod = createModule(getFileName(), options); + const mod = new Module(getFileName(), options); expect(() => mod.evaluate(dedent` @@ -238,7 +230,7 @@ it('has setInterval, clearInterval available', () => { }); it('has setImmediate, clearImmediate available', () => { - const mod = createModule(getFileName(), options); + const mod = new Module(getFileName(), options); expect(() => mod.evaluate(dedent` @@ -252,7 +244,7 @@ it('has setImmediate, clearImmediate available', () => { }); it('has global objects available without referencing global', () => { - const mod = createModule(getFileName(), options); + const mod = new Module(getFileName(), options); expect(() => mod.evaluate(dedent` @@ -266,7 +258,7 @@ it('changes resolve behaviour on overriding _resolveFilename', () => { Module._resolveFilename = (id) => (id === 'foo' ? 'bar' : id); - const mod = createModule(getFileName(), options); + const mod = new Module(getFileName(), options); mod.evaluate(dedent` module.exports = [ @@ -283,7 +275,7 @@ it('changes resolve behaviour on overriding _resolveFilename', () => { it('correctly processes export declarations in strict mode', () => { const filename = '/foo/bar/test.js'; - const mod = createModule(filename, options); + const mod = new Module(filename, options); mod.evaluate('"use strict"; exports = module.exports = () => 42'); @@ -293,7 +285,7 @@ it('correctly processes export declarations in strict mode', () => { }); it('export * compiled by typescript to commonjs works', () => { - const mod = createModule(getFileName(), options); + const mod = new Module(getFileName(), options); mod.evaluate(dedent` const { foo } = require('./ts-compiled-re-exports'); diff --git a/packages/utils/src/acquireFallback.ts b/packages/utils/src/acquireFallback.ts new file mode 100644 index 000000000..5f8dead19 --- /dev/null +++ b/packages/utils/src/acquireFallback.ts @@ -0,0 +1,27 @@ +import { readFileSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; + +import asyncResolve, { syncResolve } from './asyncResolveFallback'; + +const encoding = 'utf8'; +export const syncAcquire = ( + what: string, + importer: string, + stack: string[] +) => { + const id = syncResolve(what, importer, stack); + const code = readFileSync(id, encoding); + return { id, code }; +}; + +export const asyncAcquire = async ( + what: string, + importer: string, + stack: string[] +) => { + const id = await asyncResolve(what, importer, stack); + const code = await readFile(id, encoding); + return { id, code }; +}; + +export default asyncAcquire; diff --git a/packages/utils/src/asyncResolveFallback.ts b/packages/utils/src/asyncResolveFallback.ts index b6ce583a0..0600d9f72 100644 --- a/packages/utils/src/asyncResolveFallback.ts +++ b/packages/utils/src/asyncResolveFallback.ts @@ -1,4 +1,4 @@ -import path from 'path'; +import path from 'node:path'; const safeResolve = (name: string, where: string[]): string | Error => { try { diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 597be41c8..f1106ca3d 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -3,6 +3,7 @@ export { default as asyncResolveFallback, syncResolve, } from './asyncResolveFallback'; +export * from './acquireFallback'; export { default as collectExportsAndImports } from './collectExportsAndImports'; export * from './collectExportsAndImports'; export { default as findIdentifiers, nonType } from './findIdentifiers'; diff --git a/packages/vite/src/index.ts b/packages/vite/src/index.ts index e5317c61e..3272797eb 100644 --- a/packages/vite/src/index.ts +++ b/packages/vite/src/index.ts @@ -10,13 +10,12 @@ import { createFilter } from '@rollup/pluginutils'; import type { FilterPattern } from '@rollup/pluginutils'; import type { ModuleNode, Plugin, ResolvedConfig, ViteDevServer } from 'vite'; -import { transform, slugify } from '@linaria/babel-preset'; -import type { - PluginOptions, - Preprocessor, - CodeCache, - Module, +import { + transform, + slugify, + TransformCacheCollection, } from '@linaria/babel-preset'; +import type { PluginOptions, Preprocessor } from '@linaria/babel-preset'; import { createCustomDebug } from '@linaria/logger'; import { getFileIdx } from '@linaria/utils'; @@ -43,12 +42,11 @@ export default function linaria({ // const targets: { id: string; dependencies: string[] }[] = []; - const codeCache: CodeCache = new Map(); - const resolveCache = new Map(); - const evalCache = new Map(); - + const cache = new TransformCacheCollection(); + const { codeCache, evalCache } = cache; return { name: 'linaria', + enforce: 'post', configResolved(resolvedConfig: ResolvedConfig) { config = resolvedConfig; }, @@ -106,31 +104,36 @@ export default function linaria({ log('rollup-init', id); - const asyncResolve = async (what: string, importer: string) => { + const acquire = async (what: string, importer: string) => { const resolved = await this.resolve(what, importer); if (resolved) { log('resolve', "✅ '%s'@'%s -> %O\n%s", what, importer, resolved); // Vite adds param like `?v=667939b3` to cached modules - const resolvedId = resolved.id.split('?')[0]; + const [acquiredId] = resolved.id.split('?'); - if (resolvedId.startsWith('\0')) { + if (acquiredId.startsWith('\0')) { // \0 is a special character in Rollup that tells Rollup to not include this in the bundle // https://rollupjs.org/guide/en/#outputexports return null; } - return resolvedId; + if (devServer) { + const transformResult = await devServer.transformRequest( + acquiredId + ); + if (transformResult?.code == null) return null; + return { id: acquiredId, code: transformResult.code }; + } + + const module = await this.load({ id: acquiredId }); + if (module?.code == null) return null; + return { id: acquiredId, code: module.code }; } log('resolve', "❌ '%s'@'%s", what, importer); throw new Error(`Could not resolve ${what}`); }; - // TODO: Vite surely has some already transformed modules, solid - // why would we transform it again? - // We could provide some thing like `pretransform` and ask Vite to return transformed module - // (module.transformResult) - // So we don't need to duplicate babel plugins. const result = await transform( code, { @@ -138,11 +141,9 @@ export default function linaria({ preprocessor, pluginOptions: rest, }, - asyncResolve, + acquire, {}, - resolveCache, - codeCache, - evalCache + cache ); let { cssText, dependencies } = result; diff --git a/packages/webpack4-loader/src/index.ts b/packages/webpack4-loader/src/index.ts index e8c64a09f..ab3faf485 100644 --- a/packages/webpack4-loader/src/index.ts +++ b/packages/webpack4-loader/src/index.ts @@ -9,7 +9,7 @@ import path from 'path'; import loaderUtils from 'loader-utils'; import type { RawSourceMap } from 'source-map'; -import type { Result } from '@linaria/babel-preset'; +import type { ExternalAcquireResult, Result } from '@linaria/babel-preset'; import { transform } from '@linaria/babel-preset'; import { debug } from '@linaria/logger'; @@ -49,17 +49,23 @@ export default function webpack4Loader( const outputFileName = this.resourcePath.replace(/\.[^.]+$/, extension); - const asyncResolve = (token: string, importer: string): Promise => { + const acquire = ( + token: string, + importer: string + ): Promise => { const context = path.isAbsolute(importer) ? path.dirname(importer) : path.join(process.cwd(), path.dirname(importer)); return new Promise((resolve, reject) => { - this.resolve(context, token, (err, result) => { - if (err) { - reject(err); - } else if (result) { - this.addDependency(result); - resolve(result); + this.resolve(context, token, (resolveError, id) => { + if (resolveError) { + reject(resolveError); + } else if (id) { + this.addDependency(id); + this.loadModule(id, (loadErr, code) => { + if (loadErr) reject(loadErr); + resolve({ id, code }); + }); } else { reject(new Error(`Cannot resolve ${token}`)); } @@ -75,7 +81,7 @@ export default function webpack4Loader( pluginOptions: rest, preprocessor, }, - asyncResolve + acquire ).then( async (result: Result) => { if (result.cssText) { @@ -88,9 +94,8 @@ export default function webpack4Loader( } await Promise.all( - result.dependencies?.map((dep) => - asyncResolve(dep, this.resourcePath) - ) ?? [] + result.dependencies?.map((dep) => acquire(dep, this.resourcePath)) ?? + [] ); try { diff --git a/packages/webpack5-loader/src/index.ts b/packages/webpack5-loader/src/index.ts index 2fd30a687..2ce028e97 100644 --- a/packages/webpack5-loader/src/index.ts +++ b/packages/webpack5-loader/src/index.ts @@ -9,7 +9,11 @@ import path from 'path'; import type { RawSourceMap } from 'source-map'; import type { RawLoaderDefinitionFunction } from 'webpack'; -import type { Result, Preprocessor } from '@linaria/babel-preset'; +import type { + Result, + Preprocessor, + ExternalAcquireResult, +} from '@linaria/babel-preset'; import { transform } from '@linaria/babel-preset'; import { debug } from '@linaria/logger'; @@ -62,17 +66,23 @@ const webpack5Loader: Loader = function webpack5LoaderPlugin( const outputFileName = this.resourcePath.replace(/\.[^.]+$/, extension); - const asyncResolve = (token: string, importer: string): Promise => { + const acquire = ( + token: string, + importer: string + ): Promise => { const context = path.isAbsolute(importer) ? path.dirname(importer) : path.join(process.cwd(), path.dirname(importer)); return new Promise((resolve, reject) => { - this.resolve(context, token, (err, result) => { - if (err) { - reject(err); - } else if (result) { - this.addDependency(result); - resolve(result); + this.resolve(context, token, (resolveError, id) => { + if (resolveError) { + reject(resolveError); + } else if (id) { + this.addDependency(id); + this.loadModule(id, (loadErr, code) => { + if (loadErr) reject(loadErr); + resolve({ id, code }); + }); } else { reject(new Error(`Cannot resolve ${token}`)); } @@ -88,7 +98,7 @@ const webpack5Loader: Loader = function webpack5LoaderPlugin( pluginOptions: rest, preprocessor, }, - asyncResolve + acquire ).then( async (result: Result) => { if (result.cssText) { @@ -101,9 +111,8 @@ const webpack5Loader: Loader = function webpack5LoaderPlugin( } await Promise.all( - result.dependencies?.map((dep) => - asyncResolve(dep, this.resourcePath) - ) ?? [] + result.dependencies?.map((dep) => acquire(dep, this.resourcePath)) ?? + [] ); try {