Upgrading a Quasar app to modern tooling

ESM, flat config, and 16 rules I had to turn off

Geschrieben von Timo Rieber am 15. Mai 2025

I kept postponing this. The cloudapps frontend runs on Quasar and had been on @quasar/app-vite v1 since we set it up. When v2 landed with ESM everywhere, ESLint 9 flat config, and TypeScript 5, I knew the upgrade would only get worse the longer I waited. So one evening I just started.

The easy parts

The upgrade guide is solid. Most of the migration is mechanical: bump @quasar/app-vite to ^2.0.0, add "type": "module" to package.json, convert config files from CommonJS to ESM.

The quasar.config.js change is representative. Before:

const { configure } = require("quasar/wrappers");

module.exports = configure(function (/* ctx */) {
  return {
    // ...
  };
});
Javascript

After:

import { defineConfig } from "#q-app/wrappers";

export default defineConfig(function (/* ctx */) {
  return {
    // ...
  };
});
Javascript

Same for postcss.config.js. The tsconfig.json went from thirteen lines of configuration to this:

{
  "extends": "./.quasar/tsconfig.json"
}
Json

Quasar now generates and manages the TypeScript config in .quasar. Less to maintain. All Vue single-file components, Quasar component imports, the router, Pinia stores - none of that needed any changes. The build output and deployment pipeline kept working as before.

ESLint flat config

This was the actual work. Everything else took maybe thirty minutes. The ESLint migration took the rest of the evening and a follow-up session the next day.

The old .eslintrc.js was 94 lines with root, extends, plugins, parserOptions, env, globals and rules as top-level keys. The new eslint.config.js uses an array of config objects composed from imported plugins. Quasar ships a pluginQuasar.configs.recommended() preset, and the Vue and TypeScript ecosystems each have their own flat config helpers now.

The result is structurally cleaner:

export default defineConfigWithVueTs(
  { ignores: [] },
  pluginQuasar.configs.recommended(),
  js.configs.recommended,
  pluginVue.configs["flat/essential"],
  vueTsConfigs.recommendedTypeChecked,
  {
    languageOptions: { /* globals, ecmaVersion, sourceType */ },
    rules: { /* project-specific overrides */ },
  },
);
Javascript

64 lines instead of 94, and most of the boilerplate is gone. The problem was somewhere else.

I chose vueTsConfigs.recommendedTypeChecked because it was what the Quasar docs suggested. That preset enables full type-checked linting - much stricter than the plugin:@typescript-eslint/recommended we had before. Our codebase had been living with no-explicit-any and no-non-null-assertion turned off for a long time.

I ended up turning off 16 rules in a follow-up commit just to get it to pass:

rules: {
  "@typescript-eslint/no-explicit-any": "off",
  "@typescript-eslint/no-floating-promises": "off",
  "@typescript-eslint/no-misused-promises": "off",
  "@typescript-eslint/no-unused-vars": "off",
  "@typescript-eslint/require-await": "off",
  // ... and 11 more
}
Javascript

Not proud of that. The plan is to re-enable them one by one and fix the underlying code. But during a migration, getting the build green comes first.

The dependency pile

Beyond @quasar/app-vite itself, the upgrade pulled along eslint 8 to 9, typescript 4 to 5, prettier 2 to 3, vue-tsc 1 to 2, and eslint-plugin-vue 9 to 10. New additions: @eslint/js, @vue/eslint-config-prettier, @vue/eslint-config-typescript, globals. The .eslintignore file was deleted - ignores live inside the config now.

The main commit touched 10 files with 1518 insertions and 984 deletions. Most of that is yarn.lock. The only application code that changed was src/stores/index.ts (a Pinia type declaration got simplified) and a generated type file that was removed.

It took a couple of evening sessions spread over two days. @quasar/app-vite v1 used Vite all along, so the dev server was already fast - the point was staying current before ESLint 10 removes .eslintrc entirely.