UNPKG

@quasar/app-webpack

Version:

Quasar Framework App CLI with Webpack

448 lines (360 loc) 12.7 kB
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) } }