@quasar/app-webpack
Version:
Quasar Framework App CLI with Webpack
448 lines (360 loc) • 12.7 kB
JavaScript
const { relative, resolve } = require('node:path')
const { pathToFileURL } = require('node:url')
const fse = require('fs-extra')
const { isBinaryFileSync: isBinary } = require('isbinaryfile')
const compileTemplate = require('lodash/template.js')
const { log, warn, fatal } = require('../utils/logger.js')
const { IndexAPI } = require('./api-classes/IndexAPI.js')
const { InstallAPI } = require('./api-classes/InstallAPI.js')
const { UninstallAPI } = require('./api-classes/UninstallAPI.js')
const { PromptsAPI } = require('./api-classes/PromptsAPI.js')
const { getPackagePath } = require('../utils/get-package-path.js')
async function promptOverwrite ({ targetPath, options, ctx }) {
const choices = [
{ name: 'Overwrite', value: 'overwrite' },
{ name: 'Overwrite all', value: 'overwriteAll' },
{ name: 'Skip (might break extension)', value: 'skip' },
{ name: 'Skip all (might break extension)', value: 'skipAll' }
]
const { default: inquirer } = await import('inquirer')
return await inquirer.prompt([ {
name: 'action',
type: 'list',
message: `Overwrite "${ relative(ctx.appPaths.appDir, targetPath) }"?`,
choices: options !== void 0
? choices.filter(choice => options.includes(choice.value))
: choices,
default: 'overwrite'
} ])
}
async function renderFile ({ sourcePath, targetPath, rawCopy, scope, overwritePrompt }, ctx) {
if (overwritePrompt === true && fse.existsSync(targetPath)) {
const answer = await promptOverwrite({
targetPath,
options: [ 'overwrite', 'skip' ],
ctx
})
if (answer.action === 'skip') return
}
fse.ensureFileSync(targetPath)
if (rawCopy || isBinary(sourcePath)) {
fse.copyFileSync(sourcePath, targetPath)
}
else {
const rawContent = fse.readFileSync(sourcePath, 'utf-8')
const template = compileTemplate(rawContent, { interpolate: /<%=([\s\S]+?)%>/g })
fse.writeFileSync(targetPath, template(scope), 'utf-8')
}
}
async function renderFolders ({ source, rawCopy, scope }, ctx) {
let overwrite
const { globSync } = require('tinyglobby')
const files = globSync([ '**/*' ], { cwd: source })
for (const rawPath of files) {
const targetRelativePath = rawPath.split('/').map(name => {
// dotfiles are ignored when published to npm, therefore in templates
// we need to use underscore instead (e.g. "_gitignore")
if (name.charAt(0) === '_' && name.charAt(1) !== '_') {
return `.${ name.slice(1) }`
}
if (name.charAt(0) === '_' && name.charAt(1) === '_') {
return `${ name.slice(1) }`
}
return name
}).join('/')
const targetPath = ctx.appPaths.resolve.app(targetRelativePath)
const sourcePath = resolve(source, rawPath)
if (overwrite !== 'overwriteAll' && fse.existsSync(targetPath)) {
if (overwrite === 'skipAll') {
continue
}
else {
const answer = await promptOverwrite({ targetPath, ctx })
if (answer.action === 'overwriteAll') {
overwrite = 'overwriteAll'
}
else if (answer.action === 'skipAll') {
overwrite = 'skipAll'
continue
}
else if (answer.action === 'skip') {
continue
}
}
}
await renderFile({ sourcePath, targetPath, rawCopy, scope }, ctx)
}
}
module.exports.AppExtensionInstance = class AppExtensionInstance {
#ctx
#appExtJson
extId
packageFullName
packageName
#isInstalled = null
constructor ({ extName, ctx, appExtJson }) {
this.#ctx = ctx
this.#appExtJson = appExtJson
if (extName.charAt(0) === '@') {
const slashIndex = extName.indexOf('/')
if (slashIndex === -1) {
fatal(`Invalid Quasar App Extension name: "${ extName }"`)
}
this.packageFullName = extName.substring(0, slashIndex + 1)
+ 'quasar-app-extension-'
+ extName.substring(slashIndex + 1)
this.packageName = '@' + this.#stripVersion(this.packageFullName.substring(1))
this.extId = '@' + this.#stripVersion(extName.substring(1))
}
else {
this.packageFullName = `quasar-app-extension-${ extName }`
this.packageName = this.#stripVersion(this.packageFullName)
this.extId = this.#stripVersion(extName)
}
}
get isInstalled () {
if (this.#isInstalled === null) {
this.#loadPackageInfo()
}
return this.#isInstalled
}
#loadPackageInfo () {
const { appDir } = this.#ctx.appPaths
try {
const resolvedPath = (
// Try `import('quasar-app-extension-foo/package.json')`. It might not work if using `package.json > exports` and the file is not listed
getPackagePath(
`${ this.packageFullName }/package.json`,
appDir
)
// Try `import('quasar-app-extension-foo')` to see if the root import is available (through `package.json > exports` or `package.json > main`)
|| getPackagePath(
this.packageFullName,
appDir
)
// As a last resort, try to resolve the index script. By not doing this as the only/first option, we can give a more precise error message
// if the package is installed but the index script is missing
|| this.#getScriptPath('index')
)
if (resolvedPath !== void 0) {
this.#isInstalled = true
return
}
}
catch (_) {}
this.#isInstalled = false
}
async install (skipPkgInstall) {
if (/quasar-app-extension-/.test(this.extId)) {
this.extId = this.extId.replace('quasar-app-extension-', '')
log(
`When using an extension, "quasar-app-extension-" is added automatically. Just run "quasar ext add ${
this.extId
}"`
)
}
log(`${ skipPkgInstall ? 'Invoking' : 'Installing' } "${ this.extId }" Quasar App Extension`)
log()
// verify if already installed
if (skipPkgInstall === true) {
if (!this.isInstalled) {
fatal(`Tried to invoke App Extension "${ this.extId }" but its npm package is not installed`)
}
}
else if (this.isInstalled) {
const { default: inquirer } = await import('inquirer')
const answer = await inquirer.prompt([ {
name: 'reinstall',
type: 'confirm',
message: 'Already installed. Reinstall?',
default: false
} ])
if (!answer.reinstall) return
}
if (skipPkgInstall !== true) {
await this.#installPackage()
}
const prompts = await this.#getScriptPrompts()
this.#appExtJson.set(this.extId, prompts)
// run extension install
const hooks = await this.#runInstallScript(prompts)
log(`Quasar App Extension "${ this.extId }" successfully installed.`)
log()
if (hooks && hooks.exitLog.length > 0) {
hooks.exitLog.forEach(msg => {
console.log(msg)
})
console.log()
}
}
async uninstall (skipPkgUninstall) {
log(`${ skipPkgUninstall ? 'Uninvoking' : 'Uninstalling' } "${ this.extId }" Quasar App Extension`)
log()
// verify if already installed
if (skipPkgUninstall === true) {
if (!this.isInstalled) {
fatal(`Tried to uninvoke App Extension "${ this.extId }" but there's no npm package installed for it.`)
}
}
else if (!this.isInstalled) {
warn(`Quasar App Extension "${ this.packageName }" is not installed...`)
return
}
const prompts = this.getPrompts()
const hooks = await this.#runUninstallScript(prompts)
this.#appExtJson.remove(this.extId)
if (skipPkgUninstall !== true) {
await this.#uninstallPackage()
}
log(`Quasar App Extension "${ this.extId }" successfully removed.`)
log()
if (hooks && hooks.exitLog.length > 0) {
hooks.exitLog.forEach(msg => {
console.log(msg)
})
console.log()
}
}
async run () {
if (!this.isInstalled) {
warn(`Quasar App Extension "${ this.extId }" is missing...`)
process.exit(1, 'ext-missing')
}
const script = await this.#getScript('index', true)
const api = new IndexAPI({
ctx: this.#ctx,
extId: this.extId,
prompts: this.getPrompts()
}, this.#appExtJson)
log(`Running "${ this.extId }" Quasar App Extension...`)
await script(api)
return api.__getHooks(this.#appExtJson)
}
#stripVersion (packageFullName) {
const index = packageFullName.indexOf('@')
return index > -1
? packageFullName.substring(0, index)
: packageFullName
}
getPrompts () {
return this.#appExtJson.getPrompts(this.extId)
}
async #getScriptPrompts () {
const getPromptsObject = await this.#getScript('prompts')
if (typeof getPromptsObject !== 'function') return {}
const api = new PromptsAPI({
ctx: this.#ctx,
extId: this.extId
}, this.#appExtJson)
const { default: inquirer } = await import('inquirer')
const prompts = await inquirer.prompt(
await getPromptsObject(api)
)
console.log()
return prompts
}
async #installPackage () {
const nodePackager = await this.#ctx.cacheProxy.getModule('nodePackager')
nodePackager.installPackage(this.packageFullName, { isDevDependency: true })
this.#loadPackageInfo()
}
async #uninstallPackage () {
const nodePackager = await this.#ctx.cacheProxy.getModule('nodePackager')
nodePackager.uninstallPackage(this.packageFullName)
this.#isInstalled = false
}
#scriptsTargetFolderList = [ 'dist', 'src' ]
#scriptsExtensionList = [ '', '.js', '.mjs', '.cjs' ]
/**
* Returns the absolute path to the script file.
*
* It uses Node import resolution rather than filesystem-based resolution, so `package.json > exports` will affect the result, if exists.
* It will try to resolve the file with no extension, then with `.js`, `.mjs` and `.cjs`.
* For each extension, it will first check the `dist` directory, then the `src` directory.
* To give some examples to the import resolution:
* - `quasar-app-extension-foo/dist/index`
* - `quasar-app-extension-foo/dist/index.js`
*
* This allows to use preprocessors (e.g. TypeScript) for all AE files (including index, install, uninstall, etc. AE scripts)
*/
#getScriptPath (scriptName) {
if (this.isInstalled === false) return
for (const ext of this.#scriptsExtensionList) {
for (const folder of this.#scriptsTargetFolderList) {
const path = getPackagePath(
`${ this.packageFullName }/${ folder }/${ scriptName }${ ext }`,
this.#ctx.appPaths.appDir
)
if (path !== void 0) return path
}
}
}
async #getScript (scriptName, fatalError) {
const scriptPath = this.#getScriptPath(scriptName)
if (!scriptPath) {
if (fatalError) {
fatal(`App Extension "${ this.extId }" has missing ${ scriptName } script...`)
}
return
}
let fn
try {
const { default: defaultFn } = await import(
pathToFileURL(scriptPath)
)
fn = defaultFn
}
catch (err) {
console.error(err)
if (fatalError) {
fatal(`App Extension "${ this.extId }" > ${ scriptName } script has thrown the error from above.`)
}
}
if (typeof fn !== 'function') {
if (fatalError) {
fatal(`App Extension "${ this.extId }" > ${ scriptName } script does not have a default export as a function...`)
}
return
}
return fn
}
async #runInstallScript (prompts) {
const script = await this.#getScript('install')
if (typeof script !== 'function') return
log('Running App Extension install script...')
const api = new InstallAPI({
ctx: this.#ctx,
extId: this.extId,
prompts
}, this.#appExtJson)
await script(api)
const hooks = api.__getHooks(this.#appExtJson)
if (hooks.renderFolders.length > 0) {
for (const entry of hooks.renderFolders) {
await renderFolders(entry, this.#ctx)
}
}
if (hooks.renderFiles.length > 0) {
for (const entry of hooks.renderFiles) {
await renderFile(entry, this.#ctx)
}
}
if (api.__getNodeModuleNeedsUpdate(this.#appExtJson) === true) {
const nodePackager = await this.#ctx.cacheProxy.getModule('nodePackager')
nodePackager.install()
}
return hooks
}
async #runUninstallScript (prompts) {
const script = await this.#getScript('uninstall')
if (typeof script !== 'function') return
log('Running App Extension uninstall script...')
const api = new UninstallAPI({
ctx: this.#ctx,
extId: this.extId,
prompts
}, this.#appExtJson)
await script(api)
return api.__getHooks(this.#appExtJson)
}
}