Skip to content

Commit

Permalink
fix(babel): improved cache (fixes #1304 and #1287) (#1306)
Browse files Browse the repository at this point in the history
  • Loading branch information
Anber committed Jul 27, 2023
1 parent a4263c4 commit 9bb782d
Show file tree
Hide file tree
Showing 14 changed files with 630 additions and 245 deletions.
7 changes: 7 additions & 0 deletions .changeset/poor-chefs-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@linaria/babel-preset': patch
'@linaria/logger': patch
'@linaria/testkit': patch
---

The improved cache that fixes race conditions which lead to "The expression evaluated to 'undefined'" (fixes #1304 and #1287)
141 changes: 123 additions & 18 deletions packages/babel/src/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,149 @@ import { createHash } from 'crypto';

import type { File } from '@babel/types';

import type Module from './module';
import { linariaLogger } from '@linaria/logger';

import type { IModule } from './module';
import type { ITransformFileResult } from './types';

function hashContent(content: string) {
return createHash('sha256').update(content).digest('hex');
}

interface ICaches {
resolve: Map<string, string>;
resolveTask: Map<
string,
Promise<{
importedFile: string;
importsOnly: string[];
resolved: string | null;
}>
>;
code: Map<
string,
{
imports: Map<string, string[]> | null;
only: string[];
result: ITransformFileResult;
}
>;
eval: Map<string, IModule>;
originalAST: Map<string, File>;
}

type MapValue<T> = T extends Map<string, infer V> ? V : never;

const cacheLogger = linariaLogger.extend('cache');

const cacheNames = [
'resolve',
'resolveTask',
'code',
'eval',
'originalAST',
] as const;
type CacheNames = typeof cacheNames[number];

const loggers = cacheNames.reduce(
(acc, key) => ({
...acc,
[key]: cacheLogger.extend(key),
}),
{} as Record<CacheNames, typeof linariaLogger>
);

export class TransformCacheCollection {
private contentHashes = new Map<string, string>();

constructor(
public readonly resolveCache: Map<string, string> = new Map(),
public readonly codeCache: Map<
string,
{
imports: Map<string, string[]> | null;
only: string[];
result: ITransformFileResult;
}
> = new Map(),
public readonly evalCache: Map<string, Module> = new Map(),
public readonly originalASTCache: Map<string, File> = new Map()
) {}
protected readonly resolve: Map<string, string>;

protected readonly resolveTask: Map<string, Promise<string>>;

protected readonly code: Map<
string,
{
imports: Map<string, string[]> | null;
only: string[];
result: ITransformFileResult;
}
>;

protected readonly eval: Map<string, IModule>;

protected readonly originalAST: Map<string, File>;

constructor(caches: Partial<ICaches> = {}) {
this.resolve = caches.resolve || new Map();
this.resolveTask = caches.resolveTask || new Map();
this.code = caches.code || new Map();
this.eval = caches.eval || new Map();
this.originalAST = caches.originalAST || new Map();
}

public invalidateForFile(filename: string) {
this.resolveCache.delete(filename);
this.codeCache.delete(filename);
this.evalCache.delete(filename);
this.originalASTCache.delete(filename);
cacheNames.forEach((cacheName) => {
this.invalidate(cacheName, filename);
});
}

public invalidateIfChanged(filename: string, content: string) {
const hash = this.contentHashes.get(filename);
const newHash = hashContent(content);

if (hash !== newHash) {
cacheLogger('content has changed, invalidate all for %s', filename);
this.contentHashes.set(filename, newHash);
this.invalidateForFile(filename);
}
}

public add<
TCache extends CacheNames,
TValue extends MapValue<ICaches[TCache]>
>(cacheName: TCache, key: string, value: TValue): void {
const cache = this[cacheName] as Map<string, TValue>;
loggers[cacheName]('add %s %f', key, () => {
if (!cache.has(key)) {
return 'added';
}

return cache.get(key) === value ? 'unchanged' : 'updated';
});

cache.set(key, value);
}

public get<
TCache extends CacheNames,
TValue extends MapValue<ICaches[TCache]>
>(cacheName: TCache, key: string): TValue | undefined {
const cache = this[cacheName] as Map<string, TValue>;

const res = cache.get(key);
loggers[cacheName]('get', key, res === undefined ? 'miss' : 'hit');
return res;
}

public has(cacheName: CacheNames, key: string): boolean {
const cache = this[cacheName] as Map<string, unknown>;

const res = cache.has(key);
loggers[cacheName]('has', key, res);
return res;
}

public invalidate(cacheName: CacheNames, key: string): void {
loggers[cacheName]('invalidate', key);
const cache = this[cacheName] as Map<string, unknown>;

cache.delete(key);
}

public clear(cacheName: CacheNames): void {
loggers[cacheName]('clear');
const cache = this[cacheName] as Map<string, unknown>;

cache.clear();
}
}
7 changes: 6 additions & 1 deletion packages/babel/src/evaluators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ export default function evaluate(
pluginOptions: StrictOptions,
filename: string
) {
const m = new Module(filename ?? 'unknown', pluginOptions, cache);
const m = new Module(
filename ?? 'unknown',
'__linariaPreval',
pluginOptions,
cache
);

m.dependencies = [];
m.evaluate(code);
Expand Down
1 change: 1 addition & 0 deletions packages/babel/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export { slugify } from '@linaria/utils';
export { default as preeval } from './plugins/preeval';
export { default as withLinariaMetadata } from './utils/withLinariaMetadata';
export { default as Module, DefaultModuleImplementation } from './module';
export type { IModule } from './module';
export { default as transform } from './transform';
export * from './types';
export { parseFile } from './transform-stages/helpers/parseFile';
Expand Down

0 comments on commit 9bb782d

Please sign in to comment.