Skip to content

Commit

Permalink
fix: parse extended tsconfigs when transpiling script blocks (#502)
Browse files Browse the repository at this point in the history
* fix(tsconfig): parse extended tsconfigs when transpiling script blocks

A change introduced in v28.1.0 in PR #471 unintentionally changed the behavior of the tsconfig parsing such that configs using "extends" were no longer being considered.

Fixes: #495

* chore(cache): cache tsconfig parsing to avoid the cost per vue file / interpolated string

Co-authored-by: Adam Hines <ahines@factset.com>
  • Loading branch information
thebanjomatic and Adam Hines committed Nov 3, 2022
1 parent 62d6ebc commit bb516da
Show file tree
Hide file tree
Showing 16 changed files with 220 additions and 61 deletions.
34 changes: 34 additions & 0 deletions e2e/2.x/basic/components/ExtendedTsConfig.vue
@@ -0,0 +1,34 @@
<template>
<div>
{{ exclamationMarks }}
<type-script-child />
</div>
</template>

<script lang="ts">
import TypeScriptChild from './TypeScriptChild.vue'
import moduleRequiringEsModuleInterop from './ModuleRequiringEsModuleInterop'
// The default import above relies on esModuleInterop being set to true in order to use it from
// an import statement instead of require. This option is configured in the tsconfig.base.json,
// so if we are no longer fully processing the tsconfig options (extended from a base config)
// this test should fail. This was one of the only reliable ways I could get a test to fail if
// these conditions are not being met and happen to be the use-case which was triggering errors
// in my config setup.
if (moduleRequiringEsModuleInterop()) {
throw new Error('Should never hit this')
}
export default {
computed: {
exclamationMarks(): string {
return 'string'
}
},
components: {
TypeScriptChild
}
}
</script>
1 change: 1 addition & 0 deletions e2e/2.x/basic/components/ModuleRequiringEsModuleInterop.js
@@ -0,0 +1 @@
module.exports = () => false
7 changes: 7 additions & 0 deletions e2e/2.x/basic/test.js
Expand Up @@ -21,6 +21,8 @@ import Jsx from './components/Jsx.vue'
import Constructor from './components/Constructor.vue'
import { compileStyle } from '@vue/component-compiler-utils'
import ScriptSetup from './components/ScriptSetup'
import ExtendedTsConfig from './components/ExtendedTsConfig.vue'

jest.mock('@vue/component-compiler-utils', () => ({
...jest.requireActual('@vue/component-compiler-utils'),
compileStyle: jest.fn(() => ({ errors: [], code: '' }))
Expand Down Expand Up @@ -163,6 +165,11 @@ test('processes SFC with <script setup>', () => {
expect(wrapper.html()).toContain('Welcome to Your Vue.js App')
})

test('handles extended tsconfig.json files', () => {
const wrapper = mount(ExtendedTsConfig)
expect(wrapper.element.tagName).toBe('DIV')
})

test('should pass properly "styleOptions" into "preprocessOptions"', () => {
const filePath = resolve(__dirname, './components/Basic.vue')
const fileString = readFileSync(filePath, { encoding: 'utf8' })
Expand Down
21 changes: 21 additions & 0 deletions e2e/2.x/basic/tsconfig.base.json
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "es6"],
"module": "es2015",
"moduleResolution": "node",
"types": ["vue-typescript-import-dts", "node"],
"isolatedModules": false,
"experimentalDecorators": true,
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"suppressImplicitAnyIndexErrors": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"esModuleInterop": true,
"allowJs": true
}
}
19 changes: 1 addition & 18 deletions e2e/2.x/basic/tsconfig.json
@@ -1,20 +1,3 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "es6"],
"module": "es2015",
"moduleResolution": "node",
"types": ["vue-typescript-import-dts", "node"],
"isolatedModules": false,
"experimentalDecorators": true,
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"suppressImplicitAnyIndexErrors": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"allowJs": true
}
"extends": "./tsconfig.base.json"
}
34 changes: 34 additions & 0 deletions e2e/3.x/basic/components/ExtendedTsConfig.vue
@@ -0,0 +1,34 @@
<template>
<div>
{{ exclamationMarks }}
<type-script-child />
</div>
</template>

