diff --git a/app-vite/bin/quasar.js b/app-vite/bin/quasar.js index 2cda328fbc6..9a0d8599e92 100755 --- a/app-vite/bin/quasar.js +++ b/app-vite/bin/quasar.js @@ -5,6 +5,7 @@ import '../lib/node-version-check.js' const commands = [ 'dev', 'build', + 'prepare', 'clean', 'inspect', 'describe', @@ -23,6 +24,7 @@ if (cmd) { const mapToCmd = { d: 'dev', b: 'build', + p: 'prepare', e: 'ext', r: 'run', c: 'clean', diff --git a/app-vite/lib/app-devserver.js b/app-vite/lib/app-devserver.js index 19dff2ba7ef..bb924eb9dfd 100644 --- a/app-vite/lib/app-devserver.js +++ b/app-vite/lib/app-devserver.js @@ -2,6 +2,7 @@ import { AppTool } from './app-tool.js' import { printDevRunningBanner } from './utils/banner.js' import { encodeForDiff } from './utils/encode-for-diff.js' import { EntryFilesGenerator } from './entry-files-generator.js' +import { generateTypes } from './types-generator.js' function getConfSnapshot (extractFn, quasarConf, diffExtractFnMap) { return extractFn(quasarConf, diffExtractFnMap).map(item => (item ? encodeForDiff(item) : '')) @@ -36,6 +37,11 @@ export class AppDevserver extends AppTool { quasarConf.ssr.manualPostHydrationTrigger ])) + this.registerDiff('types', quasarConf => ([ + quasarConf.build.typescript, + quasarConf.build.alias + ])) + this.registerDiff('viteUrl', quasarConf => ([ quasarConf.metaConf.APP_URL ])) @@ -63,6 +69,10 @@ export class AppDevserver extends AppTool { this.#entryFiles.generate(quasarConf) } + if (this.#diff('types', quasarConf)) { + this.#queue(this.#runId, quasarConf, () => generateTypes(quasarConf)) + } + if (__isRetry !== true) { this.#runId++ } diff --git a/app-vite/lib/cmd/build.js b/app-vite/lib/cmd/build.js index 8a8fd862863..1b493511001 100755 --- a/app-vite/lib/cmd/build.js +++ b/app-vite/lib/cmd/build.js @@ -137,9 +137,6 @@ await quasarConfFile.init() const quasarConf = await quasarConfFile.read() -const { ensureTypesFeatureFlags } = await import('../utils/types-feature-flags.js') -ensureTypesFeatureFlags(quasarConf) - const { QuasarModeBuilder } = await import(`../modes/${ argv.mode }/${ argv.mode }-builder.js`) const appBuilder = new QuasarModeBuilder({ argv, quasarConf }) @@ -151,6 +148,9 @@ const { EntryFilesGenerator } = await import('../entry-files-generator.js') const entryFiles = new EntryFilesGenerator(ctx) entryFiles.generate(quasarConf) +const { generateTypes } = await import('../types-generator.js') +await generateTypes(quasarConf) + if (typeof quasarConf.build.beforeBuild === 'function') { await quasarConf.build.beforeBuild({ quasarConf }) } diff --git a/app-vite/lib/cmd/dev.js b/app-vite/lib/cmd/dev.js index 1851cce6035..d76e04a22fd 100755 --- a/app-vite/lib/cmd/dev.js +++ b/app-vite/lib/cmd/dev.js @@ -156,9 +156,6 @@ await quasarConfFile.init() const quasarConf = await quasarConfFile.read() -import { ensureTypesFeatureFlags } from '../utils/types-feature-flags.js' -ensureTypesFeatureFlags(quasarConf) - if (quasarConf.metaConf.vueDevtools !== false) { await startVueDevtools(ctx, quasarConf.metaConf.vueDevtools.port) } diff --git a/app-vite/lib/cmd/help.js b/app-vite/lib/cmd/help.js index 31ea44c3b82..2879cd3a7ee 100755 --- a/app-vite/lib/cmd/help.js +++ b/app-vite/lib/cmd/help.js @@ -28,6 +28,7 @@ console.log(` Commands dev, d Start a dev server for your App build, b Build your app for production + prepare, p Prepare the app for linting, type-checking, IDE integration, etc. clean, c Clean dev/build cache, /dist folder & entry points new, n Quickly scaffold page/layout/component/... vue file mode, m Add/remove Quasar Modes for your App diff --git a/app-vite/lib/cmd/mode.js b/app-vite/lib/cmd/mode.js index 3339ad39d9f..7bad9d77808 100755 --- a/app-vite/lib/cmd/mode.js +++ b/app-vite/lib/cmd/mode.js @@ -44,11 +44,13 @@ if (argv._.length !== 0 && argv._.length !== 2) { import { green, gray } from 'kolorist' import { getCtx } from '../utils/get-ctx.js' -const ctx = getCtx() +import { generateTypes } from '../types-generator.js' async function run () { const [ action, mode ] = argv._ + const ctx = getCtx({ mode }) + if (![ 'add', 'remove' ].includes(action)) { console.log() warn(`Unknown action specified (${ action }).`) @@ -83,9 +85,23 @@ async function run () { } await actionMap[ action ]({ ctx }) + + // Ensure types are re-generated accordingly + const { QuasarConfigFile } = await import('../quasar-config-file.js') + const quasarConfFile = new QuasarConfigFile({ + ctx, + // host and port don't matter for this command + port: 9000, + host: 'localhost' + }) + await quasarConfFile.init() + const quasarConf = await quasarConfFile.read() + await generateTypes(quasarConf) } async function displayModes () { + const ctx = getCtx() + log('Detecting installed modes...') const info = [] diff --git a/app-vite/lib/cmd/prepare.js b/app-vite/lib/cmd/prepare.js new file mode 100755 index 00000000000..7236992d209 --- /dev/null +++ b/app-vite/lib/cmd/prepare.js @@ -0,0 +1,62 @@ +import parseArgs from 'minimist' + +import { log } from '../utils/logger.js' + +const argv = parseArgs(process.argv.slice(2), { + alias: { + h: 'help' + }, + boolean: [ 'h' ] +}) + +if (argv.help) { + console.log(` + Description + Prepare the app for linting, type-checking, IDE integration, etc. + It will generate the relevant files such as '.quasar/tsconfig.json', types files, etc. + Running 'quasar dev' or 'quasar build' will automatically handle this for you. + Use this command for a lightweight alternative to dev/build. Useful in CI/CD pipelines. + + Usage + $ quasar prepare + + Options + --help, -h Displays this message + `) + process.exit(0) +} + +const { readFileSync } = await import('node:fs') + +console.log( + readFileSync( + new URL('../../assets/logo.art', import.meta.url), + 'utf8' + ) +) + +const { getCtx } = await import('../utils/get-ctx.js') +// ctx doesn't matter for this command +const ctx = getCtx({ + mode: 'spa', + debug: false, + prod: true +}) + +const { QuasarConfigFile } = await import('../quasar-config-file.js') +const quasarConfFile = new QuasarConfigFile({ + ctx, + // host and port don't matter for this command + port: 9000, + host: 'localhost' +}) + +await quasarConfFile.init() + +const quasarConf = await quasarConfFile.read() + +const { generateTypes } = await import('../types-generator.js') +await generateTypes(quasarConf) + +log('Generated tsconfig.json and types files in .quasar directory') +log('The app is now prepared for linting, type-checking, IDE integration, etc.') diff --git a/app-vite/lib/modes/bex/bex-installation.js b/app-vite/lib/modes/bex/bex-installation.js index 4dc7e6ce37c..74b956fa30d 100644 --- a/app-vite/lib/modes/bex/bex-installation.js +++ b/app-vite/lib/modes/bex/bex-installation.js @@ -2,7 +2,6 @@ import fs from 'node:fs' import fse from 'fs-extra' import { log, warn } from '../../utils/logger.js' -import { generateTypesFeatureFlag } from '../../utils/types-feature-flags.js' export function isModeInstalled (appPaths) { return fs.existsSync(appPaths.bexDir) @@ -23,7 +22,6 @@ export async function addMode ({ log('Creating Browser Extension source folder...') fse.copySync(appPaths.resolve.cli('templates/bex/common'), appPaths.bexDir) - generateTypesFeatureFlag('bex', appPaths) const hasTypescript = await cacheProxy.getModule('hasTypescript') const format = hasTypescript ? 'ts' : 'default' diff --git a/app-vite/lib/modes/electron/electron-installation.js b/app-vite/lib/modes/electron/electron-installation.js index ce967421aaa..f21689ff4bf 100644 --- a/app-vite/lib/modes/electron/electron-installation.js +++ b/app-vite/lib/modes/electron/electron-installation.js @@ -2,7 +2,6 @@ import fs from 'node:fs' import fse from 'fs-extra' import { log, warn } from '../../utils/logger.js' -import { generateTypesFeatureFlag } from '../../utils/types-feature-flags.js' const electronDeps = { electron: 'latest' @@ -37,8 +36,6 @@ export async function addMode ({ appPaths.electronDir ) - generateTypesFeatureFlag('electron', appPaths) - log('Creating Electron icons folder...') fse.copySync( appPaths.resolve.cli('templates/electron/icons'), diff --git a/app-vite/lib/modes/pwa/pwa-installation.js b/app-vite/lib/modes/pwa/pwa-installation.js index c8ff531a6dd..7db7f1f25fd 100644 --- a/app-vite/lib/modes/pwa/pwa-installation.js +++ b/app-vite/lib/modes/pwa/pwa-installation.js @@ -2,7 +2,6 @@ import fs from 'node:fs' import fse from 'fs-extra' import { log, warn } from '../../utils/logger.js' -import { generateTypesFeatureFlag } from '../../utils/types-feature-flags.js' const defaultVersion = '^7.0.0' @@ -58,8 +57,6 @@ export async function addMode ({ hasEslint === true ? { filter: src => !src.endsWith('/.eslintrc.cjs') } : void 0 ) - generateTypesFeatureFlag('pwa', appPaths) - log('Copying PWA icons to /public/icons/ (if they are not already there)...') fse.copySync( appPaths.resolve.cli('templates/pwa-icons'), diff --git a/app-vite/lib/modes/ssr/ssr-devserver.js b/app-vite/lib/modes/ssr/ssr-devserver.js index d1f5dccae12..6a8ee04106f 100644 --- a/app-vite/lib/modes/ssr/ssr-devserver.js +++ b/app-vite/lib/modes/ssr/ssr-devserver.js @@ -279,7 +279,7 @@ export class QuasarModeDevserver extends AppDevserver { } async #bootWebserver (quasarConf) { - const done = progress(`Booting Webserver...`) + const done = progress('Booting Webserver...') if (this.#webserver !== null) { await this.#webserver.close() diff --git a/app-vite/lib/modes/ssr/ssr-installation.js b/app-vite/lib/modes/ssr/ssr-installation.js index d1f893a420c..807de51469f 100644 --- a/app-vite/lib/modes/ssr/ssr-installation.js +++ b/app-vite/lib/modes/ssr/ssr-installation.js @@ -2,7 +2,6 @@ import fs from 'node:fs' import fse from 'fs-extra' import { log, warn } from '../../utils/logger.js' -import { generateTypesFeatureFlag } from '../../utils/types-feature-flags.js' export function isModeInstalled (appPaths) { return fs.existsSync(appPaths.ssrDir) @@ -27,8 +26,6 @@ export async function addMode ({ appPaths.ssrDir ) - generateTypesFeatureFlag('ssr', appPaths) - log('SSR support was added') } diff --git a/app-vite/lib/quasar-config-file.js b/app-vite/lib/quasar-config-file.js index 07c6256ddf0..b1f1ae7255a 100644 --- a/app-vite/lib/quasar-config-file.js +++ b/app-vite/lib/quasar-config-file.js @@ -281,7 +281,14 @@ export class QuasarConfigFile { plugins: [ quasarEsbuildInjectReplacementsPlugin, quasarEsbuildVueShimPlugin - ] + ], + logOverride: { + // .quasar/tsconfig.json won't be available for the first time executing dev/build/prepare. + // So, esbuild will show a warning saying it can't find the `extends` file. + // We need to suppress the warning. Otherwise, it will be noisy and cause a temp file to be created. + // tsconfig is not really important for the config file itself anyway. + 'tsconfig.json': 'silent' + } } } @@ -755,6 +762,11 @@ export class QuasarConfigFile { assets: appPaths.resolve.src('assets'), boot: appPaths.resolve.src('boot'), stores: appPaths.resolve.src('stores') + }, + + typescript: { + strict: false, + vueShim: false } }, cfg.build) diff --git a/app-vite/lib/types-generator.js b/app-vite/lib/types-generator.js new file mode 100644 index 00000000000..fd70cb52bf8 --- /dev/null +++ b/app-vite/lib/types-generator.js @@ -0,0 +1,213 @@ +import { readFileSync, writeFileSync, statSync } from 'node:fs' +import { ensureFileSync } from 'fs-extra' +import { join, relative } from 'node:path' + +// We generate all the files for JS projects as well, because they provide +// better autocomplete and type checking in the IDE. +export async function generateTypes (quasarConf) { + const { appPaths } = quasarConf.ctx + + const tsConfigPath = appPaths.resolve.app('.quasar/tsconfig.json') + ensureFileSync(tsConfigPath) + writeFileSync(tsConfigPath, JSON.stringify(generateTsConfig(quasarConf), null, 2), 'utf-8') + + await writeFeatureFlags(quasarConf) + + writeDeclarations(quasarConf) +} + +/** + * @param {import('../types/configuration/conf').QuasarConf} quasarConf + */ +function generateTsConfig (quasarConf) { + const { appPaths, mode } = quasarConf.ctx + + const toTsPath = (path) => { + const relativePath = relative(appPaths.resolve.app('.quasar'), path) + if (relativePath.length === 0) { + return '.' + } + if (!relativePath.startsWith('./')) { + return './' + relativePath + } + return relativePath + } + + const aliases = { ...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 + const appVitePath = 'node_modules/@quasar/app-vite' + delete aliases[ '#q-app' ] // remove the existing one so that all the added ones are listed under each other + aliases[ '#q-app' ] = toTsPath(join(appVitePath, 'types/index.d.ts')) + aliases[ '#q-app/wrappers' ] = toTsPath(join(appVitePath, 'types/app-wrappers.d.ts')) + aliases[ '#q-app/bex/background' ] = toTsPath(join(appVitePath, 'types/bex/entrypoints/background.d.ts')) + aliases[ '#q-app/bex/content' ] = toTsPath(join(appVitePath, 'types/bex/entrypoints/content.d.ts')) + + if (mode.capacitor) { + // Can't use cacheProxy.getRuntime('runtimeCapacitorConfig') as it's not available here yet + const { dependencies } = JSON.parse( + readFileSync(appPaths.resolve.capacitor('package.json'), 'utf-8') + ) + const target = appPaths.resolve.capacitor('node_modules') + const depsList = Object.keys(dependencies) + depsList.forEach(dep => { + aliases[ dep ] = join(target, dep) + }) + } + + const paths = Object.fromEntries( + Object.entries(aliases).flatMap(([ alias, path ]) => { + const stats = statSync(path, { throwIfNoEntry: false }) + // If the path doesn't exist, don't add an alias for it yet (e.g. src/stores) + if (!stats) { + return [] + } + + if (stats.isFile()) { + return [ + [ alias, [ toTsPath(path) ] ] + ] + } + + return [ + // import ... from 'src' (resolves to 'src/index') + [ alias, [ toTsPath(path) ] ], + // import ... from 'src/something' (resolves to 'src/something.ts' or 'src/something/index.ts') + [ `${ alias }/*`, [ `${ toTsPath(path) }/*` ] ] + ] + }) + ) + + // See https://www.totaltypescript.com/tsconfig-cheat-sheet + // We use ESNext since we are transpiling and pretty much everything should work + const tsConfig = { + compilerOptions: { + esModuleInterop: true, + skipLibCheck: true, + target: 'esnext', + allowJs: true, + resolveJsonModule: true, + moduleDetection: 'force', + isolatedModules: true, + // force using `import type`/`export type` + verbatimModuleSyntax: 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 + }, + exclude: [ + 'dist', + '.quasar/*/*.js', + 'node_modules', + 'src-capacitor', + 'src-cordova', + 'quasar.config.*.temporary.compiled*' + ].map(path => toTsPath(path)) + } + + quasarConf.build.typescript.extendTsConfig?.(tsConfig) + + return tsConfig +} + +// We don't have a specific entry for the augmenting file in `package.json > exports` +// We rely on the wildcard entry, so we use a deep import, instead of let's say `quasar/feature-flags` +// When using TypeScript `moduleResolution: "bundler"`, it requires the file extension. +// This may sound unusual, but that's because it seems to treat wildcard entries differently. +const featureFlagsTemplate = `/* eslint-disable */ +import "quasar/dist/types/feature-flag.d.ts"; + +declare module "quasar/dist/types/feature-flag.d.ts" { + interface QuasarFeatureFlags { + __INJECTION_POINT__ + } +} +` + +/** + * Flags are also available in JS codebases because feature flags still + * benefit JS users by providing autocomplete. + * + * @param {import('../types/configuration/conf').QuasarConf} quasarConf + */ +async function writeFeatureFlags (quasarConf) { + const { appPaths } = quasarConf.ctx + + const featureFlags = new Set() + + if (quasarConf.metaConf.hasStore === true) { + featureFlags.add('store') + } + + // spa does not have a feature flag, so we skip it + const modes = [ 'pwa', 'ssr', 'cordova', 'capacitor', 'electron', 'bex' ] + for (const modeName of modes) { + const { isModeInstalled } = await import(`./modes/${ modeName }/${ modeName }-installation.js`) + if (isModeInstalled(quasarConf.ctx.appPaths)) { + featureFlags.add(modeName) + } + } + + const flagDefinitions = Array.from(featureFlags) + .map(flag => `${ flag }: true;`) + .join('\n ') + const contents = featureFlagsTemplate.replace( + '__INJECTION_POINT__', + flagDefinitions || '// no feature flags' + ) + + writeFileSync(appPaths.resolve.app('.quasar/feature-flags.d.ts'), contents) +} + +/* + Load app-vite's augmentations for `quasar` package. + It will augment CLI-specific features. + + Load Vite's client types, see https://vitejs.dev/guide/features#client-types +*/ +const declarationsTemplate = `/* eslint-disable */ +/// + +/// +` + +// Mocks all files ending in `.vue` showing them as plain Vue instances +const vueShimsTemplate = `/* eslint-disable */ +declare module '*.vue' { + import { DefineComponent } from 'vue'; + const component: DefineComponent; + export default component; +} +` + +/** + * @param {import('../types/configuration/conf').QuasarConf} quasarConf + */ +function writeDeclarations (quasarConf) { + const { appPaths } = quasarConf.ctx + + writeFileSync(appPaths.resolve.app('.quasar/quasar.d.ts'), declarationsTemplate) + if (quasarConf.build.typescript.vueShim) { + writeFileSync(appPaths.resolve.app('.quasar/shims-vue.d.ts'), vueShimsTemplate) + } +} diff --git a/app-vite/lib/utils/types-feature-flags.js b/app-vite/lib/utils/types-feature-flags.js deleted file mode 100644 index 344c36050fe..00000000000 --- a/app-vite/lib/utils/types-feature-flags.js +++ /dev/null @@ -1,75 +0,0 @@ -import { join, dirname } from 'node:path' -import { writeFileSync, existsSync } from 'node:fs' - -import { log } from './logger.js' - -// We don't have a specific entry for the augmenting file in `package.json > exports` -// We rely on the wildcard entry, so we use a deep import, instead of let's say `quasar/feature-flags` -// When using TypeScript `moduleResolution: "bundler"`, it requires the file extension. -// This may sound unusual, but that's because it seems to treat wildcard entries differently. -// -// Keep in sync with `create-quasar/templates/**/store-flag.d.ts` -const template = `/* eslint-disable */ -/* - WARNING: DO NOT MODIFY OR DELETE - This file is auto-generated by Quasar CLI - It's recommended to NOT .gitignore it - You don't have to use TypeScript in your project, don't worry -*/ -import "quasar/dist/types/feature-flag.d.ts"; - -declare module "quasar/dist/types/feature-flag.d.ts" { - interface QuasarFeatureFlags { - __FEATURE_NAME__: true; - } -} -` - -export function generateTypesFeatureFlag (modeName, appPaths) { - const destFlagPath = appPaths.resolve[ modeName ](`${ modeName }-flag.d.ts`) - - writeFileSync( - destFlagPath, - template.replace('__FEATURE_NAME__', modeName) - ) -} - -export function ensureTypesFeatureFlags (quasarConf) { - // Flags must be available even in pure JS codebases, - // because boot and configure wrappers functions files will - // provide autocomplete based on them also to JS users - // Flags files should be copied over, for every enabled mode, - // every time `quasar dev` and `quasar build` are run: - // this automatize the upgrade for existing codebases - const { appPaths, mode } = quasarConf.ctx - - if (quasarConf.metaConf.hasStore === true) { - const destFlagPath = appPaths.resolve.app( - join(dirname(quasarConf.sourceFiles.store), 'store-flag.d.ts') - ) - - if (!existsSync(destFlagPath)) { - writeFileSync( - destFlagPath, - template.replace('__FEATURE_NAME__', 'store') - ) - log('"store" feature flag was missing and has been regenerated') - } - } - - for (const modeName of Object.keys(mode)) { - if (modeName === 'spa' || mode[ modeName ] !== true) { - continue - } - - const destFlagPath = appPaths.resolve[ modeName ](`${ modeName }-flag.d.ts`) - - if (!existsSync(destFlagPath)) { - writeFileSync( - destFlagPath, - template.replace('__FEATURE_NAME__', modeName) - ) - log(`"${ modeName }" feature flag was missing and has been regenerated`) - } - } -} diff --git a/app-vite/package.json b/app-vite/package.json index 46d149c76c6..dfb6dcb8c97 100644 --- a/app-vite/package.json +++ b/app-vite/package.json @@ -12,9 +12,7 @@ "bin", "lib", "templates", - "types", - "stricter-tsconfig-preset.json", - "tsconfig-preset.json" + "types" ], "types": "./types/index.d.ts", "exports": { @@ -139,6 +137,7 @@ "electron-builder": ">= 22", "pinia": "^2.0.0", "quasar": "^2.16.0", + "typescript": ">= 5.4", "vue": "^3.2.29", "vue-router": "^4.0.12", "vuex": "^4.0.0", diff --git a/app-vite/stricter-tsconfig-preset.json b/app-vite/stricter-tsconfig-preset.json deleted file mode 100644 index 87c5c03da27..00000000000 --- a/app-vite/stricter-tsconfig-preset.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "./tsconfig-preset.json", - "compilerOptions": { - "exactOptionalPropertyTypes": true, - "noImplicitReturns": true, - "noPropertyAccessFromIndexSignature": true, - "noUncheckedIndexedAccess": true - } -} diff --git a/app-vite/tsconfig-preset.json b/app-vite/tsconfig-preset.json deleted file mode 100644 index 28c89a88ae9..00000000000 --- a/app-vite/tsconfig-preset.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "compilerOptions": { - "allowImportingTsExtensions": true, - "allowJs": true, - - // `"baseUrl": "."` option should be defined in the tsconfig file in devland which extends this preset - // That way, the `paths` we defined in this preset are resolved starting from the devland project root - // instead of this TS preset file's location - // This allows Quasar to centralize the management of default paths in the preset, - // instead of asking the dev to keep them in sync when something changes - - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - // Needed to address https://github.com/quasarframework/app-extension-typescript/issues/36 - "noEmit": true, - "resolveJsonModule": true, - "sourceMap": true, - "strict": true, - "target": "esnext", - "lib": ["esnext", "dom"], - "isolatedModules": true, - - "paths": { - // 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 - "#q-app": ["node_modules/@quasar/app-vite"], - "#q-app/wrappers": ["node_modules/@quasar/app-vite/types/app-wrappers.d.ts"], - "#q-app/bex/background": ["node_modules/@quasar/app-vite/types/bex/entrypoints/background.d.ts"], - "#q-app/bex/content": ["node_modules/@quasar/app-vite/types/bex/entrypoints/content.d.ts"], - "src/*": ["src/*"], - "app/*": ["*"], - "components/*": ["src/components/*"], - "layouts/*": ["src/layouts/*"], - "pages/*": ["src/pages/*"], - "assets/*": ["src/assets/*"], - "boot/*": ["src/boot/*"], - "stores/*": ["src/stores/*"] - }, - - // Fix Volar issue https://github.com/johnsoncodehk/volar/issues/1153 - "jsx": "preserve", - - // Rules preventing code smells and enforcing best practices, partially overlapping with linting - "allowUnreachableCode": false, - "allowUnusedLabels": false, - "noImplicitOverride": true, - "noUnusedLocals": true, - // Avoid cross-os errors due to inconsistent file casing - "forceConsistentCasingInFileNames": true, - } -} diff --git a/app-vite/types/configuration/build.d.ts b/app-vite/types/configuration/build.d.ts index 21eebb08f3a..0b45f34bb49 100644 --- a/app-vite/types/configuration/build.d.ts +++ b/app-vite/types/configuration/build.d.ts @@ -1,6 +1,7 @@ import { Plugin, UserConfig as ViteUserConfig } from "vite"; import { Options as VuePluginOptions } from "@vitejs/plugin-vue" import { QuasarHookParams } from "./conf"; +import { CompilerOptions, TypeAcquisition } from "typescript"; interface HtmlMinifierOptions { caseSensitive?: boolean; @@ -43,6 +44,30 @@ interface HtmlMinifierOptions { useShortDoctype?: boolean; } +// TSConfig type is adapted from https://github.com/unjs/pkg-types/blob/0bec64641468c9560dea95da2cff502ea8118286/src/types/tsconfig.ts +type StripEnums> = { + [K in keyof T]: T[K] extends boolean + ? T[K] + : T[K] extends string + ? T[K] + : T[K] extends object + ? T[K] + : T[K] extends Array + ? T[K] + : T[K] extends undefined + ? undefined + : any; +}; +interface TSConfig { + compilerOptions?: StripEnums; + exclude?: string[]; + compileOnSave?: boolean; + extends?: string | string[]; + files?: string[]; + include?: string[]; + typeAcquisition?: TypeAcquisition; +} + interface InvokeParams { isClient: boolean; isServer: boolean; @@ -144,6 +169,45 @@ interface QuasarStaticBuildConfiguration { * } */ alias?: { [key: string]: string }; + /** + * Configuration for TypeScript integration. + */ + typescript?: { + /** + * Once your codebase is fully using TypeScript and all team members are comfortable with it, + * you can set this to `true` to enforce stricter type checking. + * It is recommended to set this to `true` and use stricter typescript-eslint rules. + * + * It will set the following TypeScript options: + * - "strict": true + * - "allowUnreachableCode": false + * - "allowUnusedLabels": false + * - "noImplicitOverride": true + * - "exactOptionalPropertyTypes": true + * - "noUncheckedIndexedAccess": true + * + * @see https://www.typescriptlang.org/docs/handbook/migrating-from-javascript.html#getting-stricter-checks + */ + strict?: boolean; + + /** + * Extend the generated `.quasar/tsconfig.json` file. + * + * If you don't have dynamic logic, you can directly modify your `tsconfig.json` file instead. + */ + extendTsConfig?: (tsConfig: TSConfig) => void; + + /** + * Generate a shim file for `*.vue` files to process them as plain Vue component instances. + * + * Vue Language Tools VS Code extension can analyze `*.vue` files in a better way, without the shim file. + * So, you can disable the shim file generation and let the extension handle the types. + * + * However, some tools like ESLint can't work with `*.vue` files without the shim file. + * So, if your tooling is not properly working, enable this option. + */ + vueShim?: boolean; + }; /** * Public path of your app. * Use it when your public path is something else, diff --git a/app-vite/types/index.d.ts b/app-vite/types/index.d.ts index faa66435b99..a418b7cd884 100644 --- a/app-vite/types/index.d.ts +++ b/app-vite/types/index.d.ts @@ -1,10 +1,6 @@ -// These imports force TS compiler to evaluate contained declarations -// which by defaults would be ignored because inside node_modules -// and not directly referenced by any file -// These types had to be moved from `quasar` to `@quasar/app-vite` to avoid generating TS errors -// in Vue CLI projects, given that these features are only available for Quasar CLI projects -// TS doesn't allow re-exports into module augmentation, so we were forced to -// manually declare every file as a `quasar` augmentation +// `quasar` package has some Quasar CLI-specific features, e.g. $q.cordova, 'quasar/wrappers', etc. +// Those types should not be available in there when not using Quasar CLI +// So, we augment the `quasar` package with these features from each engine (app-vite, app-webpack) import "./globals"; export * from "./bex"; diff --git a/create-quasar/templates/app/quasar-v2/js-vite-2/BASE/_jsconfig.json b/create-quasar/templates/app/quasar-v2/js-vite-2/BASE/_jsconfig.json index 4abfb6b1b82..a17e05fa70c 100644 --- a/create-quasar/templates/app/quasar-v2/js-vite-2/BASE/_jsconfig.json +++ b/create-quasar/templates/app/quasar-v2/js-vite-2/BASE/_jsconfig.json @@ -1,48 +1,3 @@ { - "compilerOptions": { - "baseUrl": ".", - "paths": { - "#q-app": [ - "node_modules/@quasar/app-vite" - ], - "#q-app/wrappers": [ - "node_modules/@quasar/app-vite/types/app-wrappers.d.ts" - ], - "#q-app/bex/background": [ - "node_modules/@quasar/app-vite/types/bex/entrypoints/background.d.ts" - ], - "#q-app/bex/content": [ - "node_modules/@quasar/app-vite/types/bex/entrypoints/content.d.ts" - ], - "src/*": [ - "src/*" - ], - "app/*": [ - "*" - ], - "components/*": [ - "src/components/*" - ], - "layouts/*": [ - "src/layouts/*" - ], - "pages/*": [ - "src/pages/*" - ], - "assets/*": [ - "src/assets/*" - ], - "boot/*": [ - "src/boot/*" - ], - "stores/*": [ - "src/stores/*" - ] - } - }, - "exclude": [ - "dist", - ".quasar", - "node_modules" - ] + "extends": "./.quasar/tsconfig.json" } diff --git a/create-quasar/templates/app/quasar-v2/ts-vite-2/BASE/_package.json b/create-quasar/templates/app/quasar-v2/ts-vite-2/BASE/_package.json index 8d8cf77929e..c6faacd06d9 100644 --- a/create-quasar/templates/app/quasar-v2/ts-vite-2/BASE/_package.json +++ b/create-quasar/templates/app/quasar-v2/ts-vite-2/BASE/_package.json @@ -12,7 +12,8 @@ <% } %> "test": "echo \"No test specified\" && exit 0", "dev": "quasar dev", - "build": "quasar build" + "build": "quasar build", + "postinstall": "quasar prepare" }, "dependencies": { <% if (preset.axios) { %>"axios": "^1.2.1",<% } %> diff --git a/create-quasar/templates/app/quasar-v2/ts-vite-2/BASE/_tsconfig.json b/create-quasar/templates/app/quasar-v2/ts-vite-2/BASE/_tsconfig.json index 6b2177abff9..a17e05fa70c 100644 --- a/create-quasar/templates/app/quasar-v2/ts-vite-2/BASE/_tsconfig.json +++ b/create-quasar/templates/app/quasar-v2/ts-vite-2/BASE/_tsconfig.json @@ -1,14 +1,3 @@ { - "extends": "@quasar/app-vite/tsconfig-preset", - "compilerOptions": { - "baseUrl": "." - }, - "exclude": [ - "./dist", - "./.quasar", - "./node_modules", - "./src-capacitor", - "./src-cordova", - "./quasar.config.*.temporary.compiled*" - ] + "extends": "./.quasar/tsconfig.json" } diff --git a/create-quasar/templates/app/quasar-v2/ts-vite-2/BASE/quasar.config.ts b/create-quasar/templates/app/quasar-v2/ts-vite-2/BASE/quasar.config.ts index d9c01f0aef1..a52e6aafc4d 100644 --- a/create-quasar/templates/app/quasar-v2/ts-vite-2/BASE/quasar.config.ts +++ b/create-quasar/templates/app/quasar-v2/ts-vite-2/BASE/quasar.config.ts @@ -48,6 +48,12 @@ export default defineConfig((<% if (preset.i18n) { %>ctx<% } else { %>/* ctx */< node: 'node20' }, + typescript: { + strict: true, + vueShim: true + // extendTsConfig(tsConfig) {} + }, + vueRouterMode: 'hash', // available values: 'hash', 'history' // vueRouterBase, // vueDevtools, @@ -82,9 +88,7 @@ export default defineConfig((<% if (preset.i18n) { %>ctx<% } else { %>/* ctx */< include: [ fileURLToPath(new URL('./src/i18n', import.meta.url)) ], }]<% } %><% if (preset.lint) { %><% if (preset.i18n) { %>,<% } %> ['vite-plugin-checker', { - vueTsc: { - tsconfigPath: 'tsconfig.vue-tsc.json' - }, + vueTsc: true, eslint: { lintCommand: 'eslint "./**/*.{js,ts,mjs,cjs,vue}"' } diff --git a/create-quasar/templates/app/quasar-v2/ts-vite-2/BASE/src/App.vue b/create-quasar/templates/app/quasar-v2/ts-vite-2/BASE/src/App.vue index 9219f612279..a340030664c 100644 --- a/create-quasar/templates/app/quasar-v2/ts-vite-2/BASE/src/App.vue +++ b/create-quasar/templates/app/quasar-v2/ts-vite-2/BASE/src/App.vue @@ -3,9 +3,7 @@ <% if (sfcStyle === 'composition-setup') { %> <% } else if (sfcStyle === 'composition' || sfcStyle === 'options') { %> <% } else if (sfcStyle === 'options') { %> <% } else if (sfcStyle === 'composition') { %> <% } else if (sfcStyle === 'options') { %> <% } else if (sfcStyle === 'composition' || sfcStyle === 'options') { %> <% } else if (sfcStyle === 'composition') { %> <% } else if (sfcStyle === 'options') { %>