Skip to content

Commit

Permalink
fix(component-testing): ability to import esm modules
Browse files Browse the repository at this point in the history
  • Loading branch information
DudaGod authored and KuznetsovRoman committed Nov 13, 2024
1 parent c368b1b commit 049ea92
Show file tree
Hide file tree
Showing 2 changed files with 195 additions and 3 deletions.
114 changes: 111 additions & 3 deletions src/bundle/test-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { TRANSFORM_EXTENSIONS, JS_EXTENSION_RE } from "./constants";
import { requireModuleSync } from "../utils/module";

import type { NodePath, PluginObj, TransformOptions } from "@babel/core";
import type { ImportDeclaration } from "@babel/types";

const STYLE_EXTESTION_RE = /\.(css|less|scss|sass|styl|stylus|pcss)$/;
const IGNORE_STYLE_ERRORS = ["Unexpected token"];
Expand All @@ -30,10 +29,10 @@ export const setupTransformHook = (opts: { removeNonJsImports?: boolean } = {}):
],
};

const customIgnoreImportsPlugin = (): PluginObj => ({
const customIgnoreImportsPlugin = ({ types: t }: { types: typeof babel.types }): PluginObj => ({
name: "ignore-imports",
visitor: {
ImportDeclaration(path: NodePath<ImportDeclaration>): void {
ImportDeclaration(path: NodePath<babel.types.ImportDeclaration>): void {
const extname = nodePath.extname(path.node.source.value);

if (extname && !extname.match(JS_EXTENSION_RE)) {
Expand All @@ -46,6 +45,12 @@ export const setupTransformHook = (opts: { removeNonJsImports?: boolean } = {}):
} catch (err) {
if (shouldIgnoreImportError(err as Error)) {
path.remove();
return;
}

if ((err as NodeJS.ErrnoException).code === "ERR_REQUIRE_ESM") {
mockEsmModuleImport(t, path);
return;
}
}
},
Expand Down Expand Up @@ -81,3 +86,106 @@ function shouldIgnoreImportError(err: Error): boolean {

return isStyleFilePath;
}

/**
* Replace esm module import with a Proxy.
* Examples:
* 1) `import pkg from "package"` -> `const pkg = new Proxy({}, {get: ..., apply: ...})`
* 2) `import {a, b as c} from "package"` -> `const {a, c} = new Proxy({}, {get: ..., apply: ...})`
*/
function mockEsmModuleImport(t: typeof babel.types, path: NodePath<babel.types.ImportDeclaration>): void {
const variableKey = genVarDeclKey(t, path.node);
const variableValue = genProxy(t, [
t.objectExpression([]),
t.objectExpression([genProxyGetHandler(t), genProxyApplyHandler(t)]),
]);

const variableDecl = t.variableDeclaration("const", [t.variableDeclarator(variableKey, variableValue)]);

path.replaceWith(variableDecl);
}

/**
* Generates the name of variables from the import declaration.
* Examples:
* 1) `import pkg from "package"` -> `pkg`
* 2) `import {a, b as c} from "package"` -> `const {a, с} `
*/
function genVarDeclKey(
t: typeof babel.types,
node: NodePath<babel.types.ImportDeclaration>["node"],
): babel.types.Identifier | babel.types.ObjectPattern {
if (node.specifiers.length === 1) {
if (["ImportDefaultSpecifier", "ImportNamespaceSpecifier"].includes(node.specifiers[0].type)) {
return t.identifier(node.specifiers[0].local.name);
}

return t.objectPattern([
t.objectProperty(
t.identifier(node.specifiers[0].local.name),
t.identifier(node.specifiers[0].local.name),
false,
true,
),
]);
}

const objectProperties = node.specifiers.map(spec => {
return t.objectProperty(t.identifier(spec.local.name), t.identifier(spec.local.name), false, true);
});

return t.objectPattern(objectProperties);
}

// Generates Proxy expression with passed arguments: `new Proxy(args)`
function genProxy(t: typeof babel.types, args: babel.types.Expression[]): babel.types.NewExpression {
return t.newExpression(t.identifier("Proxy"), args);
}

/**
* Generates "get" handler for Proxy:
*
* get: function (target, prop) {
* return prop in target ? target[prop] : new Proxy(() => {}, this);
* }
*/
function genProxyGetHandler(t: typeof babel.types): babel.types.ObjectProperty {
return t.objectProperty(
t.identifier("get"),
t.functionExpression(
null,
[t.identifier("target"), t.identifier("prop")],
t.blockStatement([
t.returnStatement(
t.conditionalExpression(
t.binaryExpression("in", t.identifier("prop"), t.identifier("target")),
t.memberExpression(t.identifier("target"), t.identifier("prop"), true),
genProxy(t, [t.arrowFunctionExpression([], t.blockStatement([])), t.thisExpression()]),
),
),
]),
),
);
}

/**
* Generates "apply" handler for Proxy:
*
* apply: function () {
* return new Proxy(() => {}, this);
* }
*/
function genProxyApplyHandler(t: typeof babel.types): babel.types.ObjectProperty {
return t.objectProperty(
t.identifier("apply"),
t.functionExpression(
null,
[],
t.blockStatement([
t.returnStatement(
genProxy(t, [t.arrowFunctionExpression([], t.blockStatement([])), t.thisExpression()]),
),
]),
),
);
}
84 changes: 84 additions & 0 deletions test/src/test-reader/test-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,90 @@ describe("test-transformer", () => {

assert.match(transformedCode, new RegExp(`require\\("${moduleName}"\\)`));
});

describe("replace import of esm module with a proxy", () => {
const moduleName = "esm-module";
const error = { message: "require() of ES Module", code: "ERR_REQUIRE_ESM" };
const expectedProxyValue = [
`new Proxy({}, {`,
` get: function (target, prop) {`,
` return prop in target ? target[prop] : new Proxy(() => {}, this);`,
` },`,
` apply: function () {`,
` return new Proxy(() => {}, this);`,
` }`,
`});`,
].join("\n");

let setupTransformHookStub!: typeof setupTransformHook;

beforeEach(() => {
const { setupTransformHook } = proxyquire("../../../src/test-reader/test-transformer", {
"../bundle": proxyquire.noCallThru().load("../../../src/bundle/test-transformer", {
"../utils/module": {
requireModuleSync: sandbox.stub().withArgs(moduleName).throws(error),
},
}),
});
setupTransformHookStub = setupTransformHook;
});

it("should replace with default import", async () => {
let transformedCode;
(pirates.addHook as SinonStub).callsFake(cb => {
transformedCode = cb(`import pkg from "${moduleName}"`, moduleName);
});

setupTransformHookStub({ removeNonJsImports: true });

assert.match(transformedCode, `const pkg = ${expectedProxyValue}`);
});

it("should replace with namespace import", async () => {
let transformedCode;
(pirates.addHook as SinonStub).callsFake(cb => {
transformedCode = cb(`import * as pkg from "${moduleName}"`, moduleName);
});

setupTransformHookStub({ removeNonJsImports: true });

assert.match(transformedCode, `const pkg = ${expectedProxyValue}`);
});

it("should replace with property import", async () => {
let transformedCode;
(pirates.addHook as SinonStub).callsFake(cb => {
transformedCode = cb(`import {a, b as c} from "${moduleName}"`, moduleName);
});

setupTransformHookStub({ removeNonJsImports: true });

assert.match(transformedCode, `` + `const {\n` + ` a,\n` + ` c\n` + `} = ${expectedProxyValue}`);
});
});

it("should not replace import of esm module with a proxy if it doesn't fail with 'ERR_REQUIRE_ESM' code", () => {
const moduleName = "esm-module";
const error = { message: "Some error" };

const { setupTransformHook } = proxyquire("../../../src/test-reader/test-transformer", {
"../bundle": proxyquire.noCallThru().load("../../../src/bundle/test-transformer", {
"../utils/module": {
requireModuleSync: sandbox.stub().withArgs(moduleName).throws(error),
},
}),
});

let transformedCode;

(pirates.addHook as SinonStub).callsFake(cb => {
transformedCode = cb(`import "${moduleName}"`, moduleName);
});

setupTransformHook({ removeNonJsImports: true });

assert.match(transformedCode, new RegExp(`require\\("${moduleName}"\\)`));
});
});
});
});

0 comments on commit 049ea92

Please sign in to comment.