function generateTsConfig (quasarConf, fsUtils) {
  const { appPaths } = quasarConf.ctx

  /** Returns the path relative to the tsconfig.json file, in POSIX format */
  const toTsPath = pathToTransform => {
    // Folder aliases are defined as absolute paths.
    // So, the rest, e.g. `'some-pkg': 'another-pkg'`, is not absolute and must be resolved as a package.
    const itemPath = isAbsolute(pathToTransform) === false
      // Try to resolve the package path first, it's crucial to some monorepo setups like npm/yarn/bun workspaces
      ? (getPackagePath(pathToTransform, appPaths.appDir) || join('node_modules', pathToTransform))
      : pathToTransform

    const relativePath = relative(fsUtils.tsConfigDir, itemPath).replaceAll('\\', '/')

    if (relativePath.length === 0) return '.'
    if (relativePath.startsWith('./') === false) return ('./' + relativePath)
    return relativePath
  }

  const aliasMap = { ...quasarConf.build.alias }

  // TS aliases doesn't play well with package.json#exports: https://github.com/microsoft/TypeScript/issues/60460
  // So, we had to specify each entry point separately here
  delete aliasMap[ '#q-app' ] // remove the existing one so that all the added ones are listed under each other
  Object.assign(aliasMap, qAppPaths)

  if (isModeInstalled(appPaths, 'capacitor')) {
    const target = appPaths.resolve.capacitor('node_modules')
    const { dependencies } = JSON.parse(
      fse.readFileSync(appPaths.resolve.capacitor('package.json'), 'utf-8')
    )

    Object.keys(dependencies).forEach(dep => {
      aliasMap[ dep ] = join(target, dep)
    })
  }

  const paths = {}
  Object.keys(aliasMap).forEach(alias => {
    const rawPath = aliasMap[ alias ]
    const tsPath = toTsPath(rawPath)

    const stats = statSync(
      join(fsUtils.tsConfigDir, tsPath),
      { throwIfNoEntry: false }
    )

    // import ... from 'src' (resolves to 'src/index')
    paths[ alias ] = [ tsPath ]

    if (stats === void 0 || stats.isFile() === true) return

    // import ... from 'src/something' (resolves to 'src/something.ts' or 'src/something/index.ts')
    paths[ `${ alias }/*` ] = [ `${ tsPath }/*` ]
  })

  // See https://www.totaltypescript.com/tsconfig-cheat-sheet
  // We use ESNext since we are transpiling and pretty much everything should work
  // We recommend `@typescript-eslint/consistent-type-imports` instead of `verbatimModuleSyntax`, if using linting (using both can cause conflicts)
  const tsConfig = {
    compilerOptions: {
      esModuleInterop: true,
      skipLibCheck: true,
      target: 'esnext',
      allowJs: true,
      resolveJsonModule: true,
      moduleDetection: 'force',
      isolatedModules: true,

      // We are not transpiling with tsc, so leave it to the bundler
      module: 'preserve', // implies `moduleResolution: 'bundler'`
      noEmit: true,

      lib: [ 'esnext', 'dom', 'dom.iterable' ],

      /**
       * Keep in sync with the description of `typescript.strict` in {@link file://./../types/configuration/build.d.ts}
       */
      ...(quasarConf.build.typescript.strict
        ? {
            strict: true,
            allowUnreachableCode: false,
            allowUnusedLabels: false,
            noImplicitOverride: true,
            exactOptionalPropertyTypes: true,
            noUncheckedIndexedAccess: true
          }
        : {}),

      paths
    },
    // include and exclude are relative to .quasar
    include: [
      './**/*.d.ts', // Since .quasar starts with a dot, it won't be included by default
      './../**/*'
    ],
    exclude: [
      './../dist',
      './../node_modules',
      './../src-capacitor',
      './../src-cordova',
      './../quasar.config.*.temporary.compiled*'
    ]
  }

  quasarConf.build.typescript.extendTsConfig?.(tsConfig)

  fsUtils.writeFileSync(
    'tsconfig.json',
    JSON.stringify(tsConfig, null, 2)
  )
}
@param {import('../types/configuration/conf').QuasarConf} quasarConf