yeoman-generator
Version:
Rails-inspired generator system that provides scaffolding for your apps
288 lines (287 loc) • 14.2 kB
JavaScript
/* 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;
}
}