Vue3 Typescript breaks the webpack encore watcher

Viewed 1613

I have a vue3/symfony project and i started implementing typescript but i came across an issue that i cannot solve. For building assets i'm using webpack encore and when i start the watcher the assets are compiled just fine, but when i change anything in my .vue files ( even adding a blank space to force webpack to recompile ) i get this error:

TS2614: Module '"resources/ts/helpers"' has no exported member 'TestClass'. Did you mean to use 'import TestClass from "resources/ts/helpers"' instead?

TS2339: Property '__file' does not exist on type '{}'.

Important notes: if i do any kind of change ( even a blank space ) on the helper.ts the compilation is again succesfully.

This only happens to .ts files imported into .vue files. The .js or .vue(with or without typescript) files that i've imported are fine

helpers.ts:

export class TestClass {
  constructor(public test: string) {
  }
}

Imported as import { TestClass } from "resources/ts/helpers";

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "noImplicitThis": true,
    "jsx": "preserve",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "lib": [
      "esnext",
      "dom",
      "dom.iterable",
      "scripthost"
    ],
    "baseUrl": ".",
    "paths": {
      "resources/*": [
        "resources/*"
      ]
    }
  },
  "include": [
    "resources/**/*.ts",
    "resources/**/*.tsx",
    "resources/**/*.vue",
  ],
  "exclude": [
    "node_modules"
  ]
}

webpack.config.js:

const Encore = require('@symfony/webpack-encore');
const path = require('path');
const webpack = require('webpack');

// Manually configure the runtime environment if not already configured yet by the "encore" command.
// It's useful when you use tools that rely on webpack.config.js file.
if (!Encore.isRuntimeEnvironmentConfigured()) {
  Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
}

Encore
  // directory where compiled assets will be stored
  .setOutputPath('public/build/')
  // public path used by the web server to access the output path
  .setPublicPath('/build')
  // only needed for CDN's or sub-directory deploy
  //.setManifestKeyPrefix('build/')
  .copyFiles({
    from: './resources/assets/media',
    to: 'media/[path][name].[ext]',
    pattern: /\.(png|jpg|jpeg|svg)$/
  })
  .copyFiles({
    from: './resources/assets/fonts',
    to: 'fonts/[path][name].[ext]',
    pattern: /\.(ttf)$/
  })
  /*
   * ENTRY CONFIG
   *
   * Add 1 entry for each "page" of your app
   * (including one that's included on every page - e.g. "app")
   *
   * Each entry will result in one JavaScript file (e.g. main.js)
   * and one CSS file (e.g. app.css) if you JavaScript imports CSS.
   */
  .addEntry('main', './resources/main.js')

  // When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
  .splitEntryChunks()

  // will require an extra script tag for runtime.js
  // but, you probably want this, unless you're building a single-page app
  .enableSingleRuntimeChunk()

  /*
   * FEATURE CONFIG
   *
   * Enable & configure other features below. For a full
   * list of features, see:
   * https://symfony.com/doc/current/frontend.html#adding-more-features
   */
  .cleanupOutputBeforeBuild()
  .enableBuildNotifications()
  .enableSourceMaps(!Encore.isProduction())
  // enables hashed filenames (e.g. app.abc123.css)
  .enableVersioning(Encore.isProduction())

  // enables @babel/preset-env polyfills
  .configureBabel(() => {
  }, {
    useBuiltIns: 'usage',
    corejs: 3
  })

  // enables Sass/SCSS support
  .enableSassLoader()

  // enables Vue support
  .enableVueLoader(() => {
  }, {
    version: 3,
    runtimeCompilerBuild: false //if using only single file components, this is not needed (https://symfony.com/doc/current/frontend/encore/vuejs.html#runtime-compiler-build)

  })
  // uncomment if you use TypeScript
  .enableTypeScriptLoader()

  // uncomment if you're having problems with a jQuery plugin
  .autoProvidejQuery()
  .addAliases({
    'resources': path.resolve('./resources')
  })
;

module.exports = Encore.getWebpackConfig();

package.json:

