Skip to content

Commit

Permalink
fix(babel): priority queue for module evaluator (fixes #1193) (#1194)
Browse files Browse the repository at this point in the history
* fix(babel): priority queue for module evaluator (fixes #1193)

* fix(babel): an error in ordered queue implementation

* fix(babel): unprepared files during concurrent builds

* fix(rollup, vite): fix for 'Unexpected early exit'

* chore(babel): cleanup
  • Loading branch information
Anber committed Feb 2, 2023
1 parent 1d4d683 commit af78327
Show file tree
Hide file tree
Showing 15 changed files with 553 additions and 314 deletions.
5 changes: 5 additions & 0 deletions .changeset/gentle-oranges-teach.md
@@ -0,0 +1,5 @@
---
'@linaria/babel-preset': minor
---

Fix circular dependencies-related errors and freezes (fixes #1193)
2 changes: 1 addition & 1 deletion examples/rollup/package.json
Expand Up @@ -14,7 +14,7 @@
"@rollup/plugin-commonjs": "^22.0.1",
"@rollup/plugin-image": "^2.1.1",
"@rollup/plugin-node-resolve": "^13.3.0",
"rollup": "^2.76.0",
"rollup": "^3.11.0",
"rollup-plugin-css-only": "^3.1.0"
},
"scripts": {
Expand Down
File renamed without changes.
6 changes: 5 additions & 1 deletion packages/babel/src/cache.ts
Expand Up @@ -6,7 +6,11 @@ export class TransformCacheCollection {
public readonly resolveCache: Map<string, string> = new Map(),
public readonly codeCache: Map<
string,
Map<string, ITransformFileResult>
{
imports: Map<string, string[]> | null;
only: string[];
result: ITransformFileResult;
}
> = new Map(),
public readonly evalCache: Map<string, Module> = new Map()
) {}
Expand Down
2 changes: 1 addition & 1 deletion packages/babel/src/evaluators/index.ts
Expand Up @@ -9,7 +9,7 @@ import type { Options } from '../types';

export default function evaluate(
cache: TransformCacheCollection,
code: string[],
code: string,
options: Pick<Options, 'filename' | 'pluginOptions'>
) {
const filename = options?.filename ?? 'unknown';
Expand Down
163 changes: 81 additions & 82 deletions packages/babel/src/module.ts
Expand Up @@ -92,8 +92,6 @@ class Module {

#isEvaluated = false;

#evaluatedFragments = new Set<string>();

#exports: Record<string, unknown> | unknown;

// #exportsProxy: Record<string, unknown>;
Expand Down Expand Up @@ -122,11 +120,18 @@ class Module {

debug: CustomDebug;

resolveCache: Map<string, string>;
readonly #resolveCache: Map<string, string>;

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

evalCache: Map<string, Module>;
readonly #evalCache: Map<string, Module>;

constructor(
filename: string,
Expand All @@ -145,9 +150,9 @@ class Module {
this.transform = null;
this.debug = createCustomDebug('module', this.idx);

this.resolveCache = cache.resolveCache;
this.codeCache = cache.codeCache;
this.evalCache = cache.evalCache;
this.#resolveCache = cache.resolveCache;
this.#codeCache = cache.codeCache;
this.#evalCache = cache.evalCache;

Object.defineProperties(this, {
id: {
Expand Down Expand Up @@ -274,8 +279,8 @@ class Module {

resolve = (id: string) => {
const resolveCacheKey = `${this.filename} -> ${id}`;
if (this.resolveCache.has(resolveCacheKey)) {
return this.resolveCache.get(resolveCacheKey)!;
if (this.#resolveCache.has(resolveCacheKey)) {
return this.#resolveCache.get(resolveCacheKey)!;
}

const extensions = (
Expand Down Expand Up @@ -307,8 +312,7 @@ class Module {
};

require: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(id: string): any;
(id: string): unknown;
resolve: (id: string) => string;
ensure: () => void;
} = Object.assign(
Expand Down Expand Up @@ -341,23 +345,23 @@ class Module {

this.debug('require', `${id} -> ${filename}`);

if (this.evalCache.has(filename)) {
m = this.evalCache.get(filename)!;
if (this.#evalCache.has(filename)) {
m = this.#evalCache.get(filename)!;
this.debug('eval-cache', '✅ %r has been gotten from cache', {
namespace: `module:${padStart(m.idx, 5)}`,
});
} else {
this.debug('eval-cache', ` %r is going to be initialized`, {
this.debug('eval-cache', ` %r is going to be initialized`, {
namespace: `module:${padStart(getFileIdx(filename), 5)}`,
});
// Create the module if cached module is not available
m = new Module(
filename,
this.options,
{
codeCache: this.codeCache,
evalCache: this.evalCache,
resolveCache: this.resolveCache,
codeCache: this.#codeCache,
evalCache: this.#evalCache,
resolveCache: this.#resolveCache,
},
this.debuggerDepth + 1,
this
Expand All @@ -366,51 +370,54 @@ class Module {

// Store it in cache at this point with, otherwise
// we would end up in infinite loop with cyclic dependencies
this.evalCache.set(filename, m);
this.#evalCache.set(filename, m);
}

const extension = path.extname(filename);
if (extension === '.json' || this.extensions.includes(extension)) {
let code: string[] | undefined;
let code: string | undefined;
// Requested file can be already prepared for evaluation on the stage 1
if (this.codeCache.has(filename)) {
const perExportCache = this.codeCache.get(filename)!;
if (this.#codeCache.has(filename)) {
const cached = this.#codeCache.get(filename);
const only = onlyList
?.split(',')
.filter((token) => !m.#lazyValues.has(token));
const codeSet = new Set<string>();
if (only && only.every((o) => perExportCache.has(o))) {
const cachedOnly = new Set(cached?.only ?? []);
const isMatched =
cachedOnly.has('*') ||
(only && only.every((token) => cachedOnly.has(token)));
if (cached && isMatched) {
m.debug('code-cache', '✅');
only.forEach((o) => codeSet.add(perExportCache.get(o)!.code));
} else if (!only && perExportCache.has('*')) {
m.debug('code-cache', '✳️');
// The whole file is required
codeSet.add(perExportCache.get('*')!.code);
code = cached.result.code;
} else {
m.debug(
'code-cache',
'%o is missing (%o were cached)',
only?.filter((token) => !cachedOnly.has(token)) ?? [],
[...cachedOnly.values()]
);
}

code = Array.from(codeSet);
} else if (m.#isEvaluated) {
m.debug(
'code-cache',
'✅ not in the code cache, but is already evaluated'
);
code = [];
}

if (!code) {
} else {
// If code wasn't extracted from cache, read it from the file system
// TODO: transpile the file
m.debug('code-cache', '❌');
code = [fs.readFileSync(filename, 'utf-8')];
code = fs.readFileSync(filename, 'utf-8');
}

if (/\.json$/.test(filename) && code.length === 1) {
// For JSON files, parse it to a JS object similar to Node
m.exports = JSON.parse(code[0]);
m.#isEvaluated = true;
} else {
// For JS/TS files, evaluate the module
m.evaluate(code);
if (code) {
if (/\.json$/.test(filename)) {
// For JSON files, parse it to a JS object similar to Node
m.exports = JSON.parse(code);
m.#isEvaluated = true;
} else {
// For JS/TS files, evaluate the module
m.evaluate(code);
}
}
} else {
// For non JS/JSON requires, just export the id
Expand All @@ -428,11 +435,10 @@ class Module {
}
);

evaluate(arg: string | string[]): void {
const code = Array.isArray(arg) ? arg : [arg];
evaluate(source: string): void {
const { filename } = this;

if (code.length === 0) {
if (!source) {
this.debug(`evaluate`, 'there is nothing to evaluate');
}

Expand All @@ -452,49 +458,42 @@ class Module {
__dirname: path.dirname(filename),
});

code.forEach((source, idx) => {
if (this.#evaluatedFragments.has(source)) {
this.debug(
`evaluate:fragment-${padStart(idx + 1, 2)}`,
`is already evaluated`
);
return;
}

this.debug(`evaluate:fragment-${padStart(idx + 1, 2)}`, `\n${source}`);
if (this.#isEvaluated) {
this.debug('evaluate', `is already evaluated`);
return;
}

this.#evaluatedFragments.add(source);
this.debug('evaluate', `\n${source}`);

this.#isEvaluated = true;
this.#isEvaluated = true;

try {
const script = new vm.Script(
`(function (exports) { ${source}\n})(exports);`,
{
filename,
}
);

script.runInContext(context);
return;
} catch (e) {
if (e instanceof EvalError) {
throw e;
try {
const script = new vm.Script(
`(function (exports) { ${source}\n})(exports);`,
{
filename,
}
);

const callstack: string[] = ['', this.filename];
let module = this.parentModule;
while (module) {
callstack.push(module.filename);
module = module.parentModule;
}
script.runInContext(context);
return;
} catch (e) {
if (e instanceof EvalError) {
throw e;
}

this.debug('evaluate:error', '%O\n%O', e, callstack);
throw new EvalError(
`${(e as Error).message} in${callstack.join('\n| ')}\n`
);
const callstack: string[] = ['', this.filename];
let module = this.parentModule;
while (module) {
callstack.push(module.filename);
module = module.parentModule;
}
});

this.debug('evaluate:error', '%O\n%O', e, callstack);
throw new EvalError(
`${(e as Error).message} in${callstack.join('\n| ')}\n`
);
}
}
}

Expand Down
22 changes: 9 additions & 13 deletions packages/babel/src/plugins/babel-transform.ts
Expand Up @@ -24,38 +24,34 @@ export default function collector(
pre(file: BabelFile) {
debug('babel-transform:start', file.opts.filename);

const entryPoint = {
const entrypoint = {
name: file.opts.filename!,
code: file.code,
only: ['__linariaPreval'],
};

const prepareStageResults = prepareForEvalSync(
const prepareStageResult = prepareForEvalSync(
babel,
cache,
syncResolve,
entryPoint,
entrypoint,
{
root: file.opts.root ?? undefined,
pluginOptions: options,
}
);

if (
!prepareStageResults ||
prepareStageResults.every((r) => !withLinariaMetadata(r.metadata))
!prepareStageResult ||
!withLinariaMetadata(prepareStageResult?.metadata)
) {
return;
}

const evalStageResult = evalStage(
cache,
prepareStageResults.map((r) => r.code),
{
filename: file.opts.filename!,
pluginOptions: options,
}
);
const evalStageResult = evalStage(cache, prepareStageResult.code, {
filename: file.opts.filename!,
pluginOptions: options,
});

if (evalStageResult === null) {
return;
Expand Down

0 comments on commit af78327

Please sign in to comment.