T3MP3ST public launch build

This commit is contained in:
t3mp3st contributors committed 2026-07-03 10:57:20 -04:00
1 parent 972247b881
commit 184c7d4e59
1589 files changed
+57150 -506

No files matched your search

@@ -0,0 +1,50 @@
import { readFileSync } from 'node:fs';
import process from 'node:process';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import YeomanCommand, { addEnvironmentOptions } from '../util/command.ts';
import { createEnv as createEnvironment } from '../index.ts';
import { environmentAction, printGroupedGenerator } from './utils.ts';
const program = new YeomanCommand();
const packageJson = JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../../package.json')).toString());
program.version(packageJson.version).allowExcessArguments(false).enablePositionalOptions();
addEnvironmentOptions(
program
.command('run <namespace>')
.description('Run a generator')
.argument('<namespace>', 'Generator to run')
.passThroughOptions()
.allowUnknownOption()
.allowExcessArguments(true)
.action(environmentAction)
.usage('[environment options] <namespace> [generator-options]'),
);
program
.command('find')
.description('Find installed generators')
.action(async () => {
const environment = createEnvironment();
printGroupedGenerator(await environment.lookup());
});
program
.command('list')
.description('List generators available to be used')
.action(async () => {
const environment = createEnvironment();
await environment.lookup();
printGroupedGenerator(Object.values(environment.getGeneratorsMeta()));
});
try {
await program.parseAsync(process.argv);
} catch (error) {
console.log(error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
@@ -0,0 +1,43 @@
import { requireNamespace } from '@yeoman/namespace';
import type { BaseGeneratorMeta } from '@yeoman/types';
import { groupBy } from 'lodash-es';
import createLogger from 'debug';
import { createEnv as createEnvironment } from '../index.ts';
import type YeomanCommand from '../util/command.ts';
const debug = createLogger('yeoman:yoe');
export const printGroupedGenerator = (generators: BaseGeneratorMeta[]) => {
const grouped = groupBy(generators, 'packagePath');
for (const [packagePath, group] of Object.entries(grouped)) {
const namespace = requireNamespace(group[0].namespace);
console.log(` ${namespace.packageNamespace} at ${packagePath}`);
for (const generator of group) {
const generatorNamespace = requireNamespace(generator.namespace);
console.log(` :${generatorNamespace.generator || 'app'}`);
}
console.log('');
}
console.log(`${generators.length} generators`);
};
/**
* @param {string} generatorNamespace
* @param {*} options
* @param {*} command
* @returns
*/
export const environmentAction = async function (this: YeomanCommand, generatorNamespace: string, options: any, command: any) {
debug('Handling operands %o', generatorNamespace);
if (!generatorNamespace) {
return;
}
const environment = createEnvironment({ ...options, command: this });
this.env = environment;
await environment.lookupLocalPackages();
return environment.execute(generatorNamespace, command.args.splice(1));
};
@@ -0,0 +1,59 @@
import type { BaseGeneratorConstructor, GeneratorMeta } from '@yeoman/types';
import YeomanCommand, { addEnvironmentOptions } from './util/command.ts';
import { createEnv as createEnvironment } from './index.ts';
export type CommandPreparation = {
resolved?: string;
command?: YeomanCommand;
generator?: BaseGeneratorConstructor;
namespace?: string;
};
/**
* Prepare a commander instance for cli support.
*
* @param {Command} command - Command to be prepared
* @param generatorPath - Generator to create Command
* @return {Command} return command
*/
export const prepareGeneratorCommand = async ({
command = addEnvironmentOptions(new YeomanCommand()),
resolved,
generator,
namespace,
}: CommandPreparation) => {
const environment = createEnvironment();
let meta: GeneratorMeta;
if (generator && namespace) {
meta = environment.register(generator, { namespace, resolved });
} else if (resolved) {
meta = environment.register(resolved, { namespace });
} else {
throw new Error(`A generator with namespace or a generator path is required`);
}
command.env = environment;
command.registerGenerator(await meta.instantiateHelp());
command.action(async function (this: YeomanCommand) {
let rootCommand: YeomanCommand = this;
while (rootCommand.parent) {
rootCommand = rootCommand.parent as YeomanCommand;
}
const generator = await meta.instantiate(this.args, this.opts());
await environment.runGenerator(generator);
});
return command;
};
/**
* Prepare a commander instance for cli support.
*
* @param generatorPaht - Generator to create Command
* @return Return a Command instance
*/
export const prepareCommand = async (options: CommandPreparation) => {
options.command = options.command ?? new YeomanCommand();
addEnvironmentOptions(options.command);
return prepareGeneratorCommand(options);
};
@@ -0,0 +1,31 @@
import type { InputOutputAdapter } from '@yeoman/types';
import { type ConflicterOptions, createConflicterTransform, createYoResolveTransform, forceYoFiles } from '@yeoman/conflicter';
import createdLogger from 'debug';
import type { Store } from 'mem-fs';
import { type MemFsEditorFile } from 'mem-fs-editor';
import { createCommitTransform } from 'mem-fs-editor/transform';
import { isFilePending } from 'mem-fs-editor/state';
const debug = createdLogger('yeoman:environment:commit');
/**
* Commits the MemFs to the disc.
*/
export const commitSharedFsTask = async ({
adapter,
conflicterOptions,
sharedFs,
}: {
adapter: InputOutputAdapter;
conflicterOptions?: ConflicterOptions;
sharedFs: Store<MemFsEditorFile>;
}) => {
debug('Running commitSharedFsTask');
await sharedFs.pipeline(
{ filter: (file: MemFsEditorFile) => isFilePending(file) || file.path.endsWith('.yo-resolve') },
createYoResolveTransform(),
forceYoFiles(),
createConflicterTransform(adapter, conflicterOptions),
createCommitTransform(),
);
};
@@ -0,0 +1,99 @@
import crypto from 'node:crypto';
import { toNamespace } from '@yeoman/namespace';
import type { BaseGenerator, Logger } from '@yeoman/types';
import createdLogger from 'debug';
import type { InstallTask } from './package-manager.ts';
const debug = createdLogger('yeoman:environment:composed-store');
type UniqueFeatureType = 'customCommitTask' | 'customInstallTask';
export class ComposedStore {
private readonly log?;
private readonly generators: Record<string, BaseGenerator> = {};
private readonly uniqueByPathMap = new Map<string, Map<string, BaseGenerator>>();
private readonly uniqueGloballyMap = new Map<string, BaseGenerator>();
constructor({ log }: { log?: Logger } = {}) {
this.log = log;
}
get customCommitTask(): (() => Promise<void> | void) | undefined {
return this.findUniqueFeature('customCommitTask');
}
get customInstallTask(): InstallTask | undefined {
return this.findUniqueFeature('customInstallTask');
}
getGenerators(): Record<string, BaseGenerator> {
return { ...this.generators };
}
addGenerator(generator: BaseGenerator) {
const { features = (generator as any).getFeatures?.() ?? {} } = generator;
let { uniqueBy } = features;
const { uniqueGlobally } = features;
let identifier = uniqueBy;
if (!uniqueBy) {
const { namespace } = generator.options;
const instanceId = crypto.randomBytes(20).toString('hex');
let namespaceDefinition = toNamespace(namespace);
if (namespaceDefinition) {
namespaceDefinition = namespaceDefinition.with({ instanceId });
uniqueBy = namespaceDefinition.id;
identifier = namespaceDefinition.namespace;
} else {
uniqueBy = `${namespace}#${instanceId}`;
identifier = namespace;
}
}
const generatorRoot = generator.destinationRoot();
const uniqueByMap = uniqueGlobally ? this.uniqueGloballyMap : this.getUniqueByPathMap(generatorRoot);
if (uniqueByMap.has(uniqueBy)) {
return { uniqueBy, identifier, added: false, generator: uniqueByMap.get(uniqueBy) };
}
uniqueByMap.set(uniqueBy, generator);
this.generators[uniqueGlobally ? uniqueBy : `${generatorRoot}#${uniqueBy}`] = generator;
return { identifier, added: true, generator };
}
getUniqueByPathMap(root: string): Map<string, BaseGenerator> {
if (!this.uniqueByPathMap.has(root)) {
this.uniqueByPathMap.set(root, new Map());
}
return this.uniqueByPathMap.get(root)!;
}
findFeature<T = unknown>(featureName: string): Array<{ generatorId: string; feature: T }> {
return Object.entries(this.generators)
.map(([generatorId, generator]) => {
const { features = (generator as any).getFeatures?.() } = generator;
const feature = features?.[featureName];
return feature ? { generatorId, feature: feature as T } : undefined;
})
.filter(Boolean) as Array<{ generatorId: string; feature: T }>;
}
private findUniqueFeature<T = unknown>(featureName: UniqueFeatureType): T | undefined {
const providedFeatures = this.findFeature<T>(featureName);
if (providedFeatures.length > 0) {
if (providedFeatures.length > 1) {
this.log?.info?.(
`Multiple ${featureName} tasks found (${providedFeatures.map(({ generatorId }) => generatorId).join(', ')}). Using the first.`,
);
}
const [{ generatorId, feature }] = providedFeatures;
debug(`Feature ${featureName} provided by ${generatorId}`);
return feature;
}
return;
}
}
@@ -0,0 +1,17 @@
export const UNKNOWN_NAMESPACE = 'unknownnamespace';
export const UNKNOWN_RESOLVED = 'unknown';
export const defaultQueues = [
'environment:run',
'initializing',
'prompting',
'configuring',
'default',
'writing',
'transform',
'conflicts',
'environment:conflicts',
'install',
'end',
];
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,380 @@
import { createHash } from 'node:crypto';
import { join, resolve } from 'node:path';
import type { BaseGeneratorConstructor, BaseGeneratorOptions, GeneratorEnvironmentOptions, InputOutputAdapter } from '@yeoman/types';
import { type YeomanNamespace, requireNamespace, toNamespace } from '@yeoman/namespace';
import { flyImport } from 'fly-import';
import { defaults, pick, uniq } from 'lodash-es';
import { valid } from 'semver';
import { type LookupOptions } from './generator-lookup.ts';
import YeomanCommand from './util/command.ts';
import EnvironmentBase, { type EnvironmentOptions } from './environment-base.ts';
import { splitArgsFromString as splitArgumentsFromString } from './util/util.ts';
class FullEnvironment extends EnvironmentBase {
constructor(options?: EnvironmentOptions);
constructor(options: EnvironmentOptions = {}, adapterCompat?: InputOutputAdapter) {
if (adapterCompat) {
options.adapter = adapterCompat;
}
super(options);
this.loadSharedOptions(this.options);
if (this.sharedOptions.skipLocalCache === undefined) {
this.sharedOptions.skipLocalCache = true;
}
}
/**
* Load options passed to the Generator that should be used by the Environment.
*
* @param {Object} options
*/
loadEnvironmentOptions(options: EnvironmentOptions) {
const environmentOptions = pick(options, ['skipInstall', 'nodePackageManager']);
defaults(this.options, environmentOptions);
return environmentOptions;
}
/**
* Load options passed to the Environment that should be forwarded to the Generator.
*
* @param {Object} options
*/
loadSharedOptions(options: EnvironmentOptions) {
const optionsToShare = pick(options, [
'skipInstall',
'forceInstall',
'skipCache',
'skipLocalCache',
'skipParseOptions',
'localConfigOnly',
'askAnswered',
]);
Object.assign(this.sharedOptions, optionsToShare);
return optionsToShare;
}
/**
* @protected
* Outputs the general help and usage. Optionally, if generators have been
* registered, the list of available generators is also displayed.
*
* @param {String} name
*/
help(name = 'init') {
const out = [
'Usage: :binary: GENERATOR [args] [options]',
'',
'General options:',
" --help # Print generator's options and usage",
' -f, --force # Overwrite files that already exist',
'',
'Please choose a generator below.',
'',
];
const ns = this.namespaces();
const groups: Record<string, string[]> = {};
for (const namespace of ns) {
const base = namespace.split(':')[0];
if (!groups[base]) {
groups[base] = [];
}
groups[base].push(namespace);
}
for (const key of Object.keys(groups).sort()) {
const group = groups[key];
if (group.length > 0) {
out.push('', key.charAt(0).toUpperCase() + key.slice(1));
}
for (const ns of groups[key]) {
out.push(` ${ns}`);
}
}
return out.join('\n').replaceAll(':binary:', name);
}
/**
* @protected
* Returns the list of registered namespace.
* @return {Array}
*/
namespaces() {
return this.store.namespaces();
}
/**
* @protected
* Returns stored generators meta
* @return {Object}
*/
getGeneratorsMeta() {
return this.store.getGeneratorsMeta();
}
/**
* @protected
* Get registered generators names
*
* @return {Array}
*/
getGeneratorNames() {
return uniq(Object.keys(this.getGeneratorsMeta()).map(namespace => toNamespace(namespace)?.packageNamespace));
}
/**
* Get last added path for a namespace
*
* @param {String} - namespace
* @return {String} - path of the package
*/
getPackagePath(namespace: string) {
if (namespace.includes(':')) {
const generator = this.getGeneratorMeta(namespace);
return generator?.packagePath;
}
const packagePaths = this.getPackagePaths(namespace) || [];
return packagePaths[0];
}
/**
* Get paths for a namespace
*
* @param - namespace
* @return array of paths.
*/
getPackagePaths(namespace: string) {
return (
this.store.getPackagesPaths()[namespace] || this.store.getPackagesPaths()[requireNamespace(this.alias(namespace)).packageNamespace]
);
}
/**
* Generate a command for the generator and execute.
*
* @param {string} generatorNamespace
* @param {string[]} args
*/
async execute(generatorNamespace: string, arguments_ = []) {
const namespace = requireNamespace(generatorNamespace);
if (!(await this.get(namespace.namespace))) {
await this.lookup({
packagePatterns: [namespace.generatorHint],
singleResult: true,
});
}
if (!(await this.get(namespace.namespace))) {
await this.installLocalGenerators({
[namespace.generatorHint]: namespace.semver,
});
}
const namespaceCommand = this.command ? this.command.command(namespace.namespace) : new YeomanCommand();
namespaceCommand.usage('[generator-options]');
// Instantiate the generator for options
const generator = await this.create(namespace.namespace, { generatorArgs: [], generatorOptions: { help: true } });
namespaceCommand.registerGenerator(generator);
namespaceCommand.parse(arguments_, { from: 'user' });
return this.run([namespace.namespace, ...namespaceCommand.args], {
...namespaceCommand.opts(),
});
}
async requireGenerator(namespace: string): Promise<BaseGeneratorConstructor | undefined> {
if (namespace === undefined) {
try {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line import-x/no-unresolved
const { default: Generator } = await import('yeoman-generator');
return Generator;
} catch {
// ignore error
}
const { default: Generator } = await flyImport('yeoman-generator');
return Generator;
}
// Namespace is a version
if (valid(namespace)) {
// Create a hash to install any version range in the local repository
const hash = createHash('shake256', { outputLength: 2 }).update(namespace, 'utf8').digest('hex');
const { default: Generator } = await flyImport(`@yeoman/generator-impl-${hash}@npm:yeoman-generator@${namespace}`);
return Generator;
}
return this.get(namespace);
}
/**
* Install generators at the custom local repository and register.
*
* @param {Object} packages - packages to install key(packageName): value(versionRange).
* @return {Boolean} - true if the install succeeded.
*/
async installLocalGenerators(packages: Record<string, string | undefined>) {
const entries = Object.entries(packages);
const specs = entries.map(([packageName, version]) => `${packageName}${version ? `@${version}` : ''}`);
const installResult = await this.repository.install(specs);
const failToInstall = installResult.find(result => !result.path);
if (failToInstall) {
throw new Error(`Fail to install ${failToInstall.pkgid}`);
}
await this.lookup({ packagePaths: installResult.map(result => result.path) as string[] });
return true;
}
/**
* Lookup and register generators from the custom local repository.
*
* @param {String[]} [packagesToLookup='generator-*'] - packages to lookup.
*/
async lookupLocalPackages(packagesToLookup = ['generator-*']) {
await this.lookup({
packagePatterns: packagesToLookup,
npmPaths: [join(this.repository.repositoryPath, 'node_modules')],
});
}
/**
* Lookup and register generators from the custom local repository.
*
* @private
* @param {YeomanNamespace[]} namespacesToLookup - namespaces to lookup.
* @return {Promise<Object[]>} List of generators
*/
async lookupLocalNamespaces(namespacesToLookup: string | string[]) {
if (!namespacesToLookup) {
return [];
}
namespacesToLookup = Array.isArray(namespacesToLookup) ? namespacesToLookup : [namespacesToLookup];
const namespaces = namespacesToLookup.map(ns => requireNamespace(ns));
// Keep only those packages that has a compatible version.
return this.lookupLocalPackages(namespaces.map(ns => ns.generatorHint));
}
/**
* Search for generators or sub generators by namespace.
*
* @private
* @param {boolean|Object} [options] options passed to lookup. Options singleResult,
* filePatterns and packagePatterns can be overridden
* @return {Array|Object} List of generators
*/
async lookupNamespaces(namespaces: string | string[], options: LookupOptions = {}) {
if (!namespaces) {
return [];
}
namespaces = Array.isArray(namespaces) ? namespaces : [namespaces];
const namespacesObjs = namespaces.map(ns => requireNamespace(ns));
const options_ = namespacesObjs.map(ns => {
const nsOptions: LookupOptions = { packagePatterns: [ns.generatorHint] };
if (ns.generator) {
// Build filePatterns to look specifically for the namespace.
const genPath = ns.generator.split(':').join('/');
let filePatterns = [`${genPath}/index.?s`, `${genPath}.?s`];
const lookups = options.lookups ?? this.lookups;
filePatterns = lookups.flatMap(prefix => filePatterns.map(pattern => join(prefix, pattern)));
nsOptions.filePatterns = filePatterns;
nsOptions.singleResult = true;
}
return nsOptions;
});
return Promise.all(options_.flatMap(async opt => this.lookup({ ...opt, ...options })));
}
/**
* Load or install namespaces based on the namespace flag
*
* @private
* @param {String|Array} - namespaces
* @return {boolean} - true if every required namespace was found.
*/
async prepareEnvironment(namespaces: string | string[]) {
namespaces = Array.isArray(namespaces) ? namespaces : [namespaces];
let missing = namespaces.map(ns => requireNamespace(ns));
const updateMissing = async () => {
const entries = await Promise.all(missing.map(async ns => [ns, await this.get(ns)]));
missing = entries.filter(([_ns, gen]) => Boolean(gen)).map(([ns]) => ns) as YeomanNamespace[];
};
await updateMissing();
// Install missing
const toInstall: Record<string, string | undefined> = Object.fromEntries(missing.map(ns => [ns.generatorHint, ns.semver]));
await this.installLocalGenerators(toInstall);
await updateMissing();
if (missing.length === 0) {
return true;
}
throw new Error(`Error preparing environment for ${missing.map(ns => ns.complete).join(',')}`);
}
/**
* Tries to locate and run a specific generator. The lookup is done depending
* on the provided arguments, options and the list of registered generators.
*
* When the environment was unable to resolve a generator, an error is raised.
*/
async run(
arguments_?: string | string[],
options: EnvironmentOptions & BaseGeneratorOptions & Partial<GeneratorEnvironmentOptions> = {},
) {
arguments_ = Array.isArray(arguments_) ? arguments_ : splitArgumentsFromString(arguments_ as unknown as string);
let name = arguments_.shift();
if (!name) {
throw new Error('Must provide at least one argument, the generator namespace to invoke.');
}
if (name.startsWith('.')) {
name = resolve(name);
}
this.loadEnvironmentOptions(options);
if (this.experimental && !this.getGeneratorMeta(name)) {
try {
await this.prepareEnvironment(name);
} catch {
// ignore error
}
}
const generator = await this.create(name, {
generatorArgs: arguments_,
generatorOptions: options,
});
if (options.help) {
console.log((generator as any).help());
return;
}
return this.runGenerator(generator);
}
}
export default FullEnvironment;
@@ -0,0 +1,117 @@
import { extname, isAbsolute, join, posix } from 'node:path';
import { pathToFileURL } from 'node:url';
import type { LookupOptions as LookupOptionsApi } from '@yeoman/types';
import { requireNamespace, toNamespace } from '@yeoman/namespace';
import { type ModuleLookupOptions, findPackagesIn, getNpmPaths, moduleLookupSync } from './module-lookup.ts';
import { asNamespace, defaultLookups } from './util/namespace.ts';
export type LookupOptions = LookupOptionsApi &
ModuleLookupOptions & {
lookups?: string[];
};
type LookupMeta = { filePath: string; packagePath: string; lookups: string[] };
export const defaultExtensions = ['.ts', '.cts', '.mts', '.js', '.cjs', '.mjs'];
/**
* Search for generators and their sub generators.
*
* A generator is a `:lookup/:name/index.js` file placed inside an npm package.
*
* Defaults lookups are:
* - ./
* - generators/
* - lib/generators/
*
* So this index file `node_modules/generator-dummy/lib/generators/yo/index.js` would be
* registered as `dummy:yo` generator.
*
* @param {boolean|Object} [options]
* @param {boolean} [options.localOnly = false] - Set true to skip lookups of
* globally-installed generators.
* @param {string|Array} [options.packagePaths] - Paths to look for generators.
* @param {string|Array} [options.npmPaths] - Repository paths to look for generators packages.
* @param {string|Array} [options.filePatterns='*\/index.js'] - File pattern to look for.
* @param {string|Array} [options.packagePatterns='generator-*'] - Package pattern to look for.
* @param {boolean} [options.singleResult=false] - Set true to stop lookup on the first match.
* @param {Number} [options.globbyDeep] - Deep option to be passed to globby.
* @return {Promise<Object[]>} List of generators
*/
export async function lookupGenerators(options: LookupOptions = {}, register?: (meta: LookupMeta) => boolean) {
const { lookups = defaultLookups } = options;
options = {
// Js generators should be after, last will override registered one.
filePatterns: lookups.flatMap(prefix => defaultExtensions.map(extension => `${prefix}/*/index${extension}`)),
filterPaths: false,
packagePatterns: ['generator-*'],
reverse: !options.singleResult,
...options,
};
return moduleLookupSync(options, ({ packagePath, files }) => {
files = [...files].sort((a, b) => {
return defaultExtensions.indexOf(extname(a)) - defaultExtensions.indexOf(extname(b));
});
for (const filePath of files) {
const registered = register?.({ filePath, packagePath, lookups });
if (options.singleResult && registered) {
return filePath;
}
}
return;
});
}
/**
* Lookup for a specific generator.
*
* @param {String} namespace
* @param {Object} [options]
* @param {Boolean} [options.localOnly=false] - Set true to skip lookups of
* globally-installed generators.
* @param {Boolean} [options.packagePath=false] - Set true to return the package
* path instead of generators file.
* @param {Boolean} [options.singleResult=true] - Set false to return multiple values.
* @return {String} generator
*/
export function lookupGenerator(namespace: string, options?: ModuleLookupOptions & { packagePath?: boolean; generatorPath?: boolean }) {
options = typeof options === 'boolean' ? { localOnly: options } : (options ?? {});
options.singleResult = options.singleResult ?? true;
options.filePatterns = options.filePatterns ?? defaultLookups.map(prefix => join(prefix, '*/index.{js,ts}'));
const ns = requireNamespace(namespace);
options.packagePatterns = options.packagePatterns ?? [ns.generatorHint];
options.npmPaths = options.npmPaths ?? getNpmPaths({ localOnly: options.localOnly }).toReversed();
options.packagePatterns = options.packagePatterns ?? ['generator-*'];
options.packagePaths = options.packagePaths ?? findPackagesIn(options.npmPaths, options.packagePatterns);
let paths: string[] | string | undefined = options.singleResult ? undefined : [];
moduleLookupSync(options, ({ files, packagePath }) => {
for (const filename of files) {
const fileNs = asNamespace(filename, { lookups: defaultLookups });
const ns = toNamespace(fileNs);
if (namespace === fileNs || (options!.packagePath && namespace === ns?.packageNamespace)) {
// Version 2.6.0 returned pattern instead of modulePath for options.packagePath
const returnPath = options!.packagePath ? packagePath : options!.generatorPath ? posix.join(filename, '../../') : filename;
if (options!.singleResult) {
paths = returnPath;
return filename;
}
(paths as string[]).push(returnPath);
}
}
return;
});
if (options.singleResult) {
const generatorPath = paths as unknown as string;
return generatorPath && isAbsolute(generatorPath) ? pathToFileURL(generatorPath).toString() : generatorPath;
}
return paths!.map(gen => (isAbsolute(gen) ? pathToFileURL(gen).toString() : gen));
}
@@ -0,0 +1,16 @@
import { type EnvironmentOptions } from './environment-base.ts';
import Environment from './environment-full.ts';
export { default } from './environment-full.ts';
export { default as EnvironmentBase, type EnvironmentOptions } from './environment-base.ts';
export const createEnv = (options?: EnvironmentOptions) => new Environment(options);
// Backward compatibility
export const enforceUpdate = () => {};
export * from './commands.ts';
export * from './util/command.ts';
export * from './package-manager.ts';
export * from './commit.ts';
export { lookupGenerator } from './generator-lookup.ts';
@@ -0,0 +1,297 @@
import { existsSync, lstatSync, readFileSync } from 'node:fs';
import { delimiter, dirname, join, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import process from 'node:process';
import arrify from 'arrify';
import { compact, uniq } from 'lodash-es';
import { type Options as GlobbyOptions, globbySync } from 'globby';
import slash from 'slash';
import createdLogger from 'debug';
import { execaOutput } from './util/util.ts';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const PROJECT_ROOT = join(__dirname, '..');
const PACKAGE_NAME_PATTERN = [JSON.parse(readFileSync(join(PROJECT_ROOT, 'package.json')).toString()).name];
const win32 = process.platform === 'win32';
const nvm = process.env.NVM_HOME;
const debug = createdLogger('yeoman:environment');
export type ModuleLookupOptions = {
/** Set true to skip lookups of globally-installed generators */
localOnly?: boolean;
/** Paths to look for generators */
packagePaths?: string[];
/** Repository paths to look for generators packages */
npmPaths?: string[];
/** File pattern to look for */
filePatterns?: string[];
/** The package patterns to look for */
packagePatterns?: string[];
/** A value indicating whether the lookup should be stopped after finding the first result */
singleResult?: boolean;
filterPaths?: boolean;
/** Set true reverse npmPaths/packagePaths order */
reverse?: boolean;
/** The `deep` option to pass to `globby` */
globbyDeep?: number;
globbyOptions?: any;
};
/**
* Search for npm packages.
*/
export function moduleLookupSync(
options: ModuleLookupOptions,
find: (argument: { files: string[]; packagePath: string }) => string | undefined,
) {
debug('Running lookup with options: %o', options);
options = { ...options };
options.filePatterns = arrify(options.filePatterns ?? 'package.json').map(filePattern => slash(filePattern));
if (options.packagePaths) {
options.packagePaths = arrify(options.packagePaths);
if (options.reverse) {
options.packagePaths = options.packagePaths.toReversed();
}
} else {
options.npmPaths = options.npmPaths ?? getNpmPaths(options);
if (options.reverse && Array.isArray(options.npmPaths)) {
options.npmPaths = options.npmPaths.toReversed();
}
options.packagePatterns = arrify(options.packagePatterns ?? PACKAGE_NAME_PATTERN).map(packagePattern => slash(packagePattern));
options.packagePaths = findPackagesIn(options.npmPaths, options.packagePatterns);
}
debug('Lookup calculated options: %o', options);
const modules = [];
for (const packagePath of options.packagePaths) {
if (!existsSync(packagePath) || (!lstatSync(packagePath).isDirectory() && !lstatSync(packagePath).isSymbolicLink())) {
continue;
}
const files = globbySync(options.filePatterns, {
cwd: packagePath,
absolute: true,
...options.globbyOptions,
} as GlobbyOptions);
const filePath = find({ files, packagePath });
if (filePath) {
return [{ filePath, packagePath }];
}
for (const filePath of files) {
modules.push({ filePath, packagePath });
}
}
return modules;
}
/**
* Search npm for every available generators.
* Generators are npm packages who's name start with `generator-` and who're placed in the
* top level `node_module` path. They can be installed globally or locally.
*
* @method
*
* @param searchPaths List of search paths
* @param packagePatterns Pattern of the packages
* @param globbyOptions
* @return List of the generator modules path
*/
export function findPackagesIn(searchPaths: string[], packagePatterns: string[], globbyOptions?: any): any[] {
searchPaths = arrify(searchPaths)
.filter(Boolean)
.map(npmPath => resolve(npmPath));
let modules: any[] = [];
for (const root of searchPaths) {
if (!existsSync(root) || (!lstatSync(root).isDirectory() && !lstatSync(root).isSymbolicLink())) {
continue;
}
// Some folders might not be readable to the current user. For those, we add a try
// catch to handle the error gracefully as globby doesn't have an option to skip
// restricted folders.
try {
modules = modules.concat(
globbySync(packagePatterns, {
cwd: root,
onlyDirectories: true,
expandDirectories: false,
absolute: true,
deep: 0,
...globbyOptions,
} as GlobbyOptions),
);
// To limit recursive lookups into non-namespace folders within globby,
// fetch all namespaces in root, then search each namespace separately
// for generator modules
const scopes = globbySync(['@*'], {
cwd: root,
onlyDirectories: true,
expandDirectories: false,
absolute: true,
deep: 0,
...globbyOptions,
} as GlobbyOptions);
for (const scope of scopes) {
modules = modules.concat(
globbySync(packagePatterns, {
cwd: scope,
onlyDirectories: true,
expandDirectories: false,
absolute: true,
deep: 0,
...globbyOptions,
} as GlobbyOptions),
);
}
} catch (error) {
debug('Could not access %s (%s)', root, error);
}
}
return modules;
}
/**
* Get the npm lookup directories (`node_modules/`)
*
* @method
*
* @param {boolean|Object} [options]
* @param {boolean} [options.localOnly = false] - Set true to skip lookups of
* globally-installed generators.
* @param {boolean} [options.filterPaths = false] - Remove paths that don't ends
* with a supported path (don't touch at NODE_PATH paths).
* @return {Array} lookup paths
*/
export function getNpmPaths(options: { localOnly?: boolean; filterPaths?: boolean } = {}): string[] {
// Resolve signature where options is boolean (localOnly).
if (typeof options === 'boolean') {
options = { localOnly: options };
}
// Start with the local paths.
let paths = getLocalNpmPaths();
// Append global paths, unless they should be excluded.
if (!options.localOnly) {
paths = paths.concat(getGlobalNpmPaths(options.filterPaths));
}
return uniq(paths);
}
/**
* Get the local npm lookup directories
* @private
* @return {Array} lookup paths
*/
function getLocalNpmPaths(): string[] {
const paths: string[] = [];
// Walk up the CWD and add `node_modules/` folder lookup on each level
process
.cwd()
.split(sep)
.forEach((part, index, parts) => {
let lookup = join(...parts.slice(0, index + 1), 'node_modules');
if (!win32) {
lookup = `/${lookup}`;
}
paths.push(lookup);
});
return uniq(paths.toReversed());
}
/**
* Get the global npm lookup directories
* Reference: https://nodejs.org/api/modules.html
* @private
* @return {Array} lookup paths
*/
function getGlobalNpmPaths(filterPaths = true): string[] {
let paths: string[] = [];
// Node.js will search in the following list of GLOBAL_FOLDERS:
// 1: $HOME/.node_modules
// 2: $HOME/.node_libraries
// 3: $PREFIX/lib/node
const filterValidNpmPath = function (path: string, ignore = false): string[] {
return ignore ? [path] : ['/node_modules', '/.node_modules', '/.node_libraries', '/node'].some(dir => path.endsWith(dir)) ? [path] : [];
};
// Default paths for each system
if (nvm && process.env.NVM_HOME) {
paths.push(join(process.env.NVM_HOME, process.version, 'node_modules'));
} else if (win32 && process.env.APPDATA) {
paths.push(join(process.env.APPDATA, 'npm/node_modules'));
} else {
paths.push('/usr/lib/node_modules', '/usr/local/lib/node_modules');
}
// Add NVM prefix directory
if (process.env.NVM_PATH) {
paths.push(join(dirname(process.env.NVM_PATH), 'node_modules'));
}
// Adding global npm directories
// We tried using npm to get the global modules path, but it haven't work out
// because of bugs in the parseable implementation of `ls` command and mostly
// performance issues. So, we go with our best bet for now.
if (process.env.NODE_PATH) {
paths = compact(process.env.NODE_PATH.split(delimiter)).concat(paths);
}
// Global node_modules should be 4 or 2 directory up this one (most of the time)
// Ex: /usr/another_global/node_modules/yeoman-denerator/node_modules/yeoman-environment/lib (1 level dependency)
paths.push(...filterValidNpmPath(join(PROJECT_ROOT, '../../..'), !filterPaths));
// Ex: /usr/another_global/node_modules/yeoman-environment/lib (installed directly)
// eslint-disable-next-line unicorn/prefer-single-call
paths.push(join(PROJECT_ROOT, '..'));
// Get yarn global directory and infer the module paths from there
const yarnBase = execaOutput('yarn', ['global', 'dir']);
if (yarnBase) {
paths.push(resolve(yarnBase, 'node_modules'), resolve(yarnBase, '../link/'));
}
if (process.env.PNPM_HOME) {
const pnpmHome = process.env.PNPM_HOME;
paths.push(
...globbySync('global/*/node_modules', {
cwd: pnpmHome,
onlyDirectories: true,
suppressErrors: true,
absolute: true,
}).map(modulesPath => resolve(modulesPath)),
);
}
// Get npm global prefix and infer the module paths from there
const globalInstall = execaOutput('npm', ['root', '-g']);
if (globalInstall) {
paths.push(resolve(globalInstall));
}
// Adds support for generator resolving when yeoman-generator has been linked
if (process.argv[1]) {
paths.push(...filterValidNpmPath(join(dirname(process.argv[1]), '../..'), !filterPaths));
}
return uniq(paths.filter(Boolean).toReversed());
}
@@ -0,0 +1,110 @@
import { join, resolve } from 'node:path';
import createdLogger from 'debug';
import { whichPackageManager } from 'which-package-manager';
import { execa } from 'execa';
import type { MemFsEditorFile } from 'mem-fs-editor';
import type { InputOutputAdapter } from '@yeoman/types';
import type { Store } from 'mem-fs';
const debug = createdLogger('yeoman:environment:package-manager');
export type InstallTask = (nodePackageManager: string | undefined, defaultTask: () => Promise<boolean>) => void | Promise<void>;
export type PackageManagerInstallTaskOptions = {
memFs: Store<MemFsEditorFile>;
packageJsonLocation: string;
adapter: InputOutputAdapter;
nodePackageManager?: string;
customInstallTask?: boolean | InstallTask;
skipInstall?: boolean;
};
/**
* Executes package manager install.
* - checks if package.json was committed.
* - uses a preferred package manager or try to detect.
* @return {Promise<boolean>} Promise true if the install execution suceeded.
*/
/*
const { customInstallTask } = this.composedStore;
packageJsonFile: join(this.cwd, 'package.json');
*/
export async function packageManagerInstallTask({
memFs,
packageJsonLocation,
customInstallTask,
adapter,
nodePackageManager,
skipInstall,
}: PackageManagerInstallTaskOptions) {
debug('Running packageManagerInstallTask');
packageJsonLocation = resolve(packageJsonLocation);
/**
* @private
* Get the destination package.json file.
* @return {Vinyl | undefined} a Vinyl file.
*/
function getDestinationPackageJson() {
return memFs.get(join(packageJsonLocation, 'package.json'));
}
/**
* @private
* Get the destination package.json commit status.
* @return {boolean} package.json commit status.
*/
function isDestinationPackageJsonCommitted() {
const file = getDestinationPackageJson();
return file.committed;
}
if (!getDestinationPackageJson()) {
return false;
}
if (customInstallTask && typeof customInstallTask !== 'function') {
debug('Install disabled by customInstallTask');
return false;
}
if (!isDestinationPackageJsonCommitted()) {
adapter.log(`
No change to package.json was detected. No package manager install will be executed.`);
return false;
}
adapter.log(`
Changes to package.json were detected.`);
if (skipInstall) {
adapter.log(`Skipping package manager install.
`);
return false;
}
let packageManagerName = nodePackageManager ?? (await whichPackageManager({ cwd: packageJsonLocation }));
const execPackageManager = async () => {
if (!packageManagerName) {
packageManagerName = 'npm';
adapter.log('Error detecting the package manager. Falling back to npm.');
}
if (!['npm', 'yarn', 'pnpm', 'bun'].includes(packageManagerName)) {
adapter.log(`${packageManagerName} is not a supported package manager. Run it by yourself.`);
return false;
}
adapter.log(`
Running ${packageManagerName} install for you to install the required dependencies.`);
await execa(packageManagerName, ['install'], { stdio: 'inherit', cwd: packageJsonLocation });
return true;
};
if (customInstallTask) {
return customInstallTask(packageManagerName, execPackageManager);
}
return execPackageManager();
}
@@ -0,0 +1,267 @@
import { pathToFileURL } from 'node:url';
import { extname, join } from 'node:path';
import { createRequire } from 'node:module';
import { toNamespace } from '@yeoman/namespace';
import type {
BaseEnvironment,
BaseGenerator,
BaseGeneratorConstructorMeta,
BaseGeneratorMeta,
GeneratorMeta,
GetGeneratorConstructor,
} from '@yeoman/types';
import createDebug from 'debug';
const debug = createDebug('yeoman:environment:store');
const require = createRequire(import.meta.url);
/**
* The Generator store
* This is used to store generator (npm packages) reference and instantiate them when
* requested.
* @constructor
* @private
*/
export default class Store {
private readonly _meta: Record<string, GeneratorMeta> = {};
// Store packages paths by ns
private readonly _packagesPaths: Record<string, string[]> = {};
// Store packages ns
private readonly _packagesNS: string[] = [];
private readonly environment: BaseEnvironment;
constructor(environment: BaseEnvironment) {
this.environment = environment;
}
/**
* Store a module under the namespace key
* @param meta
* @param generator - A generator module or a module path
*/
add<M extends BaseGeneratorMeta>(meta: M, Generator?: unknown): GeneratorMeta & M {
if (typeof meta.resolved === 'string') {
if (extname(meta.resolved)) {
meta.resolved = join(meta.resolved);
} else {
try {
// Resolve if meta.resolved is a package path.
meta.resolved = require.resolve(meta.resolved);
} catch {
// Import must be a file, append index.js to directories
meta.resolved = join(meta.resolved, 'index.js');
}
}
}
if (meta.packagePath) {
meta.packagePath = join(meta.packagePath);
}
let importModule: (() => Promise<unknown>) | undefined;
if (!Generator) {
if (!meta.resolved) {
throw new Error(`Generator Stub or resolved path is required for ${meta.namespace}`);
}
importModule = () => {
try {
return require(meta.resolved!);
} catch (error: any) {
if (error.code === 'ERR_REQUIRE_ESM' || error.code === 'ERR_REQUIRE_ASYNC_MODULE') {
return import(pathToFileURL(meta.resolved!).href);
}
throw error;
}
};
}
let importPromise: Promise<unknown> | undefined;
// eslint-disable-next-line prefer-const
let generatorMeta: (GeneratorMeta & M) | undefined;
const importGenerator: GeneratorMeta['importGenerator'] = <G extends BaseGenerator>():
| Promise<GetGeneratorConstructor<G> & BaseGeneratorConstructorMeta>
| (GetGeneratorConstructor<G> & BaseGeneratorConstructorMeta) => {
const handleImport = () => {
if (importModule && !Generator) {
const maybeModule = importModule();
if ((maybeModule as any).then) {
importPromise = maybeModule;
return maybeModule
.then((mod: any) => {
Generator = mod;
})
.finally(() => {
importPromise = undefined;
});
} else {
Generator = maybeModule;
}
}
};
const handleModule = <G extends BaseGenerator>():
| Promise<GetGeneratorConstructor<G> & BaseGeneratorConstructorMeta>
| (GetGeneratorConstructor<G> & BaseGeneratorConstructorMeta) => {
const factory = this.getFactory(Generator);
if (typeof factory === 'function') {
importPromise = factory(this.environment);
return Promise.resolve(importPromise).then((mod: any) => {
const generator = this._getGenerator<G>(mod, meta, generatorMeta);
Generator = generator;
importPromise = undefined;
return generator;
});
}
return this._getGenerator<G>(Generator, meta, generatorMeta);
};
if (importPromise) {
return importPromise.then(() => Promise.resolve(handleImport()).then(() => handleModule()));
}
const maybeImportPromise = handleImport();
if (maybeImportPromise?.then) {
return maybeImportPromise.then(() => handleModule());
}
return handleModule();
};
const instantiate: GeneratorMeta['instantiate'] = async <G extends BaseGenerator>(arguments_: string[] = [], options: any = {}) =>
this.environment.instantiate<G>(await importGenerator<G>(), { generatorArgs: arguments_, generatorOptions: options });
const instantiateHelp: GeneratorMeta['instantiateHelp'] = async () => instantiate([], { help: true });
const { packageNamespace } = toNamespace(meta.namespace) ?? {};
generatorMeta = {
...meta,
importGenerator,
importModule,
instantiate,
instantiateHelp,
packageNamespace,
};
this._meta[meta.namespace] = generatorMeta;
if (packageNamespace) {
this.addPackageNamespace(packageNamespace);
if (meta.packagePath) {
this.addPackage(packageNamespace, meta.packagePath);
}
}
return generatorMeta;
}
/**
* Get the module registered under the given namespace
* @param {String} namespace
* @return {Module}
*/
async get(namespace: string): Promise<GetGeneratorConstructor | undefined> {
return this.getMeta(namespace)?.importGenerator();
}
/**
* Get the module registered under the given namespace
* @param {String} namespace
* @return {Module}
*/
getMeta(namespace: string): GeneratorMeta | undefined {
return this._meta[namespace];
}
/**
* Returns the list of registered namespace.
* @return {Array} Namespaces array
*/
namespaces() {
return Object.keys(this._meta);
}
/**
* Get the stored generators meta data
* @return {Object} Generators metadata
*/
getGeneratorsMeta() {
return this._meta;
}
/**
* Store a package under the namespace key
* @param {String} packageNS - The key under which the generator can be retrieved
* @param {String} packagePath - The package path
*/
addPackage(packageNS: string, packagePath: string) {
if (this._packagesPaths[packageNS]) {
// Yo environment allows overriding, so the last added has preference.
if (this._packagesPaths[packageNS][0] !== packagePath) {
const packagePaths = this._packagesPaths[packageNS];
debug(
'Overriding a package with namespace %s and path %s, with path %s',
packageNS,
this._packagesPaths[packageNS][0],
packagePath,
);
// Remove old packagePath
const index = packagePaths.indexOf(packagePath);
if (index !== -1) {
packagePaths.splice(index, 1);
}
packagePaths.splice(0, 0, packagePath);
}
} else {
this._packagesPaths[packageNS] = [packagePath];
}
}
/**
* Get the stored packages namespaces with paths.
* @return {Object} Stored packages namespaces with paths.
*/
getPackagesPaths() {
return this._packagesPaths;
}
/**
* Store a package ns
* @param {String} packageNS - The key under which the generator can be retrieved
*/
addPackageNamespace(packageNS: string) {
if (!this._packagesNS.includes(packageNS)) {
this._packagesNS.push(packageNS);
}
}
/**
* Get the stored packages namespaces.
* @return {Array} Stored packages namespaces.
*/
getPackagesNS(): string[] {
return this._packagesNS;
}
private getFactory(module: any) {
// CJS is imported in default, for backward compatibility we support a Generator exported as `module.exports = { default }`
return module.createGenerator ?? module.default?.createGenerator ?? module.default?.default?.createGenerator;
}
private _getGenerator<G extends BaseGenerator>(
module: any,
meta: BaseGeneratorMeta,
generatorMeta: BaseGeneratorMeta | undefined,
): GetGeneratorConstructor<G> & BaseGeneratorConstructorMeta {
const Generator = module.default?.default ?? module.default ?? module;
if (typeof Generator !== 'function') {
throw new TypeError("The generator doesn't provide a constructor.");
}
Object.assign(Generator, meta);
Generator._meta = generatorMeta;
return Generator;
}
}
@@ -0,0 +1,136 @@
import { Command, Option } from 'commander';
import type BaseEnvironment from '../environment-base.ts';
export default class YeomanCommand extends Command {
env?: BaseEnvironment;
override createCommand(name?: string) {
return new YeomanCommand(name);
}
/**
* Override addOption to register a negative alternative for every option.
* @param {Option} option
* @return {YeomanCommand} this;
*/
addOption(option: Option) {
if (!option.long || option.required || option.optional) {
return super.addOption(option);
}
if (option.negate) {
// Add a affirmative option for negative boolean options.
// Should be done before, because commander adds a non working affirmative by itself.
super.addOption(new Option(option.long.replace(/^--no-/, '--')).hideHelp());
}
const result = super.addOption(option);
if (!option.negate) {
// Add a hidden negative option for affirmative boolean options.
super.addOption(new Option(option.long.replace(/^--/, '--no-')).hideHelp());
}
return result;
}
/**
* Load Generator options into a commander instance.
*
* @param {Generator} generator - Generator
* @return {Command} return command
*/
registerGenerator(generator: any) {
return this.addGeneratorOptions(generator._options).addGeneratorArguments(generator._arguments);
}
/**
* Register arguments using generator._arguments structure.
* @param {object[]} generatorArgs
* @return {YeomanCommand} this;
*/
addGeneratorArguments(generatorArguments: any[] = []) {
if (!generatorArguments || generatorArguments.length === 0) {
return this;
}
const arguments_ = generatorArguments
.map(argument => {
const argumentName = argument.type === Array ? `${argument.name}...` : argument.name;
return argument.required ? `<${argumentName}>` : `[${argumentName}]`;
})
.join(' ');
this.arguments(arguments_);
return this;
}
/**
* Register options using generator._options structure.
* @param {object} options
* @param {string} blueprintOptionDescription - description of the blueprint that adds the option
* @return {YeomanCommand} this;
*/
addGeneratorOptions(options: Record<string, any>) {
options = options || {};
for (const [key, value] of Object.entries(options)) {
this._addGeneratorOption(key, value);
}
return this;
}
#findOption(arg: string) {
return this.options.find(option => option.short === arg || option.long === arg);
}
_addGeneratorOption(optionName: string, optionDefinition: any, additionalDescription = '') {
if (optionName === 'help') {
return;
}
const longOption = `--${optionName}`;
const existingOption = this.#findOption(longOption);
if (this.#findOption(longOption)) {
return existingOption;
}
let cmdString = '';
if (optionDefinition.alias) {
cmdString = `-${optionDefinition.alias}, `;
}
cmdString = `${cmdString}${longOption}`;
if (optionDefinition.type === String) {
cmdString = optionDefinition.required === false ? `${cmdString} [value]` : `${cmdString} <value>`;
} else if (optionDefinition.type === Array) {
cmdString = optionDefinition.required === false ? `${cmdString} [value...]` : `${cmdString} <value...>`;
}
return this.addOption(
new Option(cmdString, `${optionDefinition.description}${additionalDescription}`)
.default(optionDefinition.default)
.hideHelp(optionDefinition.hide),
);
}
}
/* Add Environment options */
export const addEnvironmentOptions = (command = new YeomanCommand()) =>
command
.option('--cwd', 'Path to use as current dir')
/* Environment options */
.option('--skip-install', 'Do not automatically install dependencies', false)
/* Generator options */
.option('--skip-cache', 'Do not remember prompt answers', false)
.option('--local-config-only', 'Generate .yo-rc-global.json locally', false)
.option('--ask-answered', 'Show prompts for already configured options', false)
/* Conflicter options */
.option('--force', 'Override every file', false)
.option('--dry-run', 'Print conflicts', false)
.option('--whitespace', 'Whitespace changes will not trigger conflicts', false)
.option('--bail', 'Fail on first conflict', false)
.option('--skip-yo-resolve', 'Ignore .yo-resolve files', false)
/* Hidden options, used for api */
.addOption(new Option('--skip-local-cache', 'Skip local answers cache').default(true).hideHelp())
.addOption(new Option('--skip-parse-options', 'Skip legacy options parsing').default(false).hideHelp())
.addOption(new Option('--experimental', 'Experimental features').default(false).hideHelp())
.addOption(new Option('--log-cwd', 'Path for log purpose').hideHelp());
@@ -0,0 +1,77 @@
import { parse } from 'node:path';
import slash from 'slash';
import { escapeRegExp, findLast } from 'lodash-es';
type AsNamespaceOptions = {
lookups?: string[];
};
export const defaultLookups = ['.', 'generators', 'lib/generators', 'dist/generators'];
/**
* Given a String `filepath`, tries to figure out the relative namespace.
*
* ### Examples:
*
* this.namespace('backbone/all/index.js');
* // => backbone:all
*
* this.namespace('generator-backbone/model');
* // => backbone:model
*
* this.namespace('backbone.js');
* // => backbone
*
* this.namespace('generator-mocha/backbone/model/index.js');
* // => mocha:backbone:model
*
* @param filepath
* @param lookups paths
*/
export const asNamespace = (filepath: string, { lookups = defaultLookups }: AsNamespaceOptions): string => {
if (!filepath) {
throw new Error('Missing file path');
}
// Normalize path
let ns = slash(filepath);
// Ignore path before latest node_modules
const nodeModulesPath = '/node_modules/';
if (ns.includes(nodeModulesPath)) {
ns = ns.slice(ns.lastIndexOf(nodeModulesPath) + nodeModulesPath.length);
}
// Cleanup extension and normalize path for differents OS
const parsed = parse(ns);
ns = parsed.dir ? `${parsed.dir}/${parsed.name}` : parsed.name;
// Sort lookups by length so biggest are removed first
const nsLookups = [...lookups, '..']
.map(found => slash(found))
.sort((a, b) => a.split('/').length - b.split('/').length)
.toReversed();
// If `ns` contains a lookup dir in its path, remove it.
for (const lookup of nsLookups) {
// Only match full directory (begin with leading slash or start of input, end with trailing slash)
ns = ns.replaceAll(new RegExp(`(?:/|^)${escapeRegExp(lookup)}(?=/)`, 'g'), '');
}
const folders = ns.split('/');
const scope = findLast(folders, folder => folder.startsWith('@'));
// Cleanup `ns` from unwanted parts and then normalize slashes to `:`
ns = ns
.replaceAll('//', '') // Remove double `/`
.replace(/(.*generator-)/, '') // Remove before `generator-`
.replace(/\/(index|main)$/, '') // Remove `/index` or `/main`
.replace(/^\//, '') // Remove leading `/`
.replaceAll(/\/+/g, ':'); // Replace slashes by `:`
if (scope) {
ns = `${scope}/${ns}`;
}
return ns;
};
@@ -0,0 +1,47 @@
import { dirname, extname, join, normalize, resolve, sep } from 'node:path';
import { realpathSync, statSync } from 'node:fs';
import untildify from 'untildify';
import { locatePathSync } from 'locate-path';
import { defaultExtensions } from '../generator-lookup.ts';
/**
* Resolve a module path
* @param specifier - Filepath or module name
* @return - The resolved path leading to the module
*/
export function resolveModulePath(specifier: string, resolvedOrigin?: string) {
let maybeResolved = specifier;
if (maybeResolved.startsWith('.')) {
if (resolvedOrigin) {
maybeResolved = resolve(dirname(resolvedOrigin), '..', maybeResolved);
} else {
throw new Error(`Specifier ${maybeResolved} could not be calculated`);
}
}
maybeResolved = untildify(maybeResolved);
maybeResolved = normalize(maybeResolved);
if (extname(maybeResolved) === '') {
maybeResolved += sep;
}
try {
let specStat = statSync(maybeResolved);
if (specStat.isSymbolicLink()) {
specStat = statSync(realpathSync(maybeResolved));
}
if (specStat.isFile()) {
return maybeResolved;
}
if (specStat.isDirectory()) {
return locatePathSync(defaultExtensions.map(extension => `index${extension}`).map(file => join(maybeResolved, file)));
}
} catch {
// ignore error
}
throw new Error(`Error resolving ${specifier}`);
}
@@ -0,0 +1,40 @@
import { execaSync } from 'execa';
export const execaOutput = (cmg: string, arguments_: string[]): string | undefined => {
try {
const { failed, stdout } = execaSync(cmg, arguments_, { encoding: 'utf8' });
if (!failed) {
return stdout;
}
} catch {
// ignore error
}
return;
};
/**
* Two-step argument splitting function that first splits arguments in quotes,
* and then splits up the remaining arguments if they are not part of a quote.
*/
export function splitArgsFromString(argumentsString: string | string[]): string[] {
if (Array.isArray(argumentsString)) {
return argumentsString;
}
let result: string[] = [];
if (!argumentsString) {
return result;
}
const quoteSeparatedArguments = argumentsString.split(/("[^"]*")/).filter(Boolean);
for (const argument of quoteSeparatedArguments) {
if (argument.includes('"')) {
result.push(argument.replaceAll('"', ''));
} else {
result = result.concat(argument.trim().split(' '));
}
}
return result;
}