This guide is for when you want to create what essentially is a “starter kit” that adds stuff (/quasar.config file configuration, folders, files, CLI hooks) on top of the official starter kit. This allows you to have multiple projects sharing a common structure/logic (and only one package to manage them rather than having to change all projects individually to match your common pattern), and also allows you to share all this with the community.
TIP
In order for creating an App Extension project folder, please first read the Development Guide > Introduction.
Full Example
To see an example of what we will build, head over to MyStarterKit full example, which is a github repo with the App Extension that we are building on this page.
We’ll be creating an example App Extension which does the following:
- it prompts the user what features it wants this App Extension to install
- renders (copies) files into the hosting folder, according to the answers he gave
- it extends the /quasar.config file
- it extends the Vite configuration
- it uses an App Extension hook (onPublish)
- it removes the added files when the App Extension gets uninstalled
- it uses the prompts to define what the App Extension does
The structure
For the intents of this example, we’ll be creating the following folder structure:
The Prompts script
The prompts script below uses @clack/prompts, but you can replace it with anything if you want to. Just remember to install the prompt package that you are using (in /ae, into dependencies, since it will be a runtime dependency).
This script will be called upon AE installation/invoking procedure.
/**
* Quasar App Extension prompts script
* https://quasar.dev/app-extensions/development-guide/prompts-api
*/
import { definePromptsScript } from '#q-app'
import { intro, outro, confirm, text, group, cancel } from '@clack/prompts'
export default definePromptsScript(async (/* api */) => {
intro('Starter Kit App Extension')
const answers = await group(
{
serviceA: () => confirm({ message: 'Do you want service "A"?' }),
serviceB: () => confirm({ message: 'Do you want service "B"?' }),
productName: ({ results }) => {
if (!results.serviceB) return
return text({
message: 'Since you want service "B", what is the Product Name?',
initialValue: 'MyProduct',
validate(value) {
if (value.length === 0) return 'Please enter a product name'
}
})
},
publishService: () =>
confirm({
message: 'Do you want the publishing service?',
initialValue: true
})
},
{
// On Cancel callback that wraps the group
// So if the user cancels one of the prompts in the group this function will be called
onCancel: (/* { results } */) => {
cancel('Operation cancelled.')
process.exit(0)
}
}
)
outro('Thanks for answering the questions!')
return answers
})The Install script
The install script below is only rendering files into the hosted app. Notice the /src/templates folder above, where we decided to keep these templates.
import { defineInstallScript } from '#q-app'
export default defineInstallScript(api => {
// (Optional!)
// Quasar compatibility check; you may need
// hard dependencies, as in a minimum version of the "quasar"
// package or a minimum version of Quasar App CLI
api.compatibleWith('quasar', '^2.0.0')
api.compatibleWith('@quasar/app-vite', '^3.0.0-rc.1')
// We render some files into the hosting project
if (api.prompts.serviceA) {
api.render('./templates/serviceA')
}
if (api.prompts.serviceB) {
// we supply interpolation variables
// to the template
api.render('./templates/serviceB', {
productName: api.prompts.productName
})
}
// we always render the following template:
api.render('./templates/common-files')
})Notice that we use the prompts to decide what to render into the hosting project. Furthermore, if the user has selected “service B”, then we’ll also have a “productName” that we can use when we render the service B’s file.
The Index script
We do a few things in the index script, like extending the /quasar.config file, hooking into one of the many Index API hooks (onPublish in this case):
import { defineIndexScript } from '#q-app'
export default defineIndexScript(api => {
// (Optional!)
// Quasar compatibility check; you may need
// hard dependencies, as in a minimum version of the "quasar"
// package or a minimum version of Quasar App CLI
api.compatibleWith('quasar', '^2.0.0')
api.compatibleWith('@quasar/app-vite', '^3.0.0-rc.1')
// Here we extend the /quasar.config file;
// (extendQuasarConf() will be defined later in this tutorial, continue reading)
api.extendQuasarConf(extendQuasarConf)
// Here we register the onPublish hook,
// only if user answered that he wants the publishing service
if (api.prompts.publishService) {
// onPublish() will be defined later in this tutorial, continue reading
api.onPublish(onPublish)
}
api.extendViteConf(extendVite)
// there's lots more hooks that you can use...
})Here’s an example of extendQuasarConf definition:
function extendQuasarConf(conf, api) {
conf.extras.push('ionicons-v4')
conf.framework.iconSet = 'ionicons-v4'
//
// We register a boot file. User does not need to tamper with it,
// so we keep it into the App Extension code:
//
// make sure my-ext boot file is registered
conf.boot.push(
'~quasar-app-extension-my-starter-kit/src/runtime/my-starter-kit-boot.js'
)
}The onPublish function:
function onPublish(api, { arg, distDir }) {
// this hook is called when "quasar build --publish" is called
// your publish logic here...
console.log('We should publish now. But maybe later? :)')
// are we trying to publish a Cordova app?
if (api.ctx.modeName === 'cordova') {
// do something
}
}The extendVite function:
function extendVite(viteConf, { isClient, isServer }, api) {
// viteConf is a Vite config object generated by Quasar CLI
}The Uninstall script
When the App Extension gets uninstall, we need to do some cleanup. But beware what you delete from the app-space! Some files might still be needed. Proceed with extreme care, if you decide to have an uninstall script.
import { defineUninstallScript } from '#q-app'
// we PNPM added it to our App Extension,
// so we can import the following:
import rimraf from 'rimraf'
export default defineUninstallScript(api => {
// Careful when you remove folders!
// You don't want to delete files that are still needed by the Project,
// or files that are not owned by this app extension.
// Here, we could also remove the /src/services folder altogether,
// but what if the user has added other files into this folder?
if (api.prompts.serviceA) {
// we added it on install, so we remove it
rimraf.sync(api.resolve.src('services/serviceA.js'))
}
if (api.prompts.serviceB) {
// we added it on install, so we remove it
rimraf.sync(api.resolve.src('services/serviceB.js'))
}
// we added it on install, so we remove it
rimraf.sync(api.resolve.app('some-folder'))
// warning... we've added this folder, but what if the
// developer added more files into this folder???
})Notice that we are requesting rimraf npm package. This means that we pnpm added it into our App Extension project (the /ae folder). This is here as example so that you are aware that the dependencies that you use need to be supplied by your AE.
Alternatively, you can use api.removePath.