yeoman-environment 8.0.0 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@ import { type FilePipelineTransform } from '@yeoman/transform';
9
9
  import { type YeomanNamespace } from '@yeoman/namespace';
10
10
  import { type ConflicterOptions } from '@yeoman/conflicter';
11
11
  import { ComposedStore } from './composed-store.ts';
12
- import Store, { type StoreGeneratorMeta } from './store.ts';
12
+ import Store, { type StoreGeneratorMeta, type StoreSharedLookupOptions } from './store.ts';
13
13
  import type YeomanCommand from './util/command.ts';
14
14
  import { type LookupOptions } from './generator-lookup.ts';
15
15
  import { type InstallTask } from './package-manager.ts';
@@ -26,10 +26,22 @@ export type EnvironmentOptions = ConflicterOptions & BaseEnvironmentOptions & Om
26
26
  yeomanRepository?: string;
27
27
  arboristRegistry?: string;
28
28
  nodePackageManager?: string;
29
+ /**
30
+ * @deprecated Use `storeOptions`.
31
+ */
29
32
  generatorLookupOptions?: Pick<EnvironmentLookupOptions, 'customizeNamespace' | 'lookups'>;
30
33
  /**
31
- * Generators store to use instead of a new one, to share looked up and registered generators with other
32
- * environments. Generators registered through this environment are added to it.
34
+ * Lookup options of the store of the environment, shared by every lookup. When a `store` is passed, the
35
+ * environment works on a clone of it with these options merged over its own.
36
+ *
37
+ * @experimental
38
+ */
39
+ storeOptions?: StoreSharedLookupOptions;
40
+ /**
41
+ * Generators store to start from instead of an empty one, to reuse the generators already looked up and registered
42
+ * in it. The environment works on a clone of it (see `Store#clone`), with `storeOptions` merged over its options:
43
+ * the generators the environment looks up or registers are added to its clone only, they are neither added to the
44
+ * store passed nor seen by the other environments created with it. The generators already imported are shared.
33
45
  *
34
46
  * @experimental
35
47
  */
@@ -57,7 +69,6 @@ export default class EnvironmentBase extends EventEmitter implements BaseEnviron
57
69
  protected command?: YeomanCommand;
58
70
  protected runLoop: GroupedQueue;
59
71
  protected composedStore: ComposedStore;
60
- protected lookups: string[];
61
72
  protected repository: FlyRepository;
62
73
  protected experimental: boolean;
63
74
  protected _rootGenerator?: BaseGenerator;
@@ -141,6 +152,10 @@ export default class EnvironmentBase extends EventEmitter implements BaseEnviron
141
152
  * @param {Array} lookups paths
142
153
  */
143
154
  namespace(filepath: string, lookups?: string[]): string;
155
+ /**
156
+ * @deprecated Use the lookups of the store.
157
+ */
158
+ protected get lookups(): string[];
144
159
  /**
145
160
  * Returns the environment or dependency version.
146
161
  * @param {String} packageName - Module to get version.
@@ -16,7 +16,6 @@ import {} from '@yeoman/conflicter';
16
16
  import { defaults, pick } from 'lodash-es';
17
17
  import { ComposedStore } from "./composed-store.js";
18
18
  import Store, {} from "./store.js";
19
- import { asNamespace, defaultLookups } from "./util/namespace.js";
20
19
  import {} from "./generator-lookup.js";
21
20
  import { UNKNOWN_NAMESPACE, UNKNOWN_RESOLVED, defaultQueues } from "./constants.js";
22
21
  import { resolveModulePath } from "./util/resolve.js";
@@ -97,7 +96,6 @@ export default class EnvironmentBase extends EventEmitter {
97
96
  command;
98
97
  runLoop;
99
98
  composedStore;
100
- lookups;
101
99
  repository;
102
100
  experimental;
103
101
  _rootGenerator;
@@ -106,12 +104,14 @@ export default class EnvironmentBase extends EventEmitter {
106
104
  constructor(options = {}) {
107
105
  super();
108
106
  this.setMaxListeners(100);
109
- const { cwd = process.cwd(), logCwd = cwd, sharedFs = createMemFs(), command, yeomanRepository, arboristRegistry, sharedOptions = {}, experimental, console: adapterConsole, stdin, stderr, stdout, adapter = new QueuedAdapter({ console: adapterConsole, stdin, stdout, stderr }), store, ...remainingOptions } = options;
107
+ const { cwd = process.cwd(), logCwd = cwd, sharedFs = createMemFs(), command, yeomanRepository, arboristRegistry, sharedOptions = {}, experimental, console: adapterConsole, stdin, stderr, stdout, adapter = new QueuedAdapter({ console: adapterConsole, stdin, stdout, stderr }), store, storeOptions, ...remainingOptions } = options;
110
108
  this.options = remainingOptions;
111
109
  this.adapter = adapter;
112
110
  this.cwd = resolve(cwd);
113
111
  this.logCwd = logCwd;
114
- this.store = store ?? new Store(this);
112
+ const lookupOptions = { ...remainingOptions.generatorLookupOptions, ...storeOptions };
113
+ // A store passed is cloned: what this environment looks up or registers does not change it.
114
+ this.store = store ? store.clone(this, lookupOptions) : new Store(this, lookupOptions);
115
115
  this.command = command;
116
116
  this.runLoop = new GroupedQueue(defaultQueues, false);
117
117
  this.composedStore = new ComposedStore({ log: this.adapter.log });
@@ -120,7 +120,6 @@ export default class EnvironmentBase extends EventEmitter {
120
120
  // Node won't complain about event listeners leaks.
121
121
  this.runLoop.setMaxListeners(0);
122
122
  this.sharedFs.setMaxListeners(0);
123
- this.lookups = this.options.generatorLookupOptions?.lookups ?? defaultLookups;
124
123
  this.sharedOptions = sharedOptions;
125
124
  // Create a default sharedData.
126
125
  this.sharedOptions.sharedData = this.sharedOptions.sharedData ?? {};
@@ -297,8 +296,14 @@ export default class EnvironmentBase extends EventEmitter {
297
296
  * @param {String} filepath
298
297
  * @param {Array} lookups paths
299
298
  */
