@quasar/app-webpack
Version:
Quasar Framework App CLI with Webpack
272 lines (224 loc) • 8.5 kB
JavaScript
const { isAbsolute, join, relative } = require('node:path')
const { statSync } = require('node:fs')
const fse = require('fs-extra')
const { cliPkg, resolveToCliDir } = require('./utils/cli-runtime.js')
const { isModeInstalled } = require('./modes/modes-utils.js')
const { getPackagePath } = require('./utils/get-package-path.js')
const qAppPaths = (() => {
const exportsRE = /^\./
const dTsRE = /\.d\.ts$/
const localMap = {}
for (const key in cliPkg.exports) {
const localMapKey = key.replace(exportsRE, '#q-app')
const value = cliPkg.exports[ key ]
if (Object(value) === value) {
if (value.types) {
localMap[ localMapKey ] = resolveToCliDir(value.types)
}
}
else if (typeof value === 'string') {
if (dTsRE.test(value)) {
localMap[ localMapKey ] = resolveToCliDir(value)
}
}
}
return localMap
})()
// We generate all the files for JS projects as well, because they provide
// better autocomplete and type checking in the IDE.
module.exports.generateTypes = function generateTypes (quasarConf) {
const { appPaths } = quasarConf.ctx
const tsConfigDir = appPaths.resolve.app('.quasar')
const fsUtils = {
tsConfigDir,
writeFileSync (filename, content) {
const file = join(tsConfigDir, filename)
// Avoid unnecessary writes which will trigger esbuild
// to recompile & apply quasar.config file changes
if (
fse.existsSync(file) === false
|| fse.readFileSync(file, 'utf-8') !== content
) {
fse.writeFileSync(file, content, 'utf-8')
}
}
}
fse.ensureDirSync(tsConfigDir)
generateTsConfig(quasarConf, fsUtils)
writeFeatureFlags(quasarConf, fsUtils)
writeDeclarations(quasarConf, fsUtils)
}
/**
* @param {import('../types/configuration/conf').QuasarConf} quasarConf
*/
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)
)
}
// 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
*/
function writeFeatureFlags (quasarConf, fsUtils) {
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) {
if (isModeInstalled(appPaths, modeName)) {
featureFlags.add(modeName)
}
}
const flagDefinitions = Array.from(featureFlags)
.map(flag => `${ flag }: true;`)
.join('\n ')
const contents = featureFlagsTemplate.replace(
'__INJECTION_POINT__',
flagDefinitions || '// no feature flags'
)
fsUtils.writeFileSync('feature-flags.d.ts', contents)
}
/*
Load app-webpack'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 */
/// <reference types="@quasar/app-webpack" />
/// <reference types="vite/client" />
`
// 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;
}
`
const piniaTemplate = `/* eslint-disable */
import { Router } from 'vue-router';
declare module 'pinia' {
export interface PiniaCustomProperties {
readonly router: Router;
}
}
`
/**
* @param {import('../types/configuration/conf').QuasarConf} quasarConf
*/
function writeDeclarations (quasarConf, fsUtils) {
fsUtils.writeFileSync('quasar.d.ts', declarationsTemplate)
if (quasarConf.build.typescript.vueShim) {
fsUtils.writeFileSync('shims-vue.d.ts', vueShimsTemplate)
}
const { hasStore, storePackage } = quasarConf.metaConf
if (hasStore && storePackage === 'pinia') {
fsUtils.writeFileSync('pinia.d.ts', piniaTemplate)
}
}