<script lang="ts">
import TypeScriptChild from './TypeScriptChild.vue'
import moduleRequiringEsModuleInterop from './ModuleRequiringEsModuleInterop'
// The default import above relies on esModuleInterop being set to true in order to use it from
// an import statement instead of require. This option is configured in the tsconfig.base.json,
// so if we are no longer fully processing the tsconfig options (extended from a base config)
// this test should fail. This was one of the only reliable ways I could get a test to fail if
// these conditions are not being met and happen to be the use-case which was triggering errors
// in my config setup.
if (moduleRequiringEsModuleInterop()) {
throw new Error('Should never hit this')
}
export default {
computed: {
exclamationMarks(): string {
return 'string'
}
},
components: {
TypeScriptChild
}
}
</script>
1 change: 1 addition & 0 deletions e2e/3.x/basic/components/ModuleRequiringEsModuleInterop.js
@@ -0,0 +1 @@
module.exports = () => false
7 changes: 7 additions & 0 deletions e2e/3.x/basic/test.js
Expand Up @@ -23,6 +23,7 @@ import ScriptSetup from './components/ScriptSetup.vue'
import ScriptSetupSugarRef from './components/ScriptSetupSugarRef.vue'
import FunctionalRenderFn from './components/FunctionalRenderFn.vue'
import CompilerDirective from './components/CompilerDirective.vue'
import ExtendedTsConfig from './components/ExtendedTsConfig.vue'

// TODO: JSX for Vue 3? TSX?
import Jsx from './components/Jsx.vue'
Expand Down Expand Up @@ -207,3 +208,9 @@ test('ensure compilerOptions is passed down', () => {
const elm = document.querySelector('h1')
expect(elm.hasAttribute('data-test')).toBe(false)
})

test('handles extended tsconfig.json files', () => {
mount(ExtendedTsConfig)
const elm = document.querySelector('div')
expect(elm).toBeDefined()
})
21 changes: 21 additions & 0 deletions e2e/3.x/basic/tsconfig.base.json
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "es6"],
"module": "es2015",
"moduleResolution": "node",
"types": ["vue-typescript-import-dts", "node"],
"isolatedModules": false,
"experimentalDecorators": true,
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"suppressImplicitAnyIndexErrors": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"esModuleInterop": true,
"allowJs": true
}
}
19 changes: 1 addition & 18 deletions e2e/3.x/basic/tsconfig.json
@@ -1,20 +1,3 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "es6"],
"module": "es2015",
"moduleResolution": "node",
"types": ["vue-typescript-import-dts", "node"],
"isolatedModules": false,
"experimentalDecorators": true,
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"suppressImplicitAnyIndexErrors": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"allowJs": true
}
"extends": "./tsconfig.base.json"
}
2 changes: 1 addition & 1 deletion packages/vue2-jest/lib/ensure-require.js
@@ -1,4 +1,4 @@
const throwError = require('./utils').throwError
const throwError = require('./throw-error')

