Skip to content

Commit

Permalink
feat: quasar ui configurable via app.config
Browse files Browse the repository at this point in the history
  • Loading branch information
Maiquu committed Apr 25, 2024
1 parent aea6af7 commit b269497
Show file tree
Hide file tree
Showing 12 changed files with 643 additions and 410 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
},
"dependencies": {
"@nuxt/kit": "^3.11.1",
"defu": "^6.1.4",
"magic-string": "^0.30.8",
"p-memoize": "^7.1.1",
"semver": "^7.6.0"
Expand All @@ -64,7 +65,7 @@
"jsdom": "^24.0.0",
"nuxt": "^3.11.1",
"quasar": "^2.15.2",
"sass": "^1.72.0",
"sass": "^1.75.0",
"typescript": "^5.4.3",
"vite": "^5.2.7",
"vitest": "^1.4.0",
Expand Down
10 changes: 10 additions & 0 deletions playground/app.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineAppConfig } from '#imports'

export default defineAppConfig({
// UI Configuration in `app.config` will override `nuxt.config`
nuxtQuasarCustom: {
brand: {
primary: 'purple',
},
},
})
2 changes: 1 addition & 1 deletion playground/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ function toggleLeftDrawer() {
<q-toolbar>
<q-btn
flat
color="white"
dense
round
icon="menu"
aria-label="Menu"
:glossy="false"
@click="toggleLeftDrawer"
/>

Expand Down
4 changes: 4 additions & 0 deletions playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ export default defineNuxtConfig({
fontIcons: ['material-icons'],
animations: 'all',
},
appConfigKey: 'nuxtQuasarCustom',
config: {
dark: true,
brand: {
primary: '#ff0000',
},
},
components: {
defaults: {
Expand Down
885 changes: 506 additions & 379 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

18 changes: 14 additions & 4 deletions src/module.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { dirname } from 'node:path'
import { addComponent, addImports, addImportsSources, addPlugin, addTemplate, addTypeTemplate, createResolver, defineNuxtModule, resolvePath } from '@nuxt/kit'
import type { ViteConfig } from '@nuxt/schema'
import type { QuasarAnimations, QuasarFonts, QuasarIconSets as QuasarIconSet, QuasarIconSet as QuasarIconSetObject, QuasarLanguageCodes, QuasarPlugins, QuasarUIConfiguration } from 'quasar'
import type { QuasarAnimations, QuasarFonts, QuasarIconSets as QuasarIconSet, QuasarIconSet as QuasarIconSetObject, QuasarLanguageCodes, QuasarPlugins } from 'quasar'
import type { AssetURLOptions } from 'vue/compiler-sfc'
import satisfies from 'semver/functions/satisfies.js'
import { version } from '../package.json'
import { transformDirectivesPlugin } from './plugins/transform/directives'
import type { ModuleContext, QuasarFontIconSet, QuasarImportData, QuasarImports, QuasarSvgIconSet, ResolveFn } from './types'
import type { ModuleContext, QuasarFontIconSet, QuasarImportData, QuasarImports, QuasarSvgIconSet, QuasarUIConfiguration, ResolveFn } from './types'
import { transformScssPlugin } from './plugins/transform/scss'
import { kebabCase, readFileMemoized, readJSON, uniq } from './utils'
import { virtualQuasarEntryPlugin } from './plugins/virtual/entry'
Expand All @@ -15,10 +15,12 @@ import { virtualBrandPlugin } from './plugins/virtual/brand'
import { setupCss } from './setupCss'
import { enableQuietSassWarnings } from './quietSassWarnings'
import { generateTemplateQuasarConfig } from './template/config'
import { generateTemplateComponentsShim } from './template/shims'
import { generateTemplateShims } from './template/shims'

export interface QuasarComponentDefaults {}

export type { QuasarUIConfiguration }

export interface ModuleOptions {
/**
* Would you like to use Quasar's SCSS/Sass variables?
Expand Down Expand Up @@ -55,7 +57,7 @@ export interface ModuleOptions {
**/
plugins?: (keyof QuasarPlugins)[]

config?: Omit<QuasarUIConfiguration, 'lang' | 'capacitor' | 'cordova'>
config?: QuasarUIConfiguration

/**
* Default Language pack used by Quasar
Expand All @@ -76,6 +78,13 @@ export interface ModuleOptions {
*/
autoIncludeIconSet?: boolean

/**
* App Config Key
*
* @default 'nuxtQuasar'
*/
appConfigKey?: string

/**
* When enabled, it provides breakpoint aware versions for all flex (and display) related CSS classes.
*
Expand Down Expand Up @@ -140,6 +149,7 @@ export default defineNuxtModule<ModuleOptions>({
autoIncludeIconSet: true,
cssAddon: false,
sassVariables: false,
appConfigKey: 'nuxtQuasar',
components: {
defaults: {},
deepDefaults: false,
Expand Down
106 changes: 84 additions & 22 deletions src/runtime/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type { IncomingMessage, ServerResponse } from 'node:http'
import { Quasar, useQuasar } from 'quasar'
import type { UseHeadInput } from 'unhead'
import type { QVueGlobals, QuasarIconSet, QuasarLanguage } from 'quasar'
import type { App as VueApp } from 'vue'
import { computed, defineNuxtPlugin, ref, useHead } from '#imports'
import { componentsWithDefaults, quasarNuxtConfig } from '#build/quasar.config.mjs'
import { defuFn } from 'defu'
import type { QuasarUIConfiguration } from '../types'
import { computed, defineNuxtPlugin, reactive, useAppConfig, useHead, watch } from '#imports'
import { appConfigKey, componentsWithDefaults, quasarNuxtConfig } from '#build/quasar.config.mjs'

interface QuasarPluginClientContext {
parentApp: VueApp<any>
Expand Down Expand Up @@ -46,6 +49,18 @@ interface QuasarClientPlugin {
install(context: QuasarPluginClientContext): void
}

function getUpdatedDefaults<T extends object>(cfg: T, prevCfg: T) {
const prevKeys = Object.keys(prevCfg)
return {
...Object.fromEntries(prevKeys.map(k => [k, undefined])),
...cfg,
}
}

function getPrimaryColor() {
return getComputedStyle(document.body).getPropertyValue('--q-primary').trim()
}

function omit<T extends object, K extends keyof T & string>(object: T, keys: K[]): Omit<T, K>
function omit(object: Record<string, any>, keys: string[]): Record<string, any> {
return Object.keys(object).reduce((output, key) => {
Expand All @@ -57,51 +72,60 @@ function omit(object: Record<string, any>, keys: string[]): Record<string, any>
}

export default defineNuxtPlugin((nuxt) => {
const { lang, iconSet, plugins, config = {}, components } = quasarNuxtConfig
const quasarAppConfig = useAppConfig()[appConfigKey] as QuasarUIConfiguration
const { lang, iconSet, plugins, components } = quasarNuxtConfig
let ssrContext: { req: IncomingMessage; res: ServerResponse } | undefined
let quasarProxy: QuasarServerPlugin | QuasarClientPlugin
// Since brand used in `nuxt.config` is pushed to `nuxt.options.css`, we exclude it here
let config = defuFn(quasarAppConfig, omit(quasarNuxtConfig.config, ['brand']))

if (import.meta.server) {
const bodyClasses = ref('')
const htmlAttrs = ref('')
const BRAND_RE = /--q-(?:.+?):(?:.+?);/g
const meta = reactive({
bodyClasses: '',
htmlAttrs: '',
endingHeadTags: '',
})
type MetaKey = keyof typeof meta
const htmlAttrsRecord = computed(() =>
Object.fromEntries(
htmlAttrs.value
meta.htmlAttrs
.split(' ')
.map(attr => attr.split('=')),
),
)
// NOTE: Quasar currently only appends `endingHeadTags` with brand variables, this may break in future
const bodyStyles = computed(() => {
return [...meta.endingHeadTags.matchAll(BRAND_RE)]
.map(match => match[0])
.join('')
})

useHead(
computed(() => ({
bodyAttrs: {
class: bodyClasses.value,
class: meta.bodyClasses,
style: bodyStyles.value,
},
htmlAttrs: htmlAttrsRecord.value,
})),
} as UseHeadInput<any>)),
)
ssrContext = {
req: nuxt.ssrContext!.event.node.req,
res: nuxt.ssrContext!.event.node.res,
}
quasarProxy = {
install({ ssrContext }) {
bodyClasses.value = ssrContext._meta.bodyClasses
htmlAttrs.value = ssrContext._meta.htmlAttrs
meta.bodyClasses = ssrContext._meta.bodyClasses
meta.htmlAttrs = ssrContext._meta.htmlAttrs
meta.endingHeadTags = ssrContext._meta.endingHeadTags
ssrContext._meta = new Proxy({} as Record<string | symbol, any>, {
get(target, key) {
if (key === 'bodyClasses') {
return bodyClasses.value
} else if (key === 'htmlAttrs') {
return htmlAttrs.value
} else {
return target[key]
}
return meta[key as MetaKey] ?? target[key]
},
set(target, key, value) {
if (key === 'bodyClasses') {
bodyClasses.value = value
} else if (key === 'htmlAttrs') {
htmlAttrs.value = value
if (typeof meta[key as MetaKey] === 'string') {
meta[key as MetaKey] = value
} else {
target[key] = value
}
Expand All @@ -127,7 +151,7 @@ export default defineNuxtPlugin((nuxt) => {
quasarProxy,
...plugins,
},
config: omit(config, ['brand']),
config,
// @ts-expect-error Private Argument
}, ssrContext)

Expand All @@ -150,6 +174,44 @@ export default defineNuxtPlugin((nuxt) => {
}
}

if (import.meta.dev && import.meta.client) {
watch(
() => quasarAppConfig,
(newAppConfig) => {
const prevConfig = config
config = defuFn(newAppConfig, quasarNuxtConfig.config)
quasar.addressbarColor?.set(config.addressbarColor || getPrimaryColor())
const modifiedBrand = getUpdatedDefaults(
config.brand || {},
prevConfig.brand || {},
)
for (const [name, color] of Object.entries(modifiedBrand)) {
if (!color) {
document.body.style.removeProperty(`--q-${name}`)
} else {
document.body.style.setProperty(`--q-${name}`, color)
}
}
if (prevConfig.dark !== config.dark) {
quasar.dark.set(config.dark || false)
}
quasar.loading?.setDefaults(getUpdatedDefaults(
config.loading || {},
prevConfig.loading || {},
))
quasar.loadingBar?.setDefaults(getUpdatedDefaults(
config.loadingBar || {},
prevConfig.loadingBar || {},
))
plugins.Notify?.setDefaults(getUpdatedDefaults(
config.loadingBar || {},
prevConfig.loadingBar || {},
))
},
{ deep: true },
)
}

return {
provide: {
q: quasar,
Expand Down
1 change: 0 additions & 1 deletion src/setupCss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { uniq } from './utils'
* @param options
*/
export function setupCss(css: string[], options: ModuleOptions) {
// TODO: Deprecate writing `quasar/brand` to css array
const brand = options.config?.brand || {}
if (!css.includes(quasarBrandPath) && Object.keys(brand).length) {
css.unshift(quasarBrandPath)
Expand Down
2 changes: 2 additions & 0 deletions src/shims.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ declare module '#build/quasar.config.mjs' {
Notify?: Notify
}
}

export const appConfigKey: string
}

declare module 'quasar/src/composables/use-quasar.js' {
Expand Down
2 changes: 2 additions & 0 deletions src/template/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ ${when(componentsWithDefaults.length, () => `import { ${componentsWithDefaults}
export const componentsWithDefaults = { ${componentsWithDefaults} }
export const appConfigKey = ${JSON.stringify(context.options.appConfigKey)}
export const quasarNuxtConfig = {
${when(lang, 'lang,')}
${typeof iconSet === 'string'
Expand Down
11 changes: 10 additions & 1 deletion src/template/shims.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { moduleName } from '../constants'
import type { ModuleContext } from '../types'

export async function generateTemplateComponentsShim(context: Omit<ModuleContext, 'mode'>): Promise<string> {
export async function generateTemplateShims(context: Omit<ModuleContext, 'mode'>): Promise<string> {
const componentNames = context.imports.components.map(c => c.name)
return `\
type KeysMatching<T, V> = {
Expand All @@ -25,6 +25,15 @@ declare module '${moduleName}' {
}
}
declare module 'nuxt/schema' {
interface AppConfigInput {
[${JSON.stringify(context.options.appConfigKey)}]?: import("nuxt-quasar-ui").QuasarUIConfiguration
}
interface AppConfig {
[${JSON.stringify(context.options.appConfigKey)}]?: import("nuxt-quasar-ui").QuasarUIConfiguration
}
}
export {}
`
}
9 changes: 8 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { QuasarIconSets as QuasarIconSet } from 'quasar'
import type { QuasarIconSets as QuasarIconSet, QuasarUIConfiguration as _QuasarUIConfiguration } from 'quasar'
import type { ModuleOptions } from './module'

type ExtractFont<T extends string> = T extends `svg-${string}` ? never : T
Expand All @@ -22,6 +22,13 @@ export interface QuasarImportData {
path: string
}

export type QuasarUIConfiguration = Omit<_QuasarUIConfiguration, 'lang' | 'capacitor' | 'cordova'> & {
addressbarColor?: string
brand?: {
'dark-page'?: string
}
}

export interface ModuleContext {
ssr: boolean
dev: boolean
Expand Down

0 comments on commit b269497

Please sign in to comment.