Skip to content

Commit 968370e

Browse files
authored
fix: transpile js files from node_modules whenever Jest asks (#4791)
Also only show warning about `allowJs` for `isolatedModules: false` Closes #4637
1 parent ddfd812 commit 968370e

File tree

6 files changed

+178
-77
lines changed

6 files changed

+178
-77
lines changed

src/__helpers__/dedent-string.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export const omitLeadingWhitespace = (text: string): string => {
2+
return text.replace(/^\s+/gm, '')
3+
}
4+
5+
export const dedent = (strings: TemplateStringsArray, ...values: unknown[]) => {
6+
let joinedString = ''
7+
for (let i = 0; i < values.length; i++) {
8+
joinedString += `${strings[i]}${values[i]}`
9+
}
10+
joinedString += strings[strings.length - 1]
11+
12+
return omitLeadingWhitespace(joinedString)
13+
}

src/legacy/compiler/ts-compiler.spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { readFileSync } from 'fs'
22
import { basename, join, normalize } from 'path'
33

4+
import { LogLevels } from 'bs-logger'
45
import type { TsConfigJson } from 'type-fest'
56
import type { CompilerOptions, EmitOutput, transpileModule, TranspileOutput } from 'typescript'
67
import ts from 'typescript'
78

89
import { createConfigSet, makeCompiler } from '../../__helpers__/fakers'
10+
import { logTargetMock } from '../../__helpers__/mocks'
911
import type { RawCompilerOptions } from '../../raw-compiler-options'
1012
import { tsTranspileModule } from '../../transpilers/typescript/transpile-module'
1113
import type { DepGraphInfo } from '../../types'
@@ -38,6 +40,8 @@ const mockFolder = join(process.cwd(), 'src', '__mocks__')
3840

3941
const baseTsJestConfig = { tsconfig: join(process.cwd(), 'tsconfig.json') }
4042

43+
const logTarget = logTargetMock()
44+
4145
describe('TsCompiler', () => {
4246
describe('getResolvedModules', () => {
4347
const fileName = join(mockFolder, 'thing.ts')
@@ -248,6 +252,33 @@ describe('TsCompiler', () => {
248252
const jsOutput = 'var bar = 1'
249253
const sourceMap = '{}'
250254

255+
test('should return the original js content for js file with allowJs false and show warning log', () => {
256+
const configSet = createConfigSet({
257+
tsJestConfig: {
258+
...baseTsJestConfig,
259+
tsconfig: {
260+
allowJs: false,
261+
},
262+
},
263+
})
264+
const compiler = new TsCompiler(configSet, new Map())
265+
266+
expect(
267+
compiler.getCompiledOutput('const foo = 1', 'foo.js', {
268+
supportsStaticESM: false,
269+
depGraphs: new Map(),
270+
watchMode: false,
271+
}),
272+
).toEqual({
273+
code: 'const foo = 1',
274+
})
275+
expect(logTarget.filteredLines(LogLevels.warn)).toEqual(
276+
expect.arrayContaining([
277+
expect.stringContaining(interpolate(Errors.GotJsFileButAllowJsFalse, { path: 'foo.js' })),
278+
]),
279+
)
280+
})
281+
251282
test.each([
252283
{
253284
useESM: true,

src/legacy/compiler/ts-compiler.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import ts, {
2222
TranspileOutput,
2323
} from 'typescript'
2424

25-
import { LINE_FEED, TS_TSX_REGEX } from '../../constants'
25+
import { JS_JSX_REGEX, LINE_FEED, TS_TSX_REGEX } from '../../constants'
2626
import { isModernNodeModuleKind, tsTranspileModule } from '../../transpilers/typescript/transpile-module'
2727
import type {
2828
StringMap,
@@ -200,6 +200,14 @@ export class TsCompiler implements TsCompilerInstance {
200200
const moduleKind = this._initialCompilerOptions.module
201201
const currentModuleKind = this._compilerOptions.module
202202
if (this._languageService) {
203+
if (JS_JSX_REGEX.test(fileName) && !this._compilerOptions.allowJs) {
204+
this._logger.warn({ fileName: fileName }, interpolate(Errors.GotJsFileButAllowJsFalse, { path: fileName }))
205+
206+
return {
207+
code: fileContent,
208+
}
209+
}
210+
203211
this._logger.debug({ fileName }, 'getCompiledOutput(): compiling using language service')
204212

205213
// Must set memory cache before attempting to compile

src/legacy/ts-jest-transformer.spec.ts

Lines changed: 79 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ import path from 'path'
33

44
import { LogLevels } from 'bs-logger'
55
import { removeSync } from 'fs-extra'
6+
import ts from 'typescript'
67

8+
import { dedent, omitLeadingWhitespace } from '../__helpers__/dedent-string'
79
import { logTargetMock } from '../__helpers__/mocks'
10+
import type { TsJestTransformOptions } from '../types'
811
import { importer } from '../utils/importer'
912

1013
import { TsJestCompiler } from './compiler'
@@ -14,7 +17,7 @@ const SOURCE_MAPPING_PREFIX = 'sourceMappingURL='
1417

1518
const logTarget = logTargetMock()
1619
const cacheDir = path.join(process.cwd(), 'tmp')
17-
const baseTransformOptions = {
20+
const baseTransformOptions: TsJestTransformOptions = {
1821
config: {
1922
testMatch: [],
2023
testRegex: [],
@@ -298,34 +301,6 @@ describe('TsJestTransformer', () => {
298301
})
299302
})
300303

301-
test('should process js file with allowJs false and show warning log', () => {
302-
const fileContent = 'const foo = 1'
303-
const filePath = 'foo.js'
304-
const transformOptions = {
305-
...baseTransformOptions,
306-
config: {
307-
...baseTransformOptions.config,
308-
globals: {
309-
'ts-jest': { tsconfig: { allowJs: false } },
310-
},
311-
},
312-
}
313-
tr.getCacheKey(fileContent, filePath, transformOptions)
314-
logTarget.clear()
315-
316-
const result = tr.process(fileContent, filePath, transformOptions)
317-
318-
expect(result).toEqual({
319-
code: fileContent,
320-
})
321-
expect(logTarget.lines[1].substring(0)).toMatchInlineSnapshot(`
322-
"[level:40] Got a \`.js\` file to compile while \`allowJs\` option is not set to \`true\` (file: foo.js). To fix this:
323-
- if you want TypeScript to process JS files, set \`allowJs\` to \`true\` in your TypeScript config (usually tsconfig.json)
324-
- if you do not want TypeScript to process your \`.js\` files, in your Jest config change the \`transform\` key which value is \`ts-jest\` so that it does not match \`.js\` files anymore
325-
"
326-
`)
327-
})
328-
329304
test('should allow detection of ts-jest', () => {
330305
expect(process.env.TS_JEST).toBe('1')
331306
})
@@ -432,6 +407,81 @@ describe('TsJestTransformer', () => {
432407

433408
delete process.env.TS_JEST_HOOKS
434409
})
410+
411+
it.each([
412+
{
413+
filePath: 'my-project/node_modules/foo.js',
414+
fileContent: `
415+
function foo() {
416+
return 1
417+
}
418+
419+
export default foo;
420+
`,
421+
},
422+
{
423+
filePath: 'my-project/node_modules/foo.mjs',
424+
fileContent: `
425+
function foo() {
426+
return 1
427+
}
428+
429+
export default foo;
430+
`,
431+
},
432+
])('should transpile js file from node_modules for CJS', ({ filePath, fileContent }) => {
433+
const result = tr.process(fileContent, filePath, baseTransformOptions)
434+
435+
expect(omitLeadingWhitespace(result.code)).toContain(dedent`
436+
exports.default = foo;
437+
`)
438+
})
439+
440+
it('should transpile js file from node_modules for ESM', () => {
441+
const result = tr.process(
442+
`
443+
function foo() {
444+
return 1
445+
}
446+
447+
module.exports = foo;
448+
`,
449+
'my-project/node_modules/foo.js',
450+
{
451+
...baseTransformOptions,
452+
supportsStaticESM: true,
453+
transformerConfig: {
454+
useESM: true,
455+
tsconfig: {
456+
module: 'ESNext',
457+
target: 'ESNext',
458+
},
459+
},
460+
},
461+
)
462+
463+
const transpileResult = ts.transpileModule(
464+
`
465+
function foo() {
466+
return 1
467+
}
468+
469+
module.exports = foo;
470+
`,
471+
{
472+
compilerOptions: {
473+
module: ts.ModuleKind.ESNext, // Transpile to ESM
474+
target: ts.ScriptTarget.ESNext,
475+
},
476+
},
477+
)
478+
479+
console.log(transpileResult.outputText)
480+
481+
expect(omitLeadingWhitespace(result.code)).toContain(dedent`
482+
module.exports = foo;
483+
`)
484+
})
435485
})
436486

437487
describe('processAsync', () => {

src/legacy/ts-jest-transformer.ts

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path from 'path'
33

44
import type { SyncTransformer, TransformedSource } from '@jest/transform'
55
import type { Logger } from 'bs-logger'
6+
import ts from 'typescript'
67

78
import { DECLARATION_TYPE_EXT, JS_JSX_REGEX, TS_TSX_REGEX } from '../constants'
89
import type {
@@ -18,6 +19,7 @@ import { Deprecations, Errors, interpolate } from '../utils/messages'
1819
import { sha1 } from '../utils/sha1'
1920

2021
import { TsJestCompiler } from './compiler'
22+
import { updateOutput } from './compiler/compiler-utils'
2123
import { ConfigSet } from './config/config-set'
2224

2325
interface CachedConfigSet {
@@ -34,6 +36,10 @@ interface TsJestHooksMap {
3436
afterProcess?(args: any[], result: TransformedSource): TransformedSource
3537
}
3638

39+
const isNodeModule = (filePath: string) => {
40+
return path.normalize(filePath).split(path.sep).includes('node_modules')
41+
}
42+
3743
/**
3844
* @internal
3945
*/
@@ -146,9 +152,6 @@ export class TsJestTransformer implements SyncTransformer<TsJestTransformerOptio
146152
this._compiler = new TsJestCompiler(configSet, cacheFS)
147153
}
148154

149-
/**
150-
* @public
151-
*/
152155
process(sourceText: string, sourcePath: string, transformOptions: TsJestTransformOptions): TransformedSource {
153156
this._logger.debug({ fileName: sourcePath, transformOptions }, 'processing', sourcePath)
154157

@@ -226,20 +229,29 @@ export class TsJestTransformer implements SyncTransformer<TsJestTransformerOptio
226229
result = {
227230
code: '',
228231
}
229-
} else if (!configs.parsedTsConfig.options.allowJs && isJsFile) {
230-
// we've got a '.js' but the compiler option `allowJs` is not set or set to false
231-
this._logger.warn({ fileName: sourcePath }, interpolate(Errors.GotJsFileButAllowJsFalse, { path: sourcePath }))
232-
233-
result = {
234-
code: sourceText,
235-
}
236232
} else if (isJsFile || isTsFile) {
237-
// transpile TS code (source maps are included)
238-
result = this._compiler.getCompiledOutput(sourceText, sourcePath, {
239-
depGraphs: this._depGraphs,
240-
supportsStaticESM: transformOptions.supportsStaticESM,
241-
watchMode: this._watchMode,
242-
})
233+
if (isJsFile && isNodeModule(sourcePath)) {
234+
const transpiledResult = ts.transpileModule(sourceText, {
235+
compilerOptions: {
236+
...configs.parsedTsConfig.options,
237+
module:
238+
transformOptions.supportsStaticESM && transformOptions.transformerConfig.useESM
239+
? ts.ModuleKind.ESNext
240+
: ts.ModuleKind.CommonJS,
241+
},
242+
fileName: sourcePath,
243+
})
244+
result = {
245+
code: updateOutput(transpiledResult.outputText, sourcePath, transpiledResult.sourceMapText),
246+
}
247+
} else {
248+
// transpile TS code (source maps are included)
249+
result = this._compiler.getCompiledOutput(sourceText, sourcePath, {
250+
depGraphs: this._depGraphs,
251+
supportsStaticESM: transformOptions.supportsStaticESM,
252+
watchMode: this._watchMode,
253+
})
254+
}
243255
} else {
244256
// we should not get called for files with other extension than js[x], ts[x] and d.ts,
245257
// TypeScript will bail if we try to compile, and if it was to call babel, users can

src/transpilers/typescript/transpile-module.spec.ts

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { vol } from 'memfs'
44
import type { PackageJson } from 'type-fest'
55
import ts from 'typescript'
66

7+
import { dedent, omitLeadingWhitespace } from '../../__helpers__/dedent-string'
78
import { workspaceRoot } from '../../__helpers__/workspace-root'
89

910
import { tsTranspileModule } from './transpile-module'
@@ -37,20 +38,6 @@ jest.mock('node:fs', () => {
3738
}
3839
})
3940

40-
function dedent(strings: TemplateStringsArray, ...values: unknown[]) {
41-
let joinedString = ''
42-
for (let i = 0; i < values.length; i++) {
43-
joinedString += `${strings[i]}${values[i]}`
44-
}
45-
joinedString += strings[strings.length - 1]
46-
47-
return omitLeadingWhitespace(joinedString)
48-
}
49-
50-
function omitLeadingWhitespace(text: string): string {
51-
return text.replace(/^\s+/gm, '')
52-
}
53-
5441
describe('transpileModules', () => {
5542
describe('with modern Node resolution', () => {
5643
const tsFilePathInEsmModernNode = path.join(workspaceRoot, 'esm-node-modern', 'foo.ts')
@@ -300,13 +287,13 @@ describe('transpileModules', () => {
300287
})
301288

302289
expect(omitLeadingWhitespace(result.outputText)).toContain(dedent`
303-
const foo_1 = require("foo");
304-
console.log(foo_1.foo);
305-
const loadFooAsync = async () => {
306-
const fooDefault = await Promise.resolve().then(() => require('foo'));
307-
console.log(fooDefault);
308-
};
309-
console.log(loadFooAsync());
290+
const foo_1 = require("foo");
291+
console.log(foo_1.foo);
292+
const loadFooAsync = async () => {
293+
const fooDefault = await Promise.resolve().then(() => require('foo'));
294+
console.log(fooDefault);
295+
};
296+
console.log(loadFooAsync());
310297
`)
311298
})
312299

@@ -320,13 +307,13 @@ describe('transpileModules', () => {
320307
})
321308

322309
expect(omitLeadingWhitespace(result.outputText)).toContain(dedent`
323-
import { foo } from 'foo';
324-
console.log(foo);
325-
const loadFooAsync = async () => {
326-
const fooDefault = await import('foo');
327-
console.log(fooDefault);
328-
};
329-
console.log(loadFooAsync());
310+
import { foo } from 'foo';
311+
console.log(foo);
312+
const loadFooAsync = async () => {
313+
const fooDefault = await import('foo');
314+
console.log(fooDefault);
315+
};
316+
console.log(loadFooAsync());
330317
`)
331318
})
332319
})
@@ -338,10 +325,10 @@ describe('transpileModules', () => {
338325
vol.fromJSON(
339326
{
340327
'./foo.ts': `
341-
import { foo } from 'foo';
328+
import { foo } from 'foo';
342329
343-
console.log(foo);
344-
`,
330+
console.log(foo);
331+
`,
345332
},
346333
workspaceRoot,
347334
)

0 commit comments

Comments
 (0)