300
- namespace(filepath, lookups = this.lookups) {
301
- return asNamespace(filepath, { lookups });
299
+ namespace(filepath, lookups) {
300
+ return this.store.namespace(filepath, lookups);
301
+ }
302
+ /**
303
+ * @deprecated Use the lookups of the store.
304
+ */
305
+ get lookups() {
306
+ return this.store.lookups;
302
307
  }
303
308
  getVersion(packageName) {
304
309
  if (packageName && packageName !== 'yeoman-environment') {
@@ -426,12 +431,7 @@ export default class EnvironmentBase extends EventEmitter {
426
431
  * registered as `dummy:yo` generator.
427
432
  */
428
433
  async lookup(options) {
429
- return this.store.lookupSync({
430
- customizeNamespace: this.options.generatorLookupOptions?.customizeNamespace,
431
- lookups: this.lookups,
432
- ...(options ?? { localOnly: false }),
433
- env: this.asEnvironment(),
434
- });
434
+ return this.store.lookupSync({ ...options, env: this.asEnvironment() });
435
435
  }
436
436
  /**
437
437
  * Verify if a package namespace already have been registered.
@@ -8,6 +8,7 @@ import {} from "./generator-lookup.js";
8
8
  import YeomanCommand from "./util/command.js";
9
9
  import EnvironmentBase, {} from "./environment-base.js";
10
10
  import { splitArgsFromString as splitArgumentsFromString } from "./util/util.js";
11
+ import { nestedGeneratorsFolder } from "./util/namespace.js";
11
12
  class FullEnvironment extends EnvironmentBase {
12
13
  constructor(options = {}, adapterCompat) {
13
14
  if (adapterCompat) {
@@ -262,9 +263,13 @@ class FullEnvironment extends EnvironmentBase {
262
263
  const nsOptions = { packagePatterns: [ns.generatorHint] };
263
264
  if (ns.generator) {
264
265
  // Build filePatterns to look specifically for the namespace.
265
- const genPath = ns.generator.split(':').join('/');
266
- let filePatterns = [`${genPath}/index.?s`, `${genPath}.?s`];
267
- const lookups = options.lookups ?? this.lookups;
266
+ const generatorPaths = [ns.generator.split(':').join('/')];
267
+ if (options.nestedGenerators ?? this.store.lookupOptions.nestedGenerators) {
268
+ // `app:sub` is `app/generators/sub` too.
269
+ generatorPaths.push(ns.generator.split(':').join(`/${nestedGeneratorsFolder}/`));
270
+ }
271
+ let filePatterns = generatorPaths.flatMap(genPath => [`${genPath}/index.?s`, `${genPath}.?s`]);
272
+ const lookups = options.lookups ?? this.store.lookups;
268
273
  filePatterns = lookups.flatMap(prefix => filePatterns.map(pattern => join(prefix, pattern)));
269
274
  nsOptions.filePatterns = filePatterns;
270
275
  nsOptions.singleResult = true;
@@ -2,6 +2,11 @@ import type { LookupOptions as LookupOptionsApi } from '@yeoman/types';
2
2
  import { type ModuleLookupOptions } from './module-lookup.ts';
3
3
  export type LookupOptions = LookupOptionsApi & ModuleLookupOptions & {
4
4
  lookups?: string[];
5
+ /**
6
+ * Look up the generators nested in the `generators` folder of a generator too:
7
+ * `generators/server/generators/cache` is registered as `server:cache`.
8
+ */
9
+ nestedGenerators?: boolean;
5
10
  };
6
11
  type LookupMeta = {
7
12
  filePath: string;
@@ -49,13 +54,13 @@ export declare function lookupGeneratorsSync(options?: LookupOptions, register?:
49
54
  * @param {Boolean} [options.singleResult=true] - Set false to return multiple values.
50
55
  * @return {String} generator
51
56
  */
52
- export declare function lookupGenerator(namespace: string, options?: ModuleLookupOptions & {
57
+ export declare function lookupGenerator(namespace: string, options?: ModuleLookupOptions & Pick<LookupOptions, 'nestedGenerators'> & {
53
58
  packagePath?: boolean;
54
59
  generatorPath?: boolean;
55
60
  } & {
56
61
  singleResult?: true;
57
62
  }): string;
58
- export declare function lookupGenerator(namespace: string, options?: ModuleLookupOptions & {
63
+ export declare function lookupGenerator(namespace: string, options?: ModuleLookupOptions & Pick<LookupOptions, 'nestedGenerators'> & {
59
64
  packagePath?: boolean;
60
65
  generatorPath?: boolean;
61
66
  } & {
@@ -2,7 +2,7 @@ import { extname, isAbsolute, join, posix } from 'node:path';
2
2
  import { pathToFileURL } from 'node:url';
3
3
  import { requireNamespace, toNamespace } from '@yeoman/namespace';
4
4
  import { findPackagesIn, getNpmPaths, moduleLookupSync } from "./module-lookup.js";
5
- import { defaultLookups } from "./util/namespace.js";
5
+ import { defaultLookups, withNestedLookups } from "./util/namespace.js";
6
6
  import Store from "./store.js";
7
7
  export const defaultExtensions = ['.ts', '.cts', '.mts', '.js', '.cjs', '.mjs'];
8
8
  /**
@@ -30,10 +30,10 @@ export const defaultExtensions = ['.ts', '.cts', '.mts', '.js', '.cjs', '.mjs'];
30
30
  * @return {Object[]} List of generators
31
31
  */
32
32
  export function lookupGeneratorsSync(options = {}, register) {
33
- const { lookups = defaultLookups } = options;
33
+ const { lookups = defaultLookups, nestedGenerators } = options;
34
34
  options = {
35
35
  // Js generators should be after, last will override registered one.
36
- filePatterns: lookups.flatMap(prefix => defaultExtensions.map(extension => `${prefix}/*/index${extension}`)),
36
+ filePatterns: withNestedLookups(lookups, nestedGenerators).flatMap(prefix => defaultExtensions.map(extension => `${prefix}/*/index${extension}`)),
37
37
  filterPaths: false,
38
38
  packagePatterns: ['generator-*'],
39
39
  reverse: !options.singleResult,
@@ -64,7 +64,7 @@ export function lookupGenerator(namespace, options) {
64
64
  }
65
65
  // The generators are looked up by a store of their own, which keeps the ones asked for.
66
66
  const generators = new Store().lookupSync({
67
- filePatterns: defaultLookups.map(prefix => join(prefix, '*/index.{js,ts}')),
67
+ filePatterns: withNestedLookups(defaultLookups, lookupOptions.nestedGenerators).map(prefix => join(prefix, '*/index.{js,ts}')),
68
68
  reverse: false,
69
69
  ...lookupOptions,
70
70
  lookups: defaultLookups,
package/dist/index.d.ts CHANGED
@@ -2,11 +2,11 @@ import { type EnvironmentOptions } from './environment-base.ts';
2
2
  import Environment from './environment-full.ts';
3
3
  export { default } from './environment-full.ts';
4
4
  export { default as EnvironmentBase, type EnvironmentOptions } from './environment-base.ts';
5
- export { default as Store, type StoreEnvironmentOptions, type StoreGeneratorMeta, type StoreLookupGeneratorMeta, type StoreLookupOptions, } from './store.ts';
5
+ export { default as Store, type StoreEnvironmentOptions, type StoreGeneratorMeta, type StoreLookupGeneratorMeta, type StoreLookupOptions, type StoreSharedLookupOptions, } from './store.ts';
6
6
  export declare const createEnv: (options?: EnvironmentOptions) => Environment;
7
7
  export declare const enforceUpdate: () => void;
8
- export * from './commands.ts';
9
- export * from './util/command.ts';
10
- export * from './package-manager.ts';
11
- export * from './commit.ts';
8
+ export { type CommandPreparation, prepareCommand, prepareGeneratorCommand } from './commands.ts';
9
+ export { addEnvironmentOptions } from './util/command.ts';
10
+ export { type InstallTask, type PackageManagerInstallTaskOptions, packageManagerInstallTask } from './package-manager.ts';
11
+ export { commitSharedFsTask } from './commit.ts';
12
12
  export { lookupGenerator } from './generator-lookup.ts';
package/dist/index.js CHANGED
@@ -7,8 +7,8 @@ export { default as Store, } from "./store.js";
7
7
  export const createEnv = (options) => new Environment(options);
8
8
  // Backward compatibility
9
9
  export const enforceUpdate = () => { };
10
- export * from "./commands.js";
11
- export * from "./util/command.js";
12
- export * from "./package-manager.js";
13
- export * from "./commit.js";
10
+ export { prepareCommand, prepareGeneratorCommand } from "./commands.js";
11
+ export { addEnvironmentOptions } from "./util/command.js";
12
+ export { packageManagerInstallTask } from "./package-manager.js";
13
+ export { commitSharedFsTask } from "./commit.js";
14
14
  export { lookupGenerator } from "./generator-lookup.js";
package/dist/store.d.ts CHANGED
@@ -13,9 +13,16 @@ export type StoreEnvironmentOptions = {
13
13
  export type StoreLookupOptions = LookupOptions & StoreEnvironmentOptions & {
14
14
  registerToScope?: string;
15
15
  customizeNamespace?: (ns?: string) => string | undefined;
16
+ /** Derive the namespace from the name in the package.json instead of the package directory. */
17
+ usePackageName?: boolean;
16
18
  /** Generators to keep, the others are neither registered nor returned. */
17
19
  filter?: (generator: FoundGenerator) => boolean;
18
20
  };
21
+ /**
22
+ * Lookup options shared by every lookup of a store, the options passed to a lookup take precedence.
23
+ * What to look up (paths, package patterns and `singleResult`) is given by each lookup.
24
+ */
25
+ export type StoreSharedLookupOptions = Pick<StoreLookupOptions, 'lookups' | 'localOnly' | 'filterPaths' | 'customizeNamespace' | 'filter' | 'usePackageName' | 'nestedGenerators'>;
19
26
  /** A generator found by a lookup, `registered` tells if it was added to the store. */
20
27
  export type StoreLookupGeneratorMeta = FoundGenerator & ((StoreGeneratorMeta & {
21
28
  registered: true;
@@ -53,7 +60,39 @@ export default class Store {
53
60
  private readonly _boundMetas;
54
61
  /** The environment used when none is passed to `importGenerator` or `instantiate`. */
55
62
  readonly environment?: BaseEnvironment;
56
- constructor(environment?: BaseEnvironment);
63
+ /** Options passed to every lookup, so consecutive lookups share the same configuration. */
64
+ readonly lookupOptions: StoreSharedLookupOptions;
65
+ constructor(environment?: BaseEnvironment, lookupOptions?: StoreSharedLookupOptions);
66
+ /** The lookups shared by the store, or the default ones. */
67
+ get lookups(): string[];
68
+ /**
69
+ * Given a String `filepath`, tries to figure out the relative namespace.
70
+ *
71
+ * ### Examples:
72
+ *
73
+ * store.namespace('backbone/all/index.js');
74
+ * // => backbone:all
75
+ *
76
+ * store.namespace('generator-backbone/model');
77
+ * // => backbone:model
78
+ *
79
+ * store.namespace('backbone.js');
80
+ * // => backbone
81
+ *
82
+ * store.namespace('generator-mocha/backbone/model/index.js');
83
+ * // => mocha:backbone:model
84
+ *
85
+ * @param filepath
86
+ * @param lookups - The lookups of the store if omitted
87
+ */
88
+ namespace(filepath: string, lookups?: string[]): string;
89
+ /**
90
+ * A copy of the store with the same generators, packages and lookup options, whose metas default to the given
91
+ * environment. The generators already imported are shared, what is added after cloning goes only to its store.
92
+ * @param environment - The environment of the clone, the environment of the store if omitted
93
+ * @param options - Lookup options of the clone, merged over the ones of the store
94
+ */
95
+ clone(environment?: BaseEnvironment | undefined, options?: StoreSharedLookupOptions): Store;
57
96
  /**
58
97
  * Store a module under the namespace key
59
98
  * @param meta
@@ -65,6 +104,7 @@ export default class Store {
65
104
  * The meta bound to an environment: its functions default to it. The meta of the environment the store was created
66
105
  * with is the meta itself, as it already defaults to it. A meta is bound once by environment, and delegates to the
67
106
  * meta of the store instead of copying it, so it keeps showing what the store has.
107
+ * The meta of the store a clone was created from, as the one a generator class keeps, is the meta of the clone.
68
108
  */
69
109
  bindMeta<M extends StoreGeneratorMeta>(meta: M, options?: StoreEnvironmentOptions): M;
70
110
  bindMeta<M extends StoreGeneratorMeta>(meta: M | undefined, options?: StoreEnvironmentOptions): M | undefined;
@@ -80,6 +120,8 @@ export default class Store {
80
120
  *
81
121
  * So this index file `node_modules/generator-dummy/lib/generators/yo/index.js` would be
82
122
  * registered as `dummy:yo` generator.
123
+ *
124
+ * The options are merged over the lookup options the store was created with.
83
125
  */
84
126
  lookupSync(options?: StoreLookupOptions): StoreLookupGeneratorMeta[];
85
127
  /**
@@ -131,10 +173,19 @@ export default class Store {
131
173
  * Read a package.json from packagePath. Parsed results are cached by packagePath.
132
174
  */
133
175
  private getPackageJson;
176
+ /**
177
+ * The meta of the store sharing its operations with the given meta, coming from the store it was cloned from or from
178
+ * one of its clones, or the given meta.
179
+ */
180
+ private ownMeta;
134
181
  /**
135
182
  * The functions of a meta that need an environment, defaulting to the given one.
136
183
  */
137
184
  private bindMetaFunctions;
185
+ /**
186
+ * The functions of a meta, running its operations in the environment passed as `{ env }`, or the default one.
187
+ */
188
+ private createMetaFunctions;
138
189
  private getFactory;
139
190
  private _getGenerator;
140
191
  }
package/dist/store.js CHANGED
@@ -10,12 +10,25 @@ import { pathToFileURL } from 'node:url';
10
10
  import { basename, extname, join, relative } from 'node:path';
11
11
  import { createRequire } from 'node:module';
12
12
  import { readFileSync, realpathSync } from 'node:fs';
13
- import { toNamespace } from '@yeoman/namespace';
13
+ import { namespaceFromPackageName, toNamespace } from '@yeoman/namespace';
14
14
  import createDebug from 'debug';
15
15
  import { lookupGeneratorsSync } from "./generator-lookup.js";
16
16
  import { asNamespace, defaultLookups } from "./util/namespace.js";
17
17
  const debug = createDebug('yeoman:environment:store');
18
18
  const require = createRequire(import.meta.url);
19
+ // The operations of a meta, kept on the meta itself: the meta of a clone has the operations of the meta it was cloned from.
20
+ const operationsKey = Symbol('operations');
21
+ const setOperations = (meta, operations) => Object.defineProperty(meta, operationsKey, { value: operations });
22
+ const getOperations = (meta) => meta[operationsKey];
23
+ /** The namespace of a generator package, like `@scope/foo` for `@scope/generator-foo`, undefined for another name. */
24
+ const namespaceOfPackage = (packageName) => {
25
+ try {
26
+ return namespaceFromPackageName(packageName).namespace;
27
+ }
28
+ catch {
29
+ return undefined;
30
+ }
31
+ };
19
32
  /**
20
33
  * The Generator store
21
34
  * This is used to store generator (npm packages) reference and instantiate them when
@@ -39,8 +52,60 @@ export default class Store {
39
52
  _boundMetas = new WeakMap();
40
53
  /** The environment used when none is passed to `importGenerator` or `instantiate`. */
41
54
  environment;
42
- constructor(environment) {
55
+ /** Options passed to every lookup, so consecutive lookups share the same configuration. */
56
+ lookupOptions;
57
+ constructor(environment, lookupOptions = {}) {
43
58
  this.environment = environment;
59
+ this.lookupOptions = lookupOptions;
60
+ }
61
+ /** The lookups shared by the store, or the default ones. */
62
+ get lookups() {
63
+ return this.lookupOptions.lookups ?? defaultLookups;
64
+ }
65
+ /**
66
+ * Given a String `filepath`, tries to figure out the relative namespace.
67
+ *
68
+ * ### Examples:
69
+ *
70
+ * store.namespace('backbone/all/index.js');
71
+ * // => backbone:all
72
+ *
73
+ * store.namespace('generator-backbone/model');
74
+ * // => backbone:model
75
+ *
76
+ * store.namespace('backbone.js');
77
+ * // => backbone
78
+ *
79
+ * store.namespace('generator-mocha/backbone/model/index.js');
80
+ * // => mocha:backbone:model
81
+ *
82
+ * @param filepath
83
+ * @param lookups - The lookups of the store if omitted
84
+ */
85
+ namespace(filepath, lookups = this.lookups) {
86
+ return asNamespace(filepath, { lookups, nestedGenerators: this.lookupOptions.nestedGenerators });
87
+ }
88
+ /**
89
+ * A copy of the store with the same generators, packages and lookup options, whose metas default to the given
90
+ * environment. The generators already imported are shared, what is added after cloning goes only to its store.
91
+ * @param environment - The environment of the clone, the environment of the store if omitted
92
+ * @param options - Lookup options of the clone, merged over the ones of the store
93
+ */
94
+ clone(environment = this.environment, options) {
95
+ const store = new Store(environment, { ...this.lookupOptions, ...options });
96
+ for (const [namespace, meta] of Object.entries(this._meta)) {
97
+ const operations = getOperations(meta);
98
+ store._meta[namespace] = setOperations({
99
+ ...meta,
100
+ ...store.createMetaFunctions(operations, () => store.environment),
101
+ getPackageJson: () => store.getPackageJson(meta.packagePath),
102
+ }, operations);
103
+ }
104
+ for (const [packageNamespace, packagePaths] of Object.entries(this._packagesPaths)) {
105
+ store._packagesPaths[packageNamespace] = [...packagePaths];
106
+ }
107
+ store._packagesNS.push(...this._packagesNS);
108
+ return store;
44
109
  }
45
110
  /**
46
111
  * Store a module under the namespace key
@@ -138,32 +203,31 @@ export default class Store {
138
203
  createdGenerators.set(environment, creating);
139
204
  return creating;
140
205
  };
141
- const importGenerator = (({ env: environment = this.environment } = {}) => {
206
+ const importGenerator = (environment) => {
142
207
  const importing = importGeneratorModule();
143
208
  return importing ? importing.then(() => getGenerator(environment)) : getGenerator(environment);
144
- });
145
- const instantiate = async (arguments_ = [], options = {}, { env: environment = this.environment } = {}) => {
209
+ };
210
+ const instantiate = async (arguments_ = [], options = {}, environment) => {
146
211
  if (!environment) {
147
212
  throw new Error(`An environment is required to instantiate the generator ${meta.namespace}`);
148
213
  }
149
- return environment.instantiate(await importGenerator({ env: environment }), {
214
+ return environment.instantiate(await importGenerator(environment), {
150
215
  generatorArgs: arguments_,
151
216
  generatorOptions: options,
152
217
  });
153
218
  };
154
- const instantiateHelp = async (options) => instantiate([], { help: true }, options);
219
+ const operations = { importGenerator, instantiate };
155
220
  const getPackageJson = () => this.getPackageJson(meta.packagePath);
156
221
  const { packageNamespace } = toNamespace(meta.namespace) ?? {};
157
222
  generatorMeta = {
158
223
  ...meta,
159
- importGenerator,
224
+ ...this.createMetaFunctions(operations, () => this.environment),
160
225
  requireModule,
161
226
  importModule,
162
- instantiate,
163
- instantiateHelp,
164
227
  getPackageJson,
165
228
  packageNamespace,
166
229
  };
230
+ setOperations(generatorMeta, operations);
167
231
  this._meta[meta.namespace] = generatorMeta;
168
232
  if (packageNamespace) {
169
233
  this.addPackageNamespace(packageNamespace);
@@ -174,6 +238,7 @@ export default class Store {
174
238
  return this.bindMeta(generatorMeta, { env });
175
239
  }
176
240
  bindMeta(meta, { env } = {}) {
241
+ meta = this.ownMeta(meta);
177
242
  if (!meta || !env || env === this.environment) {
178
243
  return meta;
179
244
  }
@@ -201,22 +266,40 @@ export default class Store {
201
266
  *
202
267
  * So this index file `node_modules/generator-dummy/lib/generators/yo/index.js` would be
203
268
  * registered as `dummy:yo` generator.
269
+ *
270
+ * The options are merged over the lookup options the store was created with.
204
271
  */
205
272
  lookupSync(options) {
206
- const { registerToScope, customizeNamespace = (ns) => ns, filter, env, lookups = defaultLookups, ...remainingOptions } = options ?? { localOnly: false };
273
+ const { registerToScope, customizeNamespace = (ns) => ns, usePackageName, filter, env, lookups = this.lookups, ...remainingOptions } = { ...this.lookupOptions, ...options };
207
274
  const lookupOptions = { ...remainingOptions, lookups };
208
275
  const generators = [];
276
+ const { nestedGenerators } = lookupOptions;
209
277
  lookupGeneratorsSync(lookupOptions, ({ packagePath, filePath, lookups }) => {
210
- let repositoryPath = join(packagePath, '..');
211
- if (basename(repositoryPath).startsWith('@')) {
212
- // Scoped package
213
- repositoryPath = join(repositoryPath, '..');
214
- }
215
- let namespace = customizeNamespace(asNamespace(relative(repositoryPath, filePath), { lookups }));
278
+ let namespace;
216
279
  try {
280
+ const packageName = usePackageName ? this.getPackageJson(packagePath)?.name : undefined;
281
+ const packageNamespace = packageName ? namespaceOfPackage(packageName) : undefined;
282
+ let foundNamespace;
283
+ if (packageNamespace) {
284
+ // The namespace of the package, then the generator from its path in the package.
285
+ const generatorPath = join('generator-package', relative(packagePath, filePath));
286
+ foundNamespace = `${packageNamespace}${asNamespace(generatorPath, { lookups, nestedGenerators }).slice('package'.length)}`;
287
+ }
288
+ else if (packageName) {
289
+ foundNamespace = asNamespace(join(packageName, relative(packagePath, filePath)), { lookups, nestedGenerators });
290
+ }
291
+ else {
292
+ let repositoryPath = join(packagePath, '..');
293
+ if (basename(repositoryPath).startsWith('@')) {
294
+ // Scoped package
295
+ repositoryPath = join(repositoryPath, '..');
296
+ }
297
+ foundNamespace = asNamespace(relative(repositoryPath, filePath), { lookups, nestedGenerators });
298
+ }
299
+ namespace = customizeNamespace(foundNamespace);
217
300
  const resolved = realpathSync(filePath);
218
301
  if (!namespace) {
219
- namespace = customizeNamespace(asNamespace(resolved, { lookups }));
302
+ namespace = customizeNamespace(asNamespace(resolved, { lookups, nestedGenerators }));
220
303
  }
221
304
  namespace = namespace;
222
305
  if (registerToScope && !namespace.startsWith('@')) {
@@ -348,14 +431,31 @@ export default class Store {
348
431
  }
349
432
  return this._packagesJson.get(packagePath);
350
433
  }
434
+ /**
435
+ * The meta of the store sharing its operations with the given meta, coming from the store it was cloned from or from
436
+ * one of its clones, or the given meta.
437
+ */
438
+ ownMeta(meta) {
439
+ if (!meta) {
440
+ return meta;
441
+ }
442
+ const own = this._meta[meta.namespace];
443
+ return own && own !== meta && getOperations(own) === getOperations(meta) ? own : meta;
444
+ }
351
445
  /**
352
446
  * The functions of a meta that need an environment, defaulting to the given one.
353
447
  */
354
448
  bindMetaFunctions(meta, env) {
449
+ return this.createMetaFunctions(getOperations(meta), () => env);
450
+ }
451
+ /**
452
+ * The functions of a meta, running its operations in the environment passed as `{ env }`, or the default one.
453
+ */
454
+ createMetaFunctions(operations, getEnvironment) {
355
455
  return {
356
- importGenerator: (({ env: environment = env } = {}) => meta.importGenerator({ env: environment })),
357
- instantiate: (arguments_, options, { env: environment = env } = {}) => meta.instantiate(arguments_, options, { env: environment }),
358
- instantiateHelp: ({ env: environment = env } = {}) => meta.instantiateHelp({ env: environment }),
456
+ importGenerator: (({ env = getEnvironment() } = {}) => operations.importGenerator(env)),
457
+ instantiate: async (arguments_, options, { env = getEnvironment() } = {}) => operations.instantiate(arguments_, options, env),
458
+ instantiateHelp: async ({ env = getEnvironment() } = {}) => operations.instantiate([], { help: true }, env),
359
459
  };
360
460
  }
361
461
  getFactory(module) {
@@ -1,6 +1,15 @@
1
1
  type AsNamespaceOptions = {
2
2
  lookups?: string[];
3
+ /** Generators nested in the `generators` folder of a generator: `app/generators/sub` is `app:sub`. */
4
+ nestedGenerators?: boolean;
3
5
  };
6
+ /** The folder holding the generators nested in a generator. */
7
+ export declare const nestedGeneratorsFolder = "generators";
8
+ /**
9
+ * The lookups of the generators, with the lookups of the generators nested in them if `nestedGenerators`:
10
+ * `generators` and `generators/*\/generators`.
11
+ */
12
+ export declare const withNestedLookups: (lookups: string[], nestedGenerators?: boolean) => string[];
4
13
  export declare const defaultLookups: string[];
5
14
  /**
6
15
  * Given a String `filepath`, tries to figure out the relative namespace.
@@ -21,6 +30,7 @@ export declare const defaultLookups: string[];
21
30
  *
22
31
  * @param filepath
23
32
  * @param lookups paths
33
+ * @throws if the path is in a scoped package that is not a `generator-` one
24
34
  */
25
- export declare const asNamespace: (filepath: string, { lookups }: AsNamespaceOptions) => string;
35
+ export declare const asNamespace: (filepath: string, { lookups, nestedGenerators }: AsNamespaceOptions) => string;
26
36
  export {};
@@ -1,6 +1,13 @@
1
- import { parse } from 'node:path';
1
+ import { parse, posix } from 'node:path';
2
2
  import slash from 'slash';
3
3
  import { escapeRegExp, findLast } from 'lodash-es';
4
+ /** The folder holding the generators nested in a generator. */
5
+ export const nestedGeneratorsFolder = 'generators';
6
+ /**
7
+ * The lookups of the generators, with the lookups of the generators nested in them if `nestedGenerators`:
8
+ * `generators` and `generators/*\/generators`.
9
+ */
10
+ export const withNestedLookups = (lookups, nestedGenerators) => nestedGenerators ? lookups.flatMap(lookup => [lookup, posix.join(lookup, '*', nestedGeneratorsFolder)]) : lookups;
4
11
  export const defaultLookups = ['.', 'generators', 'lib/generators', 'dist/generators'];
5
12
  /**
6
13
  * Given a String `filepath`, tries to figure out the relative namespace.
@@ -21,8 +28,9 @@ export const defaultLookups = ['.', 'generators', 'lib/generators', 'dist/genera
21
28
  *
22
29
  * @param filepath
23
30
  * @param lookups paths
31
+ * @throws if the path is in a scoped package that is not a `generator-` one
24
32
  */
25
- export const asNamespace = (filepath, { lookups = defaultLookups }) => {
33
+ export const asNamespace = (filepath, { lookups = defaultLookups, nestedGenerators }) => {
26
34
  if (!filepath) {
27
35
  throw new Error('Missing file path');
28
36
  }
@@ -37,7 +45,8 @@ export const asNamespace = (filepath, { lookups = defaultLookups }) => {
37
45
  const parsed = parse(ns);
38
46
  ns = parsed.dir ? `${parsed.dir}/${parsed.name}` : parsed.name;
39
47
  // Sort lookups by length so biggest are removed first
40
- const nsLookups = [...lookups, '..']
48
+ // The folder of the nested generators is removed like a lookup.
49
+ const nsLookups = [...lookups, ...(nestedGenerators ? [nestedGeneratorsFolder] : []), '..']
41
50
  .map(found => slash(found))
42
51
  .toSorted((a, b) => a.split('/').length - b.split('/').length)
43
52
  .toReversed();
@@ -51,11 +60,17 @@ export const asNamespace = (filepath, { lookups = defaultLookups }) => {
51
60
  // Cleanup `ns` from unwanted parts and then normalize slashes to `:`
52
61
  ns = ns
53
62
  .replaceAll('//', '') // Remove double `/`
54
- .replace(/(.*generator-)/, '') // Remove before `generator-`
63
+ // Remove the path before the package, the last folder starting with `generator-`, and its `generator-` prefix:
64
+ // a `generator-` inside the name of the package or of a generator is kept.
65
+ .replace(/^(?:.*\/)?generator-/, '')
55
66
  .replace(/\/(index|main)$/, '') // Remove `/index` or `/main`
56
67
  .replace(/^\//, '') // Remove leading `/`
57
68
  .replaceAll(/\/+/g, ':'); // Replace slashes by `:`
58
69
  if (scope) {
70
+ // The path up to the scope is left when the package is not a `generator-` one.
71
+ if (new RegExp(`(?:^|:)${escapeRegExp(scope)}:`).test(ns)) {
72
+ throw new Error(`The scoped package of ${filepath} is not a generator- package, it has no namespace.`);
73
+ }
59
74
  ns = `${scope}/${ns}`;
60
75
  }
61
76
  return ns;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yeoman-environment",
3
- "version": "8.0.0",
3
+ "version": "8.2.0",
4
4
  "description": "Handles the lifecyle and bootstrapping of generators in a specific environment",
5
5
  "keywords": [
6
6
  "development",