diff --git a/src/bundle/test-transformer.ts b/src/bundle/test-transformer.ts index b134b26f5..69c66812d 100644 --- a/src/bundle/test-transformer.ts +++ b/src/bundle/test-transformer.ts @@ -2,10 +2,14 @@ import * as nodePath from "node:path"; import * as babel from "@babel/core"; import { addHook } from "pirates"; 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"]; + export const setupTransformHook = (opts: { removeNonJsImports?: boolean } = {}): VoidFunction => { const transformOptions: TransformOptions = { browserslistConfigFile: false, @@ -34,6 +38,15 @@ export const setupTransformHook = (opts: { removeNonJsImports?: boolean } = {}): if (extname && !extname.match(JS_EXTENSION_RE)) { path.remove(); + return; + } + + try { + requireModuleSync(path.node.source.value); + } catch (err) { + if (shouldIgnoreImportError(err as Error)) { + path.remove(); + } } }, }, @@ -52,3 +65,19 @@ export const setupTransformHook = (opts: { removeNonJsImports?: boolean } = {}): return revertTransformHook; }; + +function shouldIgnoreImportError(err: Error): boolean { + const shouldIgnoreImport = IGNORE_STYLE_ERRORS.some(ignoreImportErr => { + return (err as Error).message.startsWith(ignoreImportErr); + }); + + if (!shouldIgnoreImport) { + return false; + } + + const firstStackFrame = (err as Error).stack?.split("\n")[0] || ""; + const filePath = firstStackFrame.split(":")[0]; + const isStyleFilePath = STYLE_EXTESTION_RE.test(filePath); + + return isStyleFilePath; +} diff --git a/src/utils/module.ts b/src/utils/module.ts index a32ac837b..e5f47d38f 100644 --- a/src/utils/module.ts +++ b/src/utils/module.ts @@ -6,3 +6,7 @@ export const requireModule = async (modulePath: string): Promise return require(isModuleLocal ? path.resolve(modulePath) : modulePath); }; + +export const requireModuleSync = (modulePath: string): unknown => { + return require(modulePath); +}; diff --git a/test/src/test-reader/test-transformer.ts b/test/src/test-reader/test-transformer.ts index ed39e0bd1..338c13055 100644 --- a/test/src/test-reader/test-transformer.ts +++ b/test/src/test-reader/test-transformer.ts @@ -1,5 +1,6 @@ import * as pirates from "pirates"; import sinon, { SinonStub } from "sinon"; +import proxyquire from "proxyquire"; import { setupTransformHook, TRANSFORM_EXTENSIONS } from "../../../src/test-reader/test-transformer"; describe("test-transformer", () => { @@ -59,29 +60,81 @@ describe("test-transformer", () => { }); }); - [true, false].forEach(removeNonJsImports => { - describe(`should ${removeNonJsImports ? "" : "not "}remove non-js imports`, () => { - [".css", ".less", ".scss", ".jpg", ".png", ".woff"].forEach(extName => { - it(`asset with extension: "${extName}"`, () => { + describe("'removeNonJsImports' option", () => { + [true, false].forEach(removeNonJsImports => { + describe(`should ${removeNonJsImports ? "" : "not "}remove non-js imports`, () => { + [".css", ".less", ".scss", ".jpg", ".png", ".woff"].forEach(extName => { + it(`asset with extension: "${extName}"`, () => { + let transformedCode; + const fileName = `some${extName}`; + (pirates.addHook as SinonStub).callsFake(cb => { + transformedCode = cb(`import "${fileName}"`, fileName); + }); + + setupTransformHook({ removeNonJsImports }); + + const expectedCode = ['"use strict";']; + + if (!removeNonJsImports) { + expectedCode.push("", `require("some${extName}");`); + } + + expectedCode.push("//# sourceMappingURL="); + + assert.match(transformedCode, expectedCode.join("\n")); + }); + }); + }); + }); + + describe("should remove third party import with error from", () => { + [".css", ".less", ".scss", ".sass", ".styl", ".stylus", ".pcss"].forEach(extName => { + it(`${extName} style file`, () => { + const moduleName = "some-module"; + const error = { message: "Unexpected token {", stack: `foo${extName}:100500\nbar\nqux` }; + + 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; - const fileName = `some${extName}`; + (pirates.addHook as SinonStub).callsFake(cb => { - transformedCode = cb(`import "${fileName}"`, fileName); + transformedCode = cb(`import "${moduleName}"`, moduleName); }); - setupTransformHook({ removeNonJsImports }); + setupTransformHook({ removeNonJsImports: true }); - const expectedCode = ['"use strict";']; + assert.notMatch(transformedCode, new RegExp(`require\\("${moduleName}"\\)`)); + }); + }); + }); - if (!removeNonJsImports) { - expectedCode.push("", `require("some${extName}");`); - } + it("should not remove third party import with error not from style file", () => { + const moduleName = "some-module"; + const error = { message: "Some error", stack: `foo.js:100500\nbar\nqux` }; + + 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), + }, + }), + }); - expectedCode.push("//# sourceMappingURL="); + let transformedCode; - assert.match(transformedCode, expectedCode.join("\n")); - }); + (pirates.addHook as SinonStub).callsFake(cb => { + transformedCode = cb(`import "${moduleName}"`, moduleName); }); + + setupTransformHook({ removeNonJsImports: true }); + + assert.match(transformedCode, new RegExp(`require\\("${moduleName}"\\)`)); }); }); });