yeoman-environment 6.3.0 → 7.1.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 from './store.ts';
12
+ import Store, { type StoreGeneratorMeta } 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';
@@ -27,6 +27,13 @@ export type EnvironmentOptions = ConflicterOptions & BaseEnvironmentOptions & Om
27
27
  arboristRegistry?: string;
28
28
  nodePackageManager?: string;
29
29
  generatorLookupOptions?: Pick<EnvironmentLookupOptions, 'customizeNamespace' | 'lookups'>;
30
+ /**
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.
33
+ *
34
+ * @experimental
35
+ */
36
+ store?: Store;
30
37
  };
31
38
  /**
32
39
  * Copy and remove null and undefined values
@@ -62,6 +69,13 @@ export default class EnvironmentBase extends EventEmitter implements BaseEnviron
62
69
  feature: any;
63
70
  }>;
64
71
  applyTransforms(transformStreams: FilePipelineTransform[], options?: ApplyTransformsOptions): Promise<void>;
72
+ /**
73
+ * The meta of a generator of the store for this environment.
74
+ */
75
+ protected bindMeta<M extends StoreGeneratorMeta>(meta: M): M & GeneratorMeta;
76
+ protected bindMeta<M extends StoreGeneratorMeta>(meta: M | undefined): (M & GeneratorMeta) | undefined;
77
+ private getStoreMeta;
78
+ protected asEnvironment(): BaseEnvironment;
65
79
  /**
66
80
  * @param namespaceOrPath
67
81
  * @return the generator meta registered under the namespace
@@ -1,8 +1,7 @@
1
1
  import EventEmitter from 'node:events';
2
2
  import { createRequire } from 'node:module';
3
- import { basename, isAbsolute, join, relative, resolve } from 'node:path';
3
+ import { isAbsolute, join, relative, resolve } from 'node:path';
4
4
  import process from 'node:process';
5
- import { realpathSync } from 'node:fs';
6
5
  import { QueuedAdapter } from '@yeoman/adapter';
7
6
  import { create as createMemFs } from 'mem-fs';
8
7
  import {} from 'mem-fs-editor';
@@ -16,9 +15,9 @@ import chalk from 'chalk';
16
15
  import {} from '@yeoman/conflicter';
17
16
  import { defaults, pick } from 'lodash-es';
18
17
  import { ComposedStore } from "./composed-store.js";
19
- import Store from "./store.js";
18
+ import Store, {} from "./store.js";
20
19
  import { asNamespace, defaultLookups } from "./util/namespace.js";
21
- import { lookupGenerators } from "./generator-lookup.js";
20
+ import {} from "./generator-lookup.js";
22
21
  import { UNKNOWN_NAMESPACE, UNKNOWN_RESOLVED, defaultQueues } from "./constants.js";
23
22
  import { resolveModulePath } from "./util/resolve.js";
24
23
  import { commitSharedFsTask } from "./commit.js";
@@ -107,12 +106,12 @@ export default class EnvironmentBase extends EventEmitter {
107
106
  constructor(options = {}) {
108
107
  super();
109
108
  this.setMaxListeners(100);
110
- 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 }), ...remainingOptions } = options;
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;
111
110
  this.options = remainingOptions;
112
111
  this.adapter = adapter;
113
112
  this.cwd = resolve(cwd);
114
113
  this.logCwd = logCwd;
115
- this.store = new Store(this);
114
+ this.store = store ?? new Store(this);
116
115
  this.command = command;
117
116
  this.runLoop = new GroupedQueue(defaultQueues, false);
118
117
  this.composedStore = new ComposedStore({ log: this.adapter.log });
@@ -155,6 +154,15 @@ export default class EnvironmentBase extends EventEmitter {
155
154
  ]);
156
155
  }, { name, disabled: !(options?.log ?? true) });
157
156
  }
157
+ bindMeta(meta) {
158
+ return this.store.bindMeta(meta, { env: this.asEnvironment() });
159
+ }
160
+ getStoreMeta(namespace) {
161
+ return this.store.getMeta(namespace, { env: this.asEnvironment() });
162
+ }
163
+ asEnvironment() {
164
+ return this;
165
+ }
158
166
  /**
159
167
  * @param namespaceOrPath
160
168
  * @return the generator meta registered under the namespace
@@ -167,16 +175,16 @@ export default class EnvironmentBase extends EventEmitter {
167
175
  const parsed = toNamespace(namespaceOrPath);
168
176
  if (typeof namespaceOrPath !== 'string' || parsed) {
169
177
  const ns = parsed.namespace;
170
- return this.store.getMeta(ns) ?? this.store.getMeta(this.alias(ns));
178
+ return this.getStoreMeta(ns) ?? this.getStoreMeta(this.alias(ns));
171
179
  }
172
- const maybeMeta = this.store.getMeta(namespaceOrPath) ?? this.store.getMeta(this.alias(namespaceOrPath));
180
+ const maybeMeta = this.getStoreMeta(namespaceOrPath) ?? this.getStoreMeta(this.alias(namespaceOrPath));
173
181
  if (maybeMeta) {
174
182
  return maybeMeta;
175
183
  }
176
184
  try {
177
185
  const resolved = resolveModulePath(namespaceOrPath);
178
186
  if (resolved) {
179
- return this.store.add({ resolved, namespace: this.namespace(resolved) });
187
+ return this.store.add({ resolved, namespace: this.namespace(resolved) }, undefined, { env: this.asEnvironment() });
180
188
  }
181
189
  }
182
190
  catch {
@@ -241,7 +249,9 @@ export default class EnvironmentBase extends EventEmitter {
241
249
  }
242
250
  instantiate(constructor, ...arguments_) {
243
251
  const composeOptions = arguments_.length > 0 ? getInstantiateOptions(...arguments_) : {};
244
- const { namespace = UNKNOWN_NAMESPACE, resolved = UNKNOWN_RESOLVED, _meta } = constructor;
252
+ const { namespace = UNKNOWN_NAMESPACE, resolved = UNKNOWN_RESOLVED } = constructor;
253
+ // The generator class keeps the meta of the store, which may be shared: the generator gets the one of this environment.
254
+ const _meta = this.bindMeta(constructor._meta);
245
255
  const environmentOptions = { env: this, resolved, namespace, _meta };
246
256
  const generator = new constructor(composeOptions.generatorArgs ?? [], {
247
257
  ...this.sharedOptions,
@@ -416,49 +426,12 @@ export default class EnvironmentBase extends EventEmitter {
416
426
  * registered as `dummy:yo` generator.
417
427
  */
