Skip to content

Commit

Permalink
fix(babel): got rid of multiple EvalError (#1028)
Browse files Browse the repository at this point in the history
* fix(babel): imported value is undefined/not a function (fixes #1022)

* fix(babel): circuit breaker for cyclic dependencies (fixes #1022)

* fix(babel): better detector for transpiled JSX (fixes #1022)

* fix(babel): change default rules to process ES-modules (fixes #1022)
  • Loading branch information
Anber committed Jul 27, 2022
1 parent 2abc55b commit 21ba7a4
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/brown-yaks-stare.md
@@ -0,0 +1,5 @@
---
'@linaria/babel-preset': patch
---

Circuit breaker for cyclic dependencies.
6 changes: 6 additions & 0 deletions .changeset/few-jokes-flash.md
@@ -0,0 +1,6 @@
---
'@linaria/babel-preset': patch
'@linaria/testkit': patch
---

The default config was changed to process ES modules inside node_modules.
5 changes: 5 additions & 0 deletions .changeset/healthy-fans-live.md
@@ -0,0 +1,5 @@
---
'@linaria/babel-preset': patch
---

In some cases, Linaria threw an error that the imported value is undefined.
6 changes: 6 additions & 0 deletions .changeset/young-hats-fly.md
@@ -0,0 +1,6 @@
---
'@linaria/babel-preset': patch
'@linaria/testkit': patch
---

The better detector of React components.
16 changes: 14 additions & 2 deletions packages/babel/src/module.ts
Expand Up @@ -91,6 +91,8 @@ class Module {

#isEvaluated = false;

#evaluatedFragments = new Set<string>();

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

// #exportsProxy: Record<string, unknown>;
Expand Down Expand Up @@ -430,8 +432,20 @@ class Module {
});

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}`);

this.#evaluatedFragments.add(source);

this.#isEvaluated = true;

try {
const script = new vm.Script(
`(function (exports) { ${source}\n})(exports);`,
Expand Down Expand Up @@ -460,8 +474,6 @@ class Module {
);
}
});

this.#isEvaluated = true;
}
}

Expand Down
57 changes: 52 additions & 5 deletions packages/babel/src/plugins/preeval.ts
Expand Up @@ -3,10 +3,10 @@
* It works the same as main `babel/extract` preset, but do not evaluate lazy dependencies.
*/
import type { BabelFile, NodePath, PluginObj } from '@babel/core';
import type { Identifier } from '@babel/types';
import type { CallExpression, Identifier } from '@babel/types';

import { createCustomDebug } from '@linaria/logger';
import type { StrictOptions } from '@linaria/utils';
import type { StrictOptions, IImport } from '@linaria/utils';
import {
collectExportsAndImports,
getFileIdx,
Expand Down Expand Up @@ -50,6 +50,42 @@ const isBrowserGlobal = (id: NodePath<Identifier>) => {
return forbiddenGlobals.has(id.node.name) && isGlobal(id);
};

function isCreateElement(
p: NodePath<CallExpression>,
reactImports: IImport[]
): boolean {
if (reactImports.length === 0) return false;
const callee = p.get('callee');
if (callee.isIdentifier({ name: 'createElement' })) {
const bindingPath = callee.scope.getBinding('createElement')?.path;
return reactImports.some((i) => bindingPath?.isAncestor(i.local));
}

if (callee.isMemberExpression()) {
if (reactImports.some((i) => i.local === callee)) {
// It's React.createElement in CJS
return true;
}

const object = callee.get('object');
const property = callee.get('property');
const defaultImport = reactImports.find((i) => i.imported === 'default');
if (
!defaultImport ||
!defaultImport.local.isIdentifier() ||
!property.isIdentifier({ name: 'createElement' }) ||
!object.isIdentifier({ name: defaultImport.local.node.name })
) {
return false;
}

const bindingPath = object.scope.getBinding(object.node.name)?.path;
return bindingPath?.isAncestor(defaultImport.local) ?? false;
}

return false;
}

export default function preeval(
babel: Core,
options: PreevalOptions
Expand All @@ -67,6 +103,12 @@ export default function preeval(
file.opts.filename
);

const reactImports = imports.filter(
(i) =>
i.source === 'react' &&
(i.imported === 'default' || i.imported === 'createElement')
) as IImport[];

const jsxRuntime = imports.find((i) => i.source === 'react/jsx-runtime');
const jsxRuntimeName =
jsxRuntime?.local?.isIdentifier() && jsxRuntime?.local?.node?.name;
Expand All @@ -88,9 +130,14 @@ export default function preeval(
// but we have to do it after we processed template tags.
CallExpression: {
enter(p) {
if (!jsxRuntimeName) return;
const callee = p.get('callee');
if (callee.isIdentifier({ name: jsxRuntimeName })) {
if (jsxRuntimeName) {
const callee = p.get('callee');
if (callee.isIdentifier({ name: jsxRuntimeName })) {
JSXElementsRemover(p);
}
}

if (isCreateElement(p, reactImports)) {
JSXElementsRemover(p);
}
},
Expand Down
14 changes: 12 additions & 2 deletions packages/babel/src/transform-stages/1-prepare-for-eval.ts
Expand Up @@ -335,9 +335,19 @@ export default async function prepareForEval(
const promise = resolve(importedFile, name, stack).then(
(resolved) => {
log('stage-1:async-resolve', `✅ ${importedFile} -> ${resolved}`);
const resolveCacheKey = `${name} -> ${importedFile}`;
const cached = resolveCache.get(resolveCacheKey);
const importsOnlySet = new Set(importsOnly);
if (cached) {
const [, cachedOnly] = cached.split('\0');
cachedOnly?.split(',').forEach((token) => {
importsOnlySet.add(token);
});
}

resolveCache.set(
`${name} -> ${importedFile}`,
`${resolved}\0${importsOnly.join(',')}`
resolveCacheKey,
`${resolved}\0${[...importsOnlySet].join(',')}`
);
const fileContent = readFileSync(resolved, 'utf8');
return {
Expand Down
11 changes: 11 additions & 0 deletions packages/babel/src/transform-stages/helpers/loadLinariaOptions.ts
Expand Up @@ -40,6 +40,17 @@ export default function loadLinariaOptions(
test: ignore ?? /[\\/]node_modules[\\/]/,
action: 'ignore',
},
{
// Do not ignore ES-modules
test: (filename, code) => {
if (!/\/node_modules\//.test(filename)) {
return false;
}

return /(?:^|\n|;)\s*(?:export|import)\s+/.test(code);
},
action: require.resolve('@linaria/shaker'),
},
],
babelOptions,
...(result ? result.config : null),
Expand Down
68 changes: 68 additions & 0 deletions packages/testkit/src/__snapshots__/babel.test.ts.snap
Expand Up @@ -2046,6 +2046,74 @@ Dependencies: NA
`;
exports[`strategy shaker simplifies transpiled react components 1`] = `
"import * as ReactNS from 'react';
import React from 'react';
import { createElement } from 'react';
import { styled } from '@linaria/react';
import constant from './broken-dependency';
const A = () => ReactNS.createElement('div', {}, constant);
const B = () => createElement(A, {}, constant);
const C = () => React.createElement(FuncComponent, {}, constant);
const _exp = /*#__PURE__*/() => C;
export const D = /*#__PURE__*/styled(_exp())({
name: \\"D\\",
class: \\"D_dwgemqq\\"
});"
`;
exports[`strategy shaker simplifies transpiled react components 2`] = `
CSS:
.D_dwgemqq {
color: red;
}
Dependencies: NA
`;
exports[`strategy shaker simplifies transpiled react components CJS 1`] = `
"var _interopRequireWildcard = require(\\"@babel/runtime/helpers/interopRequireWildcard\\").default;
var React = _interopRequireWildcard(require(\\"react\\"));
var styled = require('@linaria/react').styled;
var constant = require('./broken-dependency').default;
const A = () => React.createElement('div', {}, constant);
const B = () => React.createElement(A, {}, constant);
const C = () => React.createElement(FuncComponent, {}, constant);
const _exp = /*#__PURE__*/() => C;
exports.D = /*#__PURE__*/styled(_exp())({
name: \\"source0\\",
class: \\"source0_swgemqq\\"
});"
`;
exports[`strategy shaker simplifies transpiled react components CJS 2`] = `
CSS:
.source0_swgemqq {
color: red;
}
Dependencies: NA
`;
exports[`strategy shaker supports both css and styled tags 1`] = `
"import { styled } from '@linaria/react';
export const Title = /*#__PURE__*/styled('h1')({
Expand Down
51 changes: 51 additions & 0 deletions packages/testkit/src/babel.test.ts
Expand Up @@ -1704,6 +1704,57 @@ describe('strategy shaker', () => {
expect(metadata).toMatchSnapshot();
});

it('simplifies transpiled react components', async () => {
const { code, metadata } = await transform(
dedent`
import * as ReactNS from 'react';
import React from 'react';
import { createElement } from 'react';
import { styled } from '@linaria/react';
import constant from './broken-dependency';
const A = () => ReactNS.createElement('div', {}, constant);
const B = () => createElement(A, {}, constant);
const C = () => React.createElement(FuncComponent, {}, constant);
export const D = styled(C)\`
color: red;
\`;
`,
[evaluator, {}, 'jsx']
);

expect(code).toMatchSnapshot();
expect(metadata).toMatchSnapshot();
});

it('simplifies transpiled react components CJS', async () => {
const { code, metadata } = await transform(
dedent`
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
var React = _interopRequireWildcard(require("react"));
var styled = require('@linaria/react').styled;
var constant = require('./broken-dependency').default;
const A = () => React.createElement('div', {}, constant);
const B = () => React.createElement(A, {}, constant);
const C = () => React.createElement(FuncComponent, {}, constant);
exports.D = styled(C)\`
color: red;
\`;
`,
[evaluator, {}, 'jsx']
);

expect(code).toMatchSnapshot();
expect(metadata).toMatchSnapshot();
});

it('ignores inline vanilla function expressions', async () => {
const { code, metadata } = await transform(
dedent`
Expand Down

0 comments on commit 21ba7a4

Please sign in to comment.