module.exports = function(name, deps) {
let i, len
Expand Down
3 changes: 3 additions & 0 deletions packages/vue2-jest/lib/throw-error.js
@@ -0,0 +1,3 @@
module.exports = function throwError(msg) {
throw new Error('\n[vue-jest] Error: ' + msg + '\n')
}
52 changes: 41 additions & 11 deletions packages/vue2-jest/lib/utils.js
@@ -1,6 +1,8 @@
const ensureRequire = require('./ensure-require')
const throwError = require('./throw-error')
const constants = require('./constants')
const loadPartialConfig = require('@babel/core').loadPartialConfig
const { loadSync: loadTsConfigSync } = require('tsconfig')
const { resolveSync: resolveTsConfigSync } = require('tsconfig')
const chalk = require('chalk')
const path = require('path')
const fs = require('fs')
Expand Down Expand Up @@ -68,23 +70,55 @@ const getBabelOptions = function loadBabelOptions(filename, options = {}) {
return loadPartialConfig(opts).options
}

const tsConfigCache = new Map()

/**
* Load TypeScript config from tsconfig.json.
* @param {string | undefined} path tsconfig.json file path (default: root)
* @returns {import('typescript').TranspileOptions | null} TypeScript compilerOptions or null
*/
const getTypeScriptConfig = function getTypeScriptConfig(path) {
const tsconfig = loadTsConfigSync(process.cwd(), path || '')
if (!tsconfig.path) {
if (tsConfigCache.has(path)) {
return tsConfigCache.get(path)
}

ensureRequire('typescript', ['typescript'])
const typescript = require('typescript')

const tsconfigPath = resolveTsConfigSync(process.cwd(), path || '')
if (!tsconfigPath) {
warn(`Not found tsconfig.json.`)
return null
}
const compilerOptions =
(tsconfig.config && tsconfig.config.compilerOptions) || {}

return {
compilerOptions: { ...compilerOptions, module: 'commonjs' }
const parsedConfig = typescript.getParsedCommandLineOfConfigFile(
tsconfigPath,
{},
{
...typescript.sys,
onUnRecoverableConfigFileDiagnostic: e => {
const errorMessage = typescript.formatDiagnostic(e, {
getCurrentDirectory: () => process.cwd(),
getNewLine: () => `\n`,
getCanonicalFileName: file => file.replace(/\\/g, '/')
})
warn(errorMessage)
}
}
)

const compilerOptions = parsedConfig ? parsedConfig.options : {}

const transpileConfig = {
compilerOptions: {
...compilerOptions,
module: typescript.ModuleKind.CommonJS
}
}

tsConfigCache.set(path, transpileConfig)

return transpileConfig
}

function isValidTransformer(transformer) {
Expand Down Expand Up @@ -131,10 +165,6 @@ const getCustomTransformer = function getCustomTransformer(
: transformer
}

const throwError = function error(msg) {
throw new Error('\n[vue-jest] Error: ' + msg + '\n')
}

const stripInlineSourceMap = function(str) {
return str.slice(0, str.indexOf('//# sourceMappingURL'))
}
Expand Down
2 changes: 1 addition & 1 deletion packages/vue3-jest/lib/ensure-require.js
@@ -1,4 +1,4 @@
const throwError = require('./utils').throwError
const throwError = require('./throw-error')

module.exports = function(name, deps) {
let i, len
Expand Down
3 changes: 3 additions & 0 deletions packages/vue3-jest/lib/throw-error.js
@@ -0,0 +1,3 @@
module.exports = function throwError(msg) {
throw new Error('\n[vue-jest] Error: ' + msg + '\n')
}
55 changes: 43 additions & 12 deletions packages/vue3-jest/lib/utils.js
@@ -1,6 +1,8 @@
const ensureRequire = require('./ensure-require')
const throwError = require('./throw-error')
const constants = require('./constants')
const loadPartialConfig = require('@babel/core').loadPartialConfig
const { loadSync: loadTsConfigSync } = require('tsconfig')
const { resolveSync: resolveTsConfigSync } = require('tsconfig')
const chalk = require('chalk')
const path = require('path')
const fs = require('fs')
Expand Down Expand Up @@ -68,24 +70,57 @@ const getBabelOptions = function loadBabelOptions(filename, options = {}) {
return loadPartialConfig(opts).options
}

const tsConfigCache = new Map()

/**
* Load TypeScript config from tsconfig.json.
* @param {string | undefined} path tsconfig.json file path (default: root)
* @returns {import('typescript').TranspileOptions | null} TypeScript compilerOptions or null
*/
const getTypeScriptConfig = function getTypeScriptConfig(path) {
const tsconfig = loadTsConfigSync(process.cwd(), path || '')
if (!tsconfig.path) {
if (tsConfigCache.has(path)) {
return tsConfigCache.get(path)
}

ensureRequire('typescript', ['typescript'])
const typescript = require('typescript')

const tsconfigPath = resolveTsConfigSync(process.cwd(), path || '')
if (!tsconfigPath) {
warn(`Not found tsconfig.json.`)
return null
}
const compilerOptions =
(tsconfig.config && tsconfig.config.compilerOptions) || {}

// Force es5 to prevent const vue_1 = require('vue') from conflicting
return {
compilerOptions: { ...compilerOptions, target: 'es5', module: 'commonjs' }
const parsedConfig = typescript.getParsedCommandLineOfConfigFile(
tsconfigPath,
{},
{
...typescript.sys,
onUnRecoverableConfigFileDiagnostic: e => {
const errorMessage = typescript.formatDiagnostic(e, {
getCurrentDirectory: () => process.cwd(),
getNewLine: () => `\n`,
getCanonicalFileName: file => file.replace(/\\/g, '/')
})
warn(errorMessage)
}
}
)

const compilerOptions = parsedConfig ? parsedConfig.options : {}

const transpileConfig = {
compilerOptions: {
...compilerOptions,
// Force es5 to prevent const vue_1 = require('vue') from conflicting
target: typescript.ScriptTarget.ES5,
module: typescript.ModuleKind.CommonJS
}
}

tsConfigCache.set(path, transpileConfig)

return transpileConfig
}

function isValidTransformer(transformer) {
Expand Down Expand Up @@ -133,10 +168,6 @@ const getCustomTransformer = function getCustomTransformer(
: transformer
}

const throwError = function error(msg) {
throw new Error('\n[vue-jest] Error: ' + msg + '\n')
}

const stripInlineSourceMap = function(str) {
return str.slice(0, str.indexOf('//# sourceMappingURL'))
}
Expand Down

0 comments on commit bb516da

Please sign in to comment.