UNPKG

@quasar/app-vite

Version:

Quasar Framework App CLI with Vite

443 lines (353 loc) 11.7 kB
import { join, relative, resolve, dirname } from 'node:path' import { pathToFileURL } from 'node:url' import fse from 'fs-extra' import inquirer from 'inquirer' import { isBinaryFileSync as isBinary } from 'isbinaryfile' import compileTemplate from 'lodash/template.js' import { log, warn, fatal } from '../utils/logger.js' import { IndexAPI } from './api-classes/IndexAPI.js' import { InstallAPI } from './api-classes/InstallAPI.js' import { UninstallAPI } from './api-classes/UninstallAPI.js' import { PromptsAPI } from './api-classes/PromptsAPI.js' import { getPackagePath } from '../utils/get-package-path.js' const scriptsExtensionList = [ '', '.js', '.mjs', '.cjs' ] const scriptsTargetFolderList = [ 'dist', 'src' ] function getPackageScriptPath (packageFullName, scriptName, dir) { for (const ext of scriptsExtensionList) { for (const folder of scriptsTargetFolderList) { const path = getPackagePath( join(packageFullName, folder, `${ scriptName }${ ext }`), dir ) if (path !== void 0) return path } } } 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' } ] 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 } = await import('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) } } export class AppExtensionInstance { #ctx #appExtJson extId packageFullName packageName #isInstalled = null #packagePath = 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 packagePath = ( getPackagePath( join(this.packageFullName, 'package.json'), appDir ) || getPackagePath( this.packageFullName, appDir ) || getPackageScriptPath( this.packageFullName, 'index', appDir ) ) if (packagePath !== void 0) { this.#isInstalled = true this.#packagePath = dirname(packagePath) return } } catch (_) {} this.#markAsNotInstalled() } #markAsNotInstalled () { this.#isInstalled = false this.#packagePath = 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() if (skipPkgInstall !== true) { await this.#installPackage() } else if (!this.isInstalled) { fatal(`Tried to invoke App Extension "${ this.extId }" but its npm package is not installed`) } 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 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 }) } async #uninstallPackage () { const nodePackager = await this.#ctx.cacheProxy.getModule('nodePackager') nodePackager.uninstallPackage(this.packageFullName) this.#markAsNotInstalled() } /** * Returns the file absolute path. If the file cannot be found into the default 'src' folder, * searches it into the `dist` folder. * * This allows to use preprocessors (eg. TypeScript) for all AE files (even index, install and other Quasar-specific scripts) * as long as the corresponding file isn't available into the `src` folder, making the feature opt-in */ #getScriptFile (scriptName) { if (this.isInstalled === false) return return getPackageScriptPath( this.packageFullName, scriptName, this.#packagePath ) } async #getScript (scriptName, fatalError) { const script = this.#getScriptFile(scriptName) if (!script) { if (fatalError) { fatal(`App Extension "${ this.extId }" has missing ${ scriptName } script...`) } return } let fn try { const { default: defaultFn } = await import( pathToFileURL(script) ) 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) } }