418
428
  async lookup(options) {
419
- const { registerToScope, customizeNamespace = this.options.generatorLookupOptions?.customizeNamespace ?? ((ns) => ns), lookups = this.lookups, ...remainingOptions } = options ?? { localOnly: false };
420
- options = {
421
- ...remainingOptions,
422
- lookups,
423
- };
424
- const generators = [];
425
- await lookupGenerators(options, ({ packagePath, filePath, lookups }) => {
426
- let repositoryPath = join(packagePath, '..');
427
- if (basename(repositoryPath).startsWith('@')) {
428
- // Scoped package
429
- repositoryPath = join(repositoryPath, '..');
430
- }
431
- let namespace = customizeNamespace(asNamespace(relative(repositoryPath, filePath), { lookups }));
432
- try {
433
- const resolved = realpathSync(filePath);
434
- if (!namespace) {
435
- namespace = customizeNamespace(asNamespace(resolved, { lookups }));
436
- }
437
- namespace = namespace;
438
- if (registerToScope && !namespace.startsWith('@')) {
439
- namespace = `@${registerToScope}/${namespace}`;
440
- }
441
- const meta = this.store.add({ namespace, packagePath, resolved });
442
- if (meta) {
443
- generators.push({
444
- ...meta,
445
- registered: true,
446
- });
447
- return Boolean(options?.singleResult);
448
- }
449
- }
450
- catch (error) {
451
- console.error('Unable to register %s (Error: %s)', filePath, error);
452
- }
453
- generators.push({
454
- resolved: filePath,
455
- namespace: namespace,
456
- packagePath,
457
- registered: false,
458
- });
459
- return false;
429
+ return this.store.lookupSync({
430
+ customizeNamespace: this.options.generatorLookupOptions?.customizeNamespace,
431
+ lookups: this.lookups,
432
+ ...(options ?? { localOnly: false }),
433
+ env: this.asEnvironment(),
460
434
  });
461
- return generators;
462
435
  }
463
436
  /**
464
437
  * Verify if a package namespace already have been registered.
@@ -487,7 +460,14 @@ export default class EnvironmentBase extends EventEmitter {
487
460
  if (!meta) {
488
461
  return;
489
462
  }
490
- return { ...meta };
463
+ // A copy with the functions bound to this environment, the bound meta itself delegates to the meta of the store.
464
+ const boundMeta = this.bindMeta(meta);
465
+ return {
466
+ ...meta,
467
+ importGenerator: boundMeta.importGenerator,
468
+ instantiate: boundMeta.instantiate,
469
+ instantiateHelp: boundMeta.instantiateHelp,
470
+ };
491
471
  }
492
472
  alias(match, value) {
493
473
  if (match && value) {
@@ -662,9 +642,9 @@ export default class EnvironmentBase extends EventEmitter {
662
642
  // Generator is already registered and matches the current namespace.
663
643
  const generatorMeta = this.store.getMeta(namespace);
664
644
  if (generatorMeta && generatorMeta.resolved === generatorPath) {
665
- return generatorMeta;
645
+ return this.bindMeta(generatorMeta);
666
646
  }
667
- const meta = this.store.add({ namespace, resolved: generatorPath, packagePath });
647
+ const meta = this.store.add({ namespace, resolved: generatorPath, packagePath }, undefined, { env: this.asEnvironment() });
668
648
  debug('Registered %s (%s) on package %s (%s)', namespace, generatorPath, meta.packageNamespace, packagePath);
669
649
  return meta;
670
650
  }
@@ -685,7 +665,7 @@ export default class EnvironmentBase extends EventEmitter {
685
665
  if (typeof namespace !== 'string') {
686
666
  throw new TypeError('You must provide a namespace to register.');
687
667
  }
688
- const meta = this.store.add({ namespace, resolved, packagePath }, Generator);
668
+ const meta = this.store.add({ namespace, resolved, packagePath }, Generator, { env: this.asEnvironment() });
689
669
  debug('Registered %s (%s) on package (%s)', namespace, resolved, packagePath);
690
670
  return meta;
691
671
  }
@@ -1,4 +1,4 @@
1
- import type { BaseGeneratorConstructor, BaseGeneratorOptions, GeneratorEnvironmentOptions } from '@yeoman/types';
1
+ import type { BaseGeneratorConstructor, BaseGeneratorOptions, GeneratorEnvironmentOptions, GeneratorMeta } from '@yeoman/types';
2
2
  import { type LookupOptions } from './generator-lookup.ts';
3
3
  import EnvironmentBase, { type EnvironmentOptions } from './environment-base.ts';
4
4
  declare class FullEnvironment extends EnvironmentBase {
@@ -34,7 +34,7 @@ declare class FullEnvironment extends EnvironmentBase {
34
34
  * Returns stored generators meta
35
35
  * @return {Object}
36
36
  */
37
- getGeneratorsMeta(): Record<string, import("@yeoman/types").GeneratorMeta>;
37
+ getGeneratorsMeta(): Record<string, GeneratorMeta>;
38
38
  /**
39
39
  * @protected
40
40
  * Get registered generators names
@@ -99,7 +99,7 @@ class FullEnvironment extends EnvironmentBase {
99
99
  * @return {Object}
100
100
  */
101
101
  getGeneratorsMeta() {
102
- return this.store.getGeneratorsMeta();
102
+ return this.store.getGeneratorsMeta({ env: this.asEnvironment() });
103
103
  }
104
104
  /**
105
105
  * @protected
@@ -167,7 +167,6 @@ class FullEnvironment extends EnvironmentBase {
167
167
  try {
168
168
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
169
169
  // @ts-ignore
170
- // eslint-disable-next-line import-x/no-unresolved
171
170
  const { default: Generator } = await import('yeoman-generator');
172
171
  return Generator;
173
172
  }
@@ -31,12 +31,12 @@ export declare const defaultExtensions: string[];
31
31
  * @param {string|Array} [options.packagePatterns='generator-*'] - Package pattern to look for.
32
32
  * @param {boolean} [options.singleResult=false] - Set true to stop lookup on the first match.
33
33
  * @param {Number} [options.globbyDeep] - Deep option to be passed to globby.
34
- * @return {Promise<Object[]>} List of generators
34
+ * @return {Object[]} List of generators
35
35
  */
36
- export declare function lookupGenerators(options?: LookupOptions, register?: (meta: LookupMeta) => boolean): Promise<{
36
+ export declare function lookupGeneratorsSync(options?: LookupOptions, register?: (meta: LookupMeta) => boolean): {
37
37
  filePath: string;
38
38
  packagePath: string;
39
- }[]>;
39
+ }[];
40
40
  /**
41
41
  * Lookup for a specific generator.
42
42
  *
@@ -2,7 +2,8 @@ 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 { asNamespace, defaultLookups } from "./util/namespace.js";
5
+ import { defaultLookups } from "./util/namespace.js";
6
+ import Store from "./store.js";
6
7
  export const defaultExtensions = ['.ts', '.cts', '.mts', '.js', '.cjs', '.mjs'];
7
8
  /**
8
9
  * Search for generators and their sub generators.
@@ -26,9 +27,9 @@ export const defaultExtensions = ['.ts', '.cts', '.mts', '.js', '.cjs', '.mjs'];
26
27
  * @param {string|Array} [options.packagePatterns='generator-*'] - Package pattern to look for.
27
28
  * @param {boolean} [options.singleResult=false] - Set true to stop lookup on the first match.
28
29
  * @param {Number} [options.globbyDeep] - Deep option to be passed to globby.
29
- * @return {Promise<Object[]>} List of generators
30
+ * @return {Object[]} List of generators
30
31
  */
31
- export async function lookupGenerators(options = {}, register) {
32
+ export function lookupGeneratorsSync(options = {}, register) {
32
33
  const { lookups = defaultLookups } = options;
33
34
  options = {
34
35
  // Js generators should be after, last will override registered one.
@@ -53,33 +54,27 @@ export async function lookupGenerators(options = {}, register) {
53
54
  }
54
55
  export function lookupGenerator(namespace, options) {
55
56
  options = typeof options === 'boolean' ? { localOnly: options } : (options ?? {});
56
- options.singleResult = options.singleResult ?? true;
57
- options.filePatterns = options.filePatterns ?? defaultLookups.map(prefix => join(prefix, '*/index.{js,ts}'));
57
+ const { packagePath: returnPackagePath, generatorPath: returnGeneratorPath, singleResult = true, ...lookupOptions } = options;
58
58
  const ns = requireNamespace(namespace);
59
- options.packagePatterns = options.packagePatterns ?? [ns.generatorHint];
60
- options.npmPaths = options.npmPaths ?? getNpmPaths({ localOnly: options.localOnly }).toReversed();
61
- options.packagePatterns = options.packagePatterns ?? ['generator-*'];
62
- options.packagePaths = options.packagePaths ?? findPackagesIn(options.npmPaths, options.packagePatterns);
63
- let paths = options.singleResult ? undefined : [];
64
- moduleLookupSync(options, ({ files, packagePath }) => {
65
- for (const filename of files) {
66
- const fileNs = asNamespace(filename, { lookups: defaultLookups });
67
- const ns = toNamespace(fileNs);
68
- if (namespace === fileNs || (options.packagePath && namespace === ns?.packageNamespace)) {
69
- // Version 2.6.0 returned pattern instead of modulePath for options.packagePath
70
- const returnPath = options.packagePath ? packagePath : options.generatorPath ? posix.join(filename, '../../') : filename;
71
- if (options.singleResult) {
72
- paths = returnPath;
73
- return filename;
74
- }
75
- paths.push(returnPath);
76
- }
77
- }
78
- return;
79
- });
80
- if (options.singleResult) {
81
- const generatorPath = paths;
82
- return generatorPath && isAbsolute(generatorPath) ? pathToFileURL(generatorPath).toString() : generatorPath;
59
+ lookupOptions.packagePatterns = lookupOptions.packagePatterns ?? [ns.generatorHint];
60
+ if (!lookupOptions.packagePaths) {
61
+ // Looking the npm paths up runs the package managers, skip it when the packages are given.
62
+ lookupOptions.npmPaths = lookupOptions.npmPaths ?? getNpmPaths({ localOnly: lookupOptions.localOnly }).toReversed();
63
+ lookupOptions.packagePaths = findPackagesIn(lookupOptions.npmPaths, lookupOptions.packagePatterns);
83
64
  }
84
- return paths.map(gen => (isAbsolute(gen) ? pathToFileURL(gen).toString() : gen));
65
+ // The generators are looked up by a store of their own, which keeps the ones asked for.
66
+ const generators = new Store().lookupSync({
67
+ filePatterns: defaultLookups.map(prefix => join(prefix, '*/index.{js,ts}')),
68
+ reverse: false,
69
+ ...lookupOptions,
70
+ lookups: defaultLookups,
71
+ singleResult,
72
+ filter: generator => generator.namespace === namespace || Boolean(returnPackagePath && toNamespace(generator.namespace)?.packageNamespace === namespace),
73
+ });
74
+ const paths = generators.map(({ filePath, packagePath }) => {
75
+ // Version 2.6.0 returned pattern instead of modulePath for options.packagePath
76
+ const returnPath = returnPackagePath ? packagePath : returnGeneratorPath ? posix.join(filePath, '../../') : filePath;
77
+ return isAbsolute(returnPath) ? pathToFileURL(returnPath).toString() : returnPath;
78
+ });
79
+ return singleResult ? paths[0] : paths;
85
80
  }
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ 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
6
  export declare const createEnv: (options?: EnvironmentOptions) => Environment;
6
7
  export declare const enforceUpdate: () => void;
7
8
  export * from './commands.ts';
package/dist/index.js CHANGED
@@ -2,6 +2,8 @@ import {} from "./environment-base.js";
2
2
  import Environment from "./environment-full.js";
3
3
  export { default } from "./environment-full.js";
4
4
  export { default as EnvironmentBase } from "./environment-base.js";
5
+ // Experimental: the Store API is not stable yet.
6
+ export { default as Store, } from "./store.js";
5
7
  export const createEnv = (options) => new Environment(options);
6
8
  // Backward compatibility
7
9
  export const enforceUpdate = () => { };
package/dist/store.d.ts CHANGED
@@ -1,36 +1,98 @@
1
- import type { BaseEnvironment, BaseGeneratorMeta, GeneratorMeta, GetGeneratorConstructor } from '@yeoman/types';
1
+ import type { BaseEnvironment, BaseGenerator, BaseGeneratorConstructorMeta, BaseGeneratorMeta, GeneratorMeta, GetGeneratorConstructor } from '@yeoman/types';
2
+ import { type LookupOptions } from './generator-lookup.ts';
3
+ type FoundGenerator = {
4
+ namespace: string;
5
+ /** Path of the generator as it was found, `resolved` is its real path. */
6
+ filePath: string;
7
+ packagePath: string;
8
+ };
9
+ /** The environment to import or instantiate a generator in. */
10
+ export type StoreEnvironmentOptions = {
11
+ env?: BaseEnvironment;
12
+ };
13
+ export type StoreLookupOptions = LookupOptions & StoreEnvironmentOptions & {
14
+ registerToScope?: string;
15
+ customizeNamespace?: (ns?: string) => string | undefined;
16
+ /** Generators to keep, the others are neither registered nor returned. */
17
+ filter?: (generator: FoundGenerator) => boolean;
18
+ };
19
+ /** A generator found by a lookup, `registered` tells if it was added to the store. */
20
+ export type StoreLookupGeneratorMeta = FoundGenerator & ((StoreGeneratorMeta & {
21
+ registered: true;
22
+ }) | (Required<BaseGeneratorMeta> & {
23
+ registered: false;
24
+ }));
25
+ /**
26
+ * Generator meta as the store keeps it. `importGenerator`, `instantiate` and `instantiateHelp` take the environment
27
+ * to use as `{ env }`, falling back to the one the meta is bound to: the environment the store was created with, if
28
+ * any, or the one passed to the store method that returned the meta.
29
+ */
30
+ export type StoreGeneratorMeta = Omit<GeneratorMeta, 'importGenerator' | 'instantiate' | 'instantiateHelp'> & {
31
+ importGenerator: <G extends BaseGenerator = BaseGenerator>(options?: StoreEnvironmentOptions) => Promise<GetGeneratorConstructor<G> & BaseGeneratorConstructorMeta> | (GetGeneratorConstructor<G> & BaseGeneratorConstructorMeta);
32
+ instantiate: <G extends BaseGenerator = BaseGenerator>(arguments_?: string[], options?: any, environmentOptions?: StoreEnvironmentOptions) => Promise<G>;
33
+ instantiateHelp: <G extends BaseGenerator = BaseGenerator>(options?: StoreEnvironmentOptions) => Promise<G>;
34
+ };
2
35
  /**
3
36
  * The Generator store
4
37
  * This is used to store generator (npm packages) reference and instantiate them when
5
38
  * requested.
6
- * @constructor
7
- * @private
39
+ *
40
+ * A store does not need an environment: generators are looked up and registered on their own, and the environment
41
+ * that imports or instantiates one is passed at that time. The same store can then serve several environments: the
42
+ * methods returning metas take `{ env }` and return metas bound to it.
43
+ *
44
+ * @experimental The Store API is not stable yet and may change in a minor release.
8
45
  */
9
46
  export default class Store {
10
47
  private readonly _meta;
11
48
  private readonly _packagesJson;
12
49
  private readonly _packagesPaths;
13
50
  private readonly _packagesNS;
14
- private readonly environment;
15
- constructor(environment: BaseEnvironment);
51
+ private readonly _boundMetas;
52
+ /** The environment used when none is passed to `importGenerator` or `instantiate`. */
53
+ readonly environment?: BaseEnvironment;
54
+ constructor(environment?: BaseEnvironment);
16
55
  /**
17
56
  * Store a module under the namespace key
18
57
  * @param meta
19
58
  * @param generator - A generator module or a module path
59
+ * @param options.env - The environment to bind the returned meta to
60
+ */
61
+ add<M extends BaseGeneratorMeta>(meta: M, Generator?: unknown, { env }?: StoreEnvironmentOptions): StoreGeneratorMeta & M;
62
+ /**
63
+ * The meta bound to an environment: its functions default to it. The meta of the environment the store was created
64
+ * with is the meta itself, as it already defaults to it. A meta is bound once by environment, and delegates to the
65
+ * meta of the store instead of copying it, so it keeps showing what the store has.
66
+ */
67
+ bindMeta<M extends StoreGeneratorMeta>(meta: M, options?: StoreEnvironmentOptions): M;
68
+ bindMeta<M extends StoreGeneratorMeta>(meta: M | undefined, options?: StoreEnvironmentOptions): M | undefined;
69
+ /**
70
+ * Search for generators and their sub generators, and add them to the store.
71
+ *
72
+ * A generator is a `:lookup/:name/index.js` file placed inside an npm package.
73
+ *
74
+ * Defaults lookups are:
75
+ * - ./
76
+ * - generators/
77
+ * - lib/generators/
78
+ *
79
+ * So this index file `node_modules/generator-dummy/lib/generators/yo/index.js` would be
80
+ * registered as `dummy:yo` generator.
20
81
  */
21
- add<M extends BaseGeneratorMeta>(meta: M, Generator?: unknown): GeneratorMeta & M;
82
+ lookupSync(options?: StoreLookupOptions): StoreLookupGeneratorMeta[];
22
83
  /**
23
84
  * Get the module registered under the given namespace
24
85
  * @param {String} namespace
86
+ * @param options.env - The environment to import the generator in
25
87
  * @return {Module}
26
88
  */
27
- get(namespace: string): Promise<GetGeneratorConstructor | undefined>;
89
+ get(namespace: string, options?: StoreEnvironmentOptions): Promise<GetGeneratorConstructor | undefined>;
28
90
  /**
29
- * Get the module registered under the given namespace
91
+ * Get the meta registered under the given namespace
30
92
  * @param {String} namespace
31
- * @return {Module}
93
+ * @param options.env - The environment to bind the meta to
32
94
  */
33
- getMeta(namespace: string): GeneratorMeta | undefined;
95
+ getMeta(namespace: string, options?: StoreEnvironmentOptions): StoreGeneratorMeta | undefined;
34
96
  /**
35
97
  * Returns the list of registered namespace.
36
98
  * @return {Array} Namespaces array
@@ -38,9 +100,10 @@ export default class Store {
38
100
  namespaces(): string[];
39
101
  /**
40
102
  * Get the stored generators meta data
103
+ * @param options.env - The environment to bind the metas to
41
104
  * @return {Object} Generators metadata
42
105
  */
43
- getGeneratorsMeta(): Record<string, GeneratorMeta>;
106
+ getGeneratorsMeta(options?: StoreEnvironmentOptions): Record<string, StoreGeneratorMeta>;
44
107
  /**
45
108
  * Store a package under the namespace key
46
109
  * @param {String} packageNS - The key under which the generator can be retrieved
@@ -66,6 +129,11 @@ export default class Store {
66
129
  * Read a package.json from packagePath. Parsed results are cached by packagePath.
67
130
  */
68
131
  private getPackageJson;
132
+ /**
133
+ * The functions of a meta that need an environment, defaulting to the given one.
134
+ */
135
+ private bindMetaFunctions;
69
136
  private getFactory;
70
137
  private _getGenerator;
71
138
  }
139
+ export {};
package/dist/store.js CHANGED
@@ -7,19 +7,25 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
7
7
  return path;
8
8
  };
9
9
  import { pathToFileURL } from 'node:url';
10
- import { extname, join } from 'node:path';
10
+ import { basename, extname, join, relative } from 'node:path';
11
11
  import { createRequire } from 'node:module';
12
- import { readFileSync } from 'node:fs';
12
+ import { readFileSync, realpathSync } from 'node:fs';
13
13
  import { toNamespace } from '@yeoman/namespace';
14
14
  import createDebug from 'debug';
15
+ import { lookupGeneratorsSync } from "./generator-lookup.js";
16
+ import { asNamespace, defaultLookups } from "./util/namespace.js";
15
17
  const debug = createDebug('yeoman:environment:store');
16
18
  const require = createRequire(import.meta.url);
17
19
  /**
18
20
  * The Generator store
19
21
  * This is used to store generator (npm packages) reference and instantiate them when
20
22
  * requested.
21
- * @constructor
22
- * @private
23
+ *
24
+ * A store does not need an environment: generators are looked up and registered on their own, and the environment
25
+ * that imports or instantiates one is passed at that time. The same store can then serve several environments: the
26
+ * methods returning metas take `{ env }` and return metas bound to it.
27
+ *
28
+ * @experimental The Store API is not stable yet and may change in a minor release.
23
29
  */
24
30
  export default class Store {
25
31
  _meta = {};
@@ -29,6 +35,9 @@ export default class Store {
29
35
  _packagesPaths = {};
30
36
  // Store packages ns
31
37
  _packagesNS = [];
38
+ // Metas bound to an environment, by the meta of the store.
39
+ _boundMetas = new WeakMap();
40
+ /** The environment used when none is passed to `importGenerator` or `instantiate`. */
32
41
  environment;
33
42
  constructor(environment) {
34
43
  this.environment = environment;
@@ -37,8 +46,9 @@ export default class Store {
37
46
  * Store a module under the namespace key
38
47
  * @param meta
39
48
  * @param generator - A generator module or a module path
49
+ * @param options.env - The environment to bind the returned meta to
40
50
  */
41
- add(meta, Generator) {
51
+ add(meta, Generator, { env } = {}) {
42
52
  if (typeof meta.resolved === 'string') {
43
53
  if (extname(meta.resolved)) {
44
54
  meta.resolved = join(meta.resolved);
@@ -74,52 +84,72 @@ export default class Store {
74
84
  }
75
85
  };
76
86
  }
77
- let importPromise;
87
+ let moduleImport;
88
+ const importGeneratorModule = () => {
89
+ if (!importModule || Generator) {
90
+ return undefined;
91
+ }
92
+ if (moduleImport) {
93
+ return moduleImport;
94
+ }
95
+ const maybeModule = importModule();
96
+ if (maybeModule.then) {
97
+ moduleImport = maybeModule
98
+ .then((module_) => {
99
+ Generator = module_;
100
+ })
101
+ .finally(() => {
102
+ moduleImport = undefined;
103
+ });
104
+ return moduleImport;
105
+ }
106
+ Generator = maybeModule;
107
+ return undefined;
108
+ };
78
109
  // eslint-disable-next-line prefer-const
79
110
  let generatorMeta;
80
- const importGenerator = () => {
81
- const handleImport = () => {
82
- if (importModule && !Generator) {
83
- const maybeModule = importModule();
84
- if (maybeModule.then) {
85
- importPromise = maybeModule;
86
- return maybeModule
87
- .then((mod) => {
88
- Generator = mod;
89
- })
90
- .finally(() => {
91
- importPromise = undefined;
92
- });
93
- }
94
- else {
95
- Generator = maybeModule;
96
- }
97
- }
98
- };
99
- const handleModule = () => {
100
- const factory = this.getFactory(Generator);
101
- if (typeof factory === 'function') {
102
- importPromise = factory(this.environment);
103
- return Promise.resolve(importPromise).then((mod) => {
104
- const generator = this._getGenerator(mod, meta, generatorMeta);
105
- Generator = generator;
106
- importPromise = undefined;
107
- return generator;
108
- });
109
- }
110
- return this._getGenerator(Generator, meta, generatorMeta);
111
- };
112
- if (importPromise) {
113
- return importPromise.then(() => Promise.resolve(handleImport()).then(() => handleModule()));
111
+ // A generator exported as a class is the same for every environment.
112
+ let generatorConstructor;
113
+ // A `createGenerator(environment)` factory builds the generator for an environment, so its result is kept by environment.
114
+ const createdGenerators = new WeakMap();
115
+ const getGenerator = (environment) => {
116
+ if (generatorConstructor) {
117
+ return generatorConstructor;
118
+ }
119
+ const factory = this.getFactory(Generator);
120
+ if (typeof factory !== 'function') {
121
+ generatorConstructor = this._getGenerator(Generator, meta, generatorMeta);
122
+ return generatorConstructor;
123
+ }
124
+ if (!environment) {
125
+ throw new Error(`An environment is required to create the generator ${meta.namespace}`);
114
126
  }
115
- const maybeImportPromise = handleImport();
116
- if (maybeImportPromise?.then) {
117
- return maybeImportPromise.then(() => handleModule());
127
+ const created = createdGenerators.get(environment);
128
+ if (created) {
129
+ return created;
130
+ }
131
+ const creating = Promise.resolve(factory(environment)).then((module_) => {
132
+ const generator = this._getGenerator(module_, meta, generatorMeta);
133
+ createdGenerators.set(environment, generator);
134
+ return generator;
135
+ });
136
+ createdGenerators.set(environment, creating);
137
+ return creating;
138
+ };
139
+ const importGenerator = (({ env: environment = this.environment } = {}) => {
140
+ const importing = importGeneratorModule();
141
+ return importing ? importing.then(() => getGenerator(environment)) : getGenerator(environment);
142
+ });
143
+ const instantiate = async (arguments_ = [], options = {}, { env: environment = this.environment } = {}) => {
144
+ if (!environment) {
145
+ throw new Error(`An environment is required to instantiate the generator ${meta.namespace}`);
118
146
  }
119
- return handleModule();
147
+ return environment.instantiate(await importGenerator({ env: environment }), {
148
+ generatorArgs: arguments_,
149
+ generatorOptions: options,
150
+ });
120
151
  };
121
- const instantiate = async (arguments_ = [], options = {}) => this.environment.instantiate(await importGenerator(), { generatorArgs: arguments_, generatorOptions: options });
122
- const instantiateHelp = async () => instantiate([], { help: true });
152
+ const instantiateHelp = async (options) => instantiate([], { help: true }, options);
123
153
  const getPackageJson = () => this.getPackageJson(meta.packagePath);
124
154
  const { packageNamespace } = toNamespace(meta.namespace) ?? {};
125
155
  generatorMeta = {
@@ -138,23 +168,91 @@ export default class Store {
138
168
  this.addPackage(packageNamespace, meta.packagePath);
139
169
  }
140
170
  }
141
- return generatorMeta;
171
+ return this.bindMeta(generatorMeta, { env });
172
+ }
173
+ bindMeta(meta, { env } = {}) {
174
+ if (!meta || !env || env === this.environment) {
175
+ return meta;
176
+ }
177
+ let boundMetas = this._boundMetas.get(meta);
178
+ if (!boundMetas) {
179
+ boundMetas = new WeakMap();
180
+ this._boundMetas.set(meta, boundMetas);
181
+ }
182
+ let boundMeta = boundMetas.get(env);
183
+ if (!boundMeta) {
184
+ boundMeta = Object.assign(Object.create(meta), this.bindMetaFunctions(meta, env));
185
+ boundMetas.set(env, boundMeta);
186
+ }
187
+ return boundMeta;
188
+ }
189
+ /**
190
+ * Search for generators and their sub generators, and add them to the store.
191
+ *
192
+ * A generator is a `:lookup/:name/index.js` file placed inside an npm package.
193
+ *
194
+ * Defaults lookups are:
195
+ * - ./
196
+ * - generators/
197
+ * - lib/generators/
198
+ *
199
+ * So this index file `node_modules/generator-dummy/lib/generators/yo/index.js` would be
200
+ * registered as `dummy:yo` generator.
201
+ */
202
+ lookupSync(options) {
203
+ const { registerToScope, customizeNamespace = (ns) => ns, filter, env, lookups = defaultLookups, ...remainingOptions } = options ?? { localOnly: false };
204
+ const lookupOptions = { ...remainingOptions, lookups };
205
+ const generators = [];
206
+ lookupGeneratorsSync(lookupOptions, ({ packagePath, filePath, lookups }) => {
207
+ let repositoryPath = join(packagePath, '..');
208
+ if (basename(repositoryPath).startsWith('@')) {
209
+ // Scoped package
210
+ repositoryPath = join(repositoryPath, '..');
211
+ }
212
+ let namespace = customizeNamespace(asNamespace(relative(repositoryPath, filePath), { lookups }));
213
+ try {
214
+ const resolved = realpathSync(filePath);
215
+ if (!namespace) {
216
+ namespace = customizeNamespace(asNamespace(resolved, { lookups }));
217
+ }
218
+ namespace = namespace;
219
+ if (registerToScope && !namespace.startsWith('@')) {
220
+ namespace = `@${registerToScope}/${namespace}`;
221
+ }
222
+ if (filter && !filter({ namespace, filePath, packagePath })) {
223
+ return false;
224
+ }
225
+ const meta = this.add({ namespace, packagePath, resolved });
226
+ if (meta) {
227
+ const boundFunctions = env && env !== this.environment ? this.bindMetaFunctions(meta, env) : {};
228
+ generators.push({ ...meta, ...boundFunctions, filePath, packagePath, registered: true });
229
+ return Boolean(lookupOptions.singleResult);
230
+ }
231
+ }
232
+ catch (error) {
233
+ console.error('Unable to register %s (Error: %s)', filePath, error);
234
+ }
235
+ generators.push({ resolved: filePath, namespace: namespace, filePath, packagePath, registered: false });
236
+ return false;
237
+ });
238
+ return generators;
142
239
  }
143
240
  /**
144
241
  * Get the module registered under the given namespace
145
242
  * @param {String} namespace
243
+ * @param options.env - The environment to import the generator in
146
244
  * @return {Module}
147
245
  */
148
- async get(namespace) {
149
- return this.getMeta(namespace)?.importGenerator();
246
+ async get(namespace, options) {
247
+ return this.getMeta(namespace)?.importGenerator(options);
150
248
  }
151
249
  /**
152
- * Get the module registered under the given namespace
250
+ * Get the meta registered under the given namespace
153
251
  * @param {String} namespace
154
- * @return {Module}
252
+ * @param options.env - The environment to bind the meta to
155
253
  */
156
- getMeta(namespace) {
157
- return this._meta[namespace];
254
+ getMeta(namespace, options) {
255
+ return this.bindMeta(this._meta[namespace], options);
158
256
  }
159
257
  /**
160
258
  * Returns the list of registered namespace.
@@ -165,10 +263,22 @@ export default class Store {
165
263
  }
166
264
  /**
167
265
  * Get the stored generators meta data
266
+ * @param options.env - The environment to bind the metas to
168
267
  * @return {Object} Generators metadata
169
268
  */
170
- getGeneratorsMeta() {
171
- return this._meta;
269
+ getGeneratorsMeta(options) {
270
+ const env = options?.env;
271
+ if (!env || env === this.environment) {
272
+ return this._meta;
273
+ }
274
+ // Bound as they are read. It is a view of the record of the store, as the record itself is what is returned
275
+ // otherwise: what is set goes to the store.
276
+ return new Proxy(this._meta, {
277
+ get: (target, property, receiver) => {
278
+ const meta = Reflect.get(target, property, receiver);
279
+ return typeof property === 'string' && meta ? this.bindMeta(meta, { env }) : meta;
280
+ },
281
+ });
172
282
  }
173
283
  /**
174
284
  * Store a package under the namespace key
@@ -235,6 +345,16 @@ export default class Store {
235
345
  }
236
346
  return this._packagesJson.get(packagePath);
237
347
  }
348
+ /**
349
+ * The functions of a meta that need an environment, defaulting to the given one.
350
+ */
351
+ bindMetaFunctions(meta, env) {
352
+ return {
353
+ importGenerator: (({ env: environment = env } = {}) => meta.importGenerator({ env: environment })),
354
+ instantiate: (arguments_, options, { env: environment = env } = {}) => meta.instantiate(arguments_, options, { env: environment }),
355
+ instantiateHelp: ({ env: environment = env } = {}) => meta.instantiateHelp({ env: environment }),
356
+ };
357
+ }
238
358
  getFactory(module) {
239
359
  // CJS is imported in default, for backward compatibility we support a Generator exported as `module.exports = { default }`
240
360
  return module.createGenerator ?? module.default?.createGenerator ?? module.default?.default?.createGenerator;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yeoman-environment",
3
- "version": "6.3.0",
3
+ "version": "7.1.0",
4
4
  "description": "Handles the lifecyle and bootstrapping of generators in a specific environment",
5
5
  "keywords": [
6
6
  "development",
@@ -41,77 +41,68 @@
41
41
  ],
42
42
  "scripts": {
43
43
  "prebuild": "rimraf dist",
44
- "build": "tsc",
44
+ "build": "tsc -p tsconfig.build.json",
45
45
  "doc": "npm run doc:generate && npm run doc:fix && npm run doc:prettier",
46
46
  "doc:fix": "sed -i -e 's:^[[:space:]]*<!--[[:space:]]*$::g' -e 's:^[[:space:]]*-->[[:space:]]*$::g' $npm_package_config_doc_path/global.html || true",
47
47
  "doc:generate": "jsdoc -c jsdoc.json -d $npm_package_config_doc_path",
48
48
  "doc:prettier": "prettier $npm_package_config_doc_path --write --ignore-path .prettierignore-doc",
49
49
  "fix": "eslint --fix && prettier . --write",
50
50
  "prepare": "npm run build",
51
- "pretest": "eslint && prettier . --check && tsc -p tsconfig.test.json",
51
+ "pretest": "eslint && prettier . --check && tsc",
52
52
  "test": "c8 --include 'src/**' esmocha --forbid-only"
53
53
  },
54
54
  "config": {
55
55
  "doc_path": "./yeoman-environment-doc"
56
56
  },
57
57
  "dependencies": {
58
- "@yeoman/adapter": "^4.0.2",
59
- "@yeoman/conflicter": "^4.1.0",
58
+ "@yeoman/adapter": "^5.0.0",
59
+ "@yeoman/conflicter": "^4.2.0",
60
60
  "@yeoman/namespace": "^2.1.0",
61
- "@yeoman/transform": "^2.1.1",
62
- "@yeoman/types": "^1.11.1",
61
+ "@yeoman/transform": "^2.1.2",
62
+ "@yeoman/types": "^1.13.1",
63
63
  "arrify": "^3.0.0",
64
- "chalk": "^5.6.2",
65
- "commander": "^14.0.3",
64
+ "chalk": "^6.0.0",
65
+ "commander": "^15.0.0",
66
66
  "debug": "^4.4.3",
67
- "execa": "^9.6.1",
67
+ "execa": "^10.0.1",
68
68
  "fly-import": "^1.0.0",
69
- "globby": "^16.2.0",
69
+ "globby": "^16.2.4",
70
70
  "grouped-queue": "^2.1.0",
71
71
  "locate-path": "^8.0.0",
72
72
  "lodash-es": "^4.18.1",
73
- "mem-fs": "^4.1.4",
74
- "mem-fs-editor": "^12.0.4",
75
- "semver": "^7.7.4",
73
+ "mem-fs": "^6.0.0",
74
+ "mem-fs-editor": "^12.0.9",
75
+ "semver": "^7.8.5",
76
76
  "slash": "^5.1.0",
77
77
  "untildify": "^6.0.0",
78
78
  "which-package-manager": "^1.0.1"
79
79
  },
80
80
  "devDependencies": {
81
- "@node-loaders/esbuild": "^2.0.0",
82
81
  "@types/debug": "^4.1.13",
83
82
  "@types/lodash-es": "^4.17.12",
84
- "@types/node": "^20.19.35",
85
- "@types/semver": "^7.7.1",
83
+ "@types/node": "^26.4.1",
84
+ "@types/semver": "^7.8.0",
86
85
  "@yeoman/eslint": "1.1.0",
87
86
  "c8": "^12.0.0",
88
- "eslint": "10.2.1",
87
+ "eslint": "10.10.0",
89
88
  "esmocha": "^5.0.0",
90
89
  "jsdoc": "^4.0.5",
91
- "prettier": "3.8.3",
90
+ "prettier": "3.9.6",
92
91
  "prettier-plugin-packagejson": "^3.0.2",
93
92
  "rimraf": "^6.1.3",
93
+ "simple-git": "^3.36.0",
94
94
  "strip-ansi": "^7.2.0",
95
95
  "typescript": "6.0.3",
96
96
  "yeoman-environment": "file:./",
97
- "yeoman-test": "^11.3.1"
97
+ "yeoman-test": "^11.8.0"
98
98
  },
99
99
  "peerDependencies": {
100
- "@yeoman/adapter": "^4.0.2",
100
+ "@yeoman/adapter": "^5.0.0",
101
101
  "@yeoman/types": "^1.10.3",
102
102
  "mem-fs": "^4.1.4 || ^6.0.0"
103
103
  },
104
104
  "engines": {
105
- "node": "^20.17.0 || >=22.9.0"
106
- },
107
- "acceptDependencies": {
108
- "@yeoman/adapter": ">=4.0.2 || ^5.0.0"
109
- },
110
- "overrides": {
111
- "@inquirer/type": "4.0.7",
112
- "@yeoman-environment/generator-tests": {
113
- "yeoman-environment": "file:./"
114
- }
105
+ "node": "^22.18.0 || >=24.11.0"
115
106
  },
116
107
  "workspaces": [
117
108
  "test/generators"