{
  "devDependencies": {
    "@symfony/webpack-encore": "^1.5.0",
    "@types/jquery": "^3.5.5",
    "@vue/compiler-sfc": "^3.0.2",
    "babel-core": "^7.0.0-bridge.0",
    "file-loader": "^6.0.0",
    "https-proxy-agent": "^2.2.1",
    "lorem-ipsum": "^2.0.3",
    "sass": "^1.32.13",
    "sass-loader": "^10.2.0",
    "ts-loader": "^8.3.0",
    "tslib": "^2.3.0",
    "vue-loader": "^16.5.0",
    "vue-template-compiler": "^2.6.12",
    "webpack-notifier": "^1.6.0"
  },
  "license": "UNLICENSED",
  "private": true,
  "scripts": {
    "dev-server": "encore dev-server",
    "dev": "encore dev",
    "watch": "encore dev --watch",
    "build": "encore production --progress"
  },
  "dependencies": {
    "@babel/polyfill": "^7.12.1",
    "@ckeditor/ckeditor5-build-classic": "^25.0.0",
    "@fortawesome/fontawesome-free": "^5.15.3",
    "@popperjs/core": "^2.5.4",
    "@tinymce/tinymce-vue": "^4.0.0",
    "@vee-validate/rules": "^4.2.4",
    "@vueform/multiselect": "^2.0.1",
    "axios": "^0.21.1",
    "bootstrap": "^5.0.2",
    "chart.js": "^2.9.4",
    "core-js": "^3.6.5",
    "dropzone": "^5.9.2",
    "element-plus": "^1.0.2-beta.36",
    "es6-promise": "^4.2.8",
    "inputmask": "^5.0.6",
    "jquery": "^3.5.1",
    "lodash": "^4.17.20",
    "nprogress": "^0.2.0",
    "perfect-scrollbar": "^1.5.0",
    "select2": "^4.0.13",
    "sweetalert2": "^10.10.0",
    "typescript": "^4.3.4",
    "vee-validate": "^4.5.0-alpha.2",
    "vue": "^3.0.7",
    "vue-inline-svg": "^3.0.0-beta.2",
    "vue-router": "^4.0.3",
    "vuex": "^4.0.0-rc.1",
    "yup": "^0.29.3"
  }
}
2 Answers

It seems like you are migrating your Vue Javascript project into the Vue typescript project. So, we have to configure the webpack and tsconfig wisely. We have to follow some steps to configure it properly.

  1. Adding typescript to our ts project
vue add typescript
  1. Configure the tsconfig file to support javascript and typescript module

    I would recommend that you must add

    • allowJS as true so that it allows the js file to be imported into our ts module.

    • importHelper as true.

    • allowSyntheticDefaultImports as true, it allows default export like below and I think this property of compiler option will help you get your work done for the current situation. It is not able to re-compile import or export statements correctly in my view.

import TestClass from "resources/ts/helpers";
  1. Add shims-vue.d.ts and shim-tsx.d.ts files under your project directory, it will allow typescript to understand *.vue file and JSX syntax of the code style. If you want to know more about the difference between the two, you should read this answer.
//shims-vue.d.ts
declare module "*.vue" {
  import Vue from 'vue';
  export default Vue;
}

//shims-tsx.d.ts
import Vue, { VNode } from 'vue';

declare global {
  namespace JSX {
    // tslint:disable no-empty-interface
    interface Element extends VNode {}
    // tslint:disable no-empty-interface
    interface ElementClass extends Vue {}
    interface IntrinsicElements {
      [elem: string]: any;
    }
  }
}

To allow typescript to read these two files, we need to add those files into the files property of our tsconfig.

// tsconfig.ts
  "files": [
     "shims-vue.d.ts",
     "shims-tsx.d.ts"
   ] 

Your whole tsconfig file would look like below

{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "noImplicitThis": true,
    "jsx": "preserve",
    "moduleResolution": "node",
    "esModuleInterop": true,

 // my recommendation
    "importHelpers": true,
    "allowJS": true,
    "allowSyntheticDefaultImports": true,
 // -------------------------

    "skipLibCheck": true,
    "lib": [
      "esnext",
      "dom",
      "dom.iterable",
      "scripthost"
    ],
    "baseUrl": ".",
    "paths": {
      "resources/*": [
        "resources/*"
      ]
    }
  },
 // my addition
  "files": [
    "shims-vue.d.ts",
    "shims-tsx.d.ts"
  ] 
 // -------------
  "include": [
    "resources/**/*.ts",
    "resources/**/*.tsx",
    "resources/**/*.vue",
  ],
  "exclude": [
    "node_modules"
  ]
}
  1. Now, the final step is to configure our webpack encore. I checked your config, I found some of the configs that you were missing.
    • First, you convert your main.js to main.ts.
    • For ts-loader to parse <script lang="ts"> blocks in .vue file you need to add appendTsSuffixTo config.
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');

 // ...
 Encore
     // ...
     .addEntry('main', './resources/main.ts')

     .enableTypeScriptLoader(function (tsConfig){
       tsConfig.appendTsSuffixTo = [/\.vue$/]; 
       tsConfig.appendTsxSuffixTo = [/\.vue$/]; 
     })

     // don't know the reason why you didn't add HtmlWebpackPlugin
     .addPlugin(new HtmlWebpackPlugin({
       template: './src/main.html',
     }))    

If you follow all these steps, I think you will not get any issues. And lastly, you also need @babel/typescript or many typescript plugins to support typescript in the project.

One possible problem that I can see is this line:

.addEntry('main', './resources/main.js')

In the Encore docs for using typescript, they set the entry to a .ts file:

.addEntry('main', './assets/main.ts')

If you have a main.ts file then that could explain why editing helper.ts causes it to work again. Editing the file triggers the typescript compiler to recompile (transpile from .ts -> .js), and that creates a main.js file (which might have been absent before).

Related