UNPKG

yeoman-generator

Version:

Rails-inspired generator system that provides scaffolding for your apps

288 lines (287 loc) • 14.2 kB
/* eslint max-params: [1, 6] */ import assert from 'node:assert'; import chalk from 'chalk'; import { splitPathOptions } from '../util/path-options.js'; /** * The context of a template rendered with the `templateContext` feature `'throw'`: reading a property of `this` throws. */ const createDisabledTemplateContext = (templatePath) => new Proxy(Object.create(null), { get(_target, property) { if (typeof property === 'symbol') { return; } throw new Error(`Template ${templatePath} reads this.${property}: the generator is not the context of its templates, pass it in the template data`); }, }); // The properties each generator warned a template reads from `this`, to warn once. const warnedTemplateContexts = new WeakMap(); /** * The context of a template rendered with the `templateContext` feature `'warn'`: the generator, warning once by * template and property read from `this`. A method keeps the generator as `this`, for its private fields. */ const createWarningTemplateContext = (generator, templatePath) => new Proxy(generator, { get(target, property) { const value = Reflect.get(target, property, target); if (typeof property !== 'symbol') { let warned = warnedTemplateContexts.get(target); if (!warned) { warned = new Set(); warnedTemplateContexts.set(target, warned); } const key = `${templatePath}#${property}`; if (!warned.has(key)) { warned.add(key); const message = `${chalk.yellow('(!)')} Template ${templatePath} reads this.${property}: the generator will not be the context of its templates in the next major, pass it in the template data (the templateContext feature 'throw' renders without it)`; // A generator without an environment adapter has no log. if (typeof target.log === 'function') { target.log(message); } else { console.warn(message); } } } return typeof value === 'function' ? value.bind(target) : value; }, }); /** * The context of a template, by the `templateContext` feature. */ const getTemplateContext = (generator, templatePath) => { const { templateContext = 'generator' } = generator.features; switch (templateContext) { case 'warn': { return createWarningTemplateContext(generator, templatePath); } case 'throw': { return createDisabledTemplateContext(templatePath); } case 'none': { return undefined; } default: { return generator; } } }; /** * A replacer is a function, an array or null, so a plain object in its position is the options object. */ const isWriteJSONOptions = (value) => typeof value === 'object' && value !== null && !Array.isArray(value); const writeJSONOptionsToPositional = ({ replacer, space, ...options }) => [ replacer, space, options, ]; /** * Merge the generator's `editorMetadata` into mem-fs-editor options, an explicit `metadata` option takes precedence. */ function withEditorMetadata(generator, options) { const { editorMetadata } = generator; if (!editorMetadata) { return options; } if (options && 'isFile' in options) { // Deprecated `write(filepath, contents, stat)` signature. return { stat: options, metadata: editorMetadata }; } return { ...options, metadata: { ...editorMetadata, ...options?.metadata } }; } export class FsMixin { fs; readTemplate(...args) { const [filepath, options, ...remaining] = args; const [pathOptions, readOptions] = splitPathOptions(options); return this.fs.read(this.templatePath(filepath, pathOptions), readOptions, ...remaining); } /** * Copy file from templates folder to destination folder. * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}. * Shortcut for this.fs!.copy(this.templatePath(from), this.destinationPath(to)) */ copyTemplate(...args) { const [from, to, options, ...remaining] = args; const [pathOptions, copyOptions] = splitPathOptions(options); return this.fs.copy(from, this.destinationPath(to, pathOptions), withEditorMetadata(this, { fromBasePath: this.templatePath(), ...copyOptions }), ...remaining); } /** * Copy file from templates folder to destination folder. * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}. * Shortcut for this.fs!.copy(this.templatePath(from), this.destinationPath(to)) */ async copyTemplateAsync(...args) { const [from, to, options, ...remaining] = args; const [pathOptions, copyOptions] = splitPathOptions(options); const templatePath = (filepath) => this.templatePath(filepath, pathOptions); return this.fs.copyAsync(Array.isArray(from) ? from.map(filepath => templatePath(filepath)) : templatePath(from), this.destinationPath(to, pathOptions), withEditorMetadata(this, copyOptions), ...remaining); } readDestination(...args) { const [filepath, options, ...remaining] = args; const [pathOptions, readOptions] = splitPathOptions(options); return this.fs.read(this.destinationPath(filepath, pathOptions), readOptions, ...remaining); } readDestinationJSON(...args) { const [filepath, defaults, options, ...remaining] = args; return this.fs.readJSON(this.destinationPath(filepath, options ?? {}), defaults, options, ...remaining); } /** * Write file to destination folder * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}. * Shortcut for this.fs!.write(this.destinationPath(filepath)). */ writeDestination(...args) { const [filepath, contents, options, ...remaining] = args; const [pathOptions, writeOptions] = splitPathOptions(options); return this.fs.write(this.destinationPath(filepath, pathOptions), contents, withEditorMetadata(this, writeOptions), ...remaining); } writeDestinationJSON(...args) { const [filepath, contents, ...rest] = args; // Normalize the `writeDestinationJSON(filepath, contents, options)` form to the positional one. const [replacer, space, options, ...remaining] = isWriteJSONOptions(rest[0]) ? writeJSONOptionsToPositional(rest[0]) : rest; const [pathOptions, writeOptions] = splitPathOptions(options); return this.fs.writeJSON(this.destinationPath(filepath, pathOptions), contents, replacer, space, withEditorMetadata(this, writeOptions), ...remaining); } /** * Delete file from destination folder * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}. * Shortcut for this.fs!.delete(this.destinationPath(filepath)). */ deleteDestination(...args) { const [paths, options, ...remaining] = args; const [pathOptions, deleteOptions] = splitPathOptions(options); const destinationPath = (filepath) => this.destinationPath(filepath, pathOptions); return this.fs.delete(Array.isArray(paths) ? paths.map(filepath => destinationPath(filepath)) : destinationPath(paths), deleteOptions, ...remaining); } /** * Copy file from destination folder to another destination folder. * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}. * Shortcut for this.fs!.copy(this.destinationPath(from), this.destinationPath(to)). */ copyDestination(...args) { const [from, to, options, ...remaining] = args; const [pathOptions, copyOptions] = splitPathOptions(options); return this.fs.copy(from, this.destinationPath(to, pathOptions), withEditorMetadata(this, { fromBasePath: this.destinationPath(), ...copyOptions }), ...remaining); } /** * Move file from destination folder to another destination folder. * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}. * Shortcut for this.fs!.move(this.destinationPath(from), this.destinationPath(to)). */ moveDestination(...args) { const [from, to, options, ...remaining] = args; const [pathOptions, moveOptions] = splitPathOptions(options); return this.fs.move(this.destinationPath(from, pathOptions), this.destinationPath(to, pathOptions), { fromBasePath: this.destinationPath(), ...moveOptions }, ...remaining); } /** * Exists file on destination folder. * mem-fs-editor method's shortcut, for more information see [mem-fs-editor]{@link https://github.com/SBoudrias/mem-fs-editor}. * Shortcut for this.fs!.exists(this.destinationPath(filepath)). */ existsDestination(...args) { const [filepath, options, ...remaining] = args; return this.fs.exists(this.destinationPath(filepath, options ?? {}), options, ...remaining); } renderTemplate(source = '', destination = source, templateData, copyOptions, compatOptions) { if (compatOptions || 'context' in (copyOptions ?? {})) { copyOptions = { ...compatOptions, transformOptions: copyOptions }; } if (templateData === undefined || typeof templateData === 'string') { templateData = this._templateData(templateData); } const [pathOptions, copyTplOptions] = splitPathOptions(copyOptions); source = Array.isArray(source) ? source : [source]; const templatePath = this.templatePath(...source, pathOptions); destination = Array.isArray(destination) ? destination : [destination]; const destinationPath = this.destinationPath(...destination, pathOptions); this.fs.copyTpl(templatePath, destinationPath, templateData, withEditorMetadata(this, { fromBasePath: this.templatePath(), ...copyTplOptions, transformOptions: { context: getTemplateContext(this, templatePath), ...copyTplOptions?.transformOptions, }, })); } async renderTemplateAsync(source = '', destination = source, templateData, copyOptions, compatOptions) { if (compatOptions || 'context' in (copyOptions ?? {})) { copyOptions = { ...compatOptions, transformOptions: copyOptions }; } if (templateData === undefined || typeof templateData === 'string') { templateData = this._templateData(templateData); } const [pathOptions, copyTplOptions] = splitPathOptions(copyOptions); source = Array.isArray(source) ? source : [source]; const templatePath = this.templatePath(...source, pathOptions); destination = Array.isArray(destination) ? destination : [destination]; const destinationPath = this.destinationPath(...destination, pathOptions); return this.fs.copyTplAsync(templatePath, destinationPath, templateData, withEditorMetadata(this, { fromBasePath: this.templatePath(), ...copyTplOptions, transformOptions: { context: getTemplateContext(this, templatePath), ...copyTplOptions?.transformOptions, }, })); } /** * Copy templates from templates folder to the destination. */ renderTemplates(templates, templateData) { assert.ok(Array.isArray(templates), 'Templates must an array'); if (templateData === undefined || typeof templateData === 'string') { templateData = this._templateData(templateData); } for (const template of templates) { const { templateData: eachData = templateData, source, destination } = template; if (!template.when || template.when(eachData, this)) { this.renderTemplate(source, destination, eachData, { fromBasePath: this.templatePath(), ...template.copyOptions, }); } } } /** * Copy templates from templates folder to the destination. * * @param templates - template file, relative to templatePath(), or absolute inside the source root (outside when the `allowTemplatesOutsideRoot` feature is enabled, inside the generator package when the `allowPackageTemplates` feature is enabled). * @param templateData - ejs data */ async renderTemplatesAsync(templates, templateData) { assert.ok(Array.isArray(templates), 'Templates must an array'); if (templateData === undefined || typeof templateData === 'string') { templateData = this._templateData(templateData); } return Promise.all(templates.map(async (template) => { const { templateData: eachData = templateData, source, destination } = template; if (!template.when || template.when(eachData, this)) { return this.renderTemplateAsync(source, destination, eachData, { fromBasePath: this.templatePath(), ...template.copyOptions, }); } return; })); } /** * Utility method to get a formatted data for templates. * * @param path - path to the storage key. * @return data to be passed to the templates. */ _templateData(path) { if (path) { return this.config.getPath(path); } const allConfig = this.config.getAll(); if (this.generatorConfig) { Object.assign(allConfig, this.generatorConfig.getAll()); } if (this.instanceConfig) { Object.assign(allConfig, this.instanceConfig.getAll()); } return allConfig; } }