T3MP3ST public launch build
This commit is contained in:
1 parent
972247b881
commit
184c7d4e59
1589 files changed
+57150
-506
No files matched your search
@@ -0,0 +1,94 @@
|
||||
--- src/environment-full.ts
|
||||
@@ -226,9 +226,23 @@ class FullEnvironment extends EnvironmentBase {
|
||||
* @param {Object} packages - packages to install key(packageName): value(versionRange).
|
||||
* @return {Boolean} - true if the install succeeded.
|
||||
*/
|
||||
- async installLocalGenerators(packages: Record<string, string | undefined>) {
|
||||
+ async installLocalGenerators(packages: Record<string, string | undefined>, forceInstall = false) {
|
||||
const entries = Object.entries(packages);
|
||||
const specs = entries.map(([packageName, version]) => `${packageName}${version ? `@${version}` : ''}`);
|
||||
+ if (forceInstall) {
|
||||
+ this.adapter.log.info(`Force install is enabled. Proceeding with installation.`);
|
||||
+ } else {
|
||||
+ const { aproveInstall } = await this.adapter.prompt({
|
||||
+ message: `The following packages need to be installed in the local repository: ${specs.join(', ')}. Do you want to proceed?`,
|
||||
+ type: 'confirm',
|
||||
+ name: 'aproveInstall',
|
||||
+ default: false,
|
||||
+ });
|
||||
+ if (!aproveInstall) {
|
||||
+ throw new Error(`Installation of ${specs.join(', ')} is declined by the user. Install manually and try again.`);
|
||||
+ }
|
||||
+ }
|
||||
+ this.adapter.log.info(`The following packages will be installed in the local repository: ${specs.join(', ')}.`);
|
||||
const installResult = await this.repository.install(specs);
|
||||
const failToInstall = installResult.find(result => !result.path);
|
||||
if (failToInstall) {
|
||||
|
||||
--- test/command.js
|
||||
@@ -3,7 +3,8 @@ import path, { dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createRequire } from 'node:module';
|
||||
import { stub } from 'sinon';
|
||||
-import { beforeEach, describe, it } from 'esmocha';
|
||||
+import { beforeEach, describe, esmocha, expect, it } from 'esmocha';
|
||||
+import { TestAdapter } from 'yeoman-test';
|
||||
import { prepareCommand } from '../src/commands.ts';
|
||||
import Environment from '../src/index.ts';
|
||||
|
||||
@@ -12,11 +13,45 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
describe('environment (command)', () => {
|
||||
+ describe('#execute()', () => {
|
||||
+ let environment;
|
||||
+ let adapter;
|
||||
+
|
||||
+ beforeEach(async () => {
|
||||
+ adapter = new TestAdapter();
|
||||
+ environment = new Environment({ skipInstall: true, dryRun: true, adapter });
|
||||
+ });
|
||||
+
|
||||
+ describe('with non existing generator', () => {
|
||||
+ it('declining installation', async () => {
|
||||
+ adapter.addAnswers({
|
||||
+ aproveInstall: false,
|
||||
+ });
|
||||
+ environment.repository.install = esmocha.fn().mockReturnValue(Promise.resolve([]));
|
||||
+ await expect(environment.execute('commands:options')).rejects.toThrow(
|
||||
+ /Installation of generator-commands is declined by the user. Install manually and try again/,
|
||||
+ );
|
||||
+ expect(environment.repository.install).not.toHaveBeenCalled();
|
||||
+ });
|
||||
+
|
||||
+ it('approving installation', async () => {
|
||||
+ adapter.addAnswers({
|
||||
+ aproveInstall: true,
|
||||
+ });
|
||||
+ environment.repository.install = esmocha.fn().mockReturnValue(Promise.resolve([]));
|
||||
+ await expect(environment.execute('commands:options')).rejects.toThrow(
|
||||
+ /You don't seem to have a generator with the name “generator-commands” installed./,
|
||||
+ );
|
||||
+ expect(environment.repository.install).toHaveBeenCalledWith(['generator-commands']);
|
||||
+ });
|
||||
+ });
|
||||
+ });
|
||||
+
|
||||
describe('#execute() with options', () => {
|
||||
let environment;
|
||||
|
||||
beforeEach(async () => {
|
||||
- environment = new Environment([], { skipInstall: true, dryRun: true });
|
||||
+ environment = new Environment({ skipInstall: true, dryRun: true });
|
||||
environment.adapter.log = stub();
|
||||
await environment.register(path.join(__dirname, 'fixtures/generator-commands/generators/options'));
|
||||
});
|
||||
@@ -87,7 +122,7 @@ describe('environment (command)', () => {
|
||||
let environment;
|
||||
|
||||
beforeEach(() => {
|
||||
- environment = new Environment([], { skipInstall: true, dryRun: true });
|
||||
+ environment = new Environment({ skipInstall: true, dryRun: true });
|
||||
environment.adapter.log = stub();
|
||||
environment.register(path.join(__dirname, 'fixtures/generator-commands/generators/arguments'));
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{"ghsa": "GHSA-vv9j-gjw2-j8wp", "cve": "CVE-2026-42089", "repo": "yeoman/environment", "severity": "high", "cwes": ["CWE-829"], "summary": "yeoman-environment Vulnerable to Arbitrary Package Installation without User Confirmation", "fix_commit": "78d2af7e60294784b8a8b3b3b5099c6874b6a1fa", "parent_commit": "dccfd126c313afa2b33ea0c4cc39a2c242fedcd8", "vuln_files": ["src/environment-full.ts"], "vuln_lines": {"src/environment-full.ts": [229]}, "published": "2026-05-26T23:10:38Z", "source_files": 17, "scope_dirs": ["src"]}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"id": "GHSA-vv9j-gjw2-j8wp", "repo": "yeoman/environment", "scope": ["src"], "files": 17, "objective": "Audit this real-world source tree for the single most serious exploitable security vulnerability."}
|
||||
Reference in new issue
Block a user