yeoman-environment 8.1.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.
@@ -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,8 +263,12 @@ 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`];
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`]);
267
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;
@@ -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/store.d.ts CHANGED
@@ -22,7 +22,7 @@ export type StoreLookupOptions = LookupOptions & StoreEnvironmentOptions & {
22
22
  * Lookup options shared by every lookup of a store, the options passed to a lookup take precedence.
23
23
  * What to look up (paths, package patterns and `singleResult`) is given by each lookup.
24
24
  */
25
- export type StoreSharedLookupOptions = Pick<StoreLookupOptions, 'lookups' | 'localOnly' | 'filterPaths' | 'customizeNamespace' | 'filter' | 'usePackageName'>;
25
+ export type StoreSharedLookupOptions = Pick<StoreLookupOptions, 'lookups' | 'localOnly' | 'filterPaths' | 'customizeNamespace' | 'filter' | 'usePackageName' | 'nestedGenerators'>;
26
26
  /** A generator found by a lookup, `registered` tells if it was added to the store. */
27
27
  export type StoreLookupGeneratorMeta = FoundGenerator & ((StoreGeneratorMeta & {
28
28
  registered: true;
package/dist/store.js CHANGED
@@ -10,7 +10,7 @@ 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";
@@ -20,6 +20,15 @@ const require = createRequire(import.meta.url);
20
20
  const operationsKey = Symbol('operations');
21
21
  const setOperations = (meta, operations) => Object.defineProperty(meta, operationsKey, { value: operations });
22
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
+ };
23
32
  /**
24
33
  * The Generator store
25
34
  * This is used to store generator (npm packages) reference and instantiate them when
@@ -74,7 +83,7 @@ export default class Store {
74
83
  * @param lookups - The lookups of the store if omitted
75
84
  */
76
85
  namespace(filepath, lookups = this.lookups) {
77
- return asNamespace(filepath, { lookups });
86
+ return asNamespace(filepath, { lookups, nestedGenerators: this.lookupOptions.nestedGenerators });
78
87
  }
79
88
  /**
80
89
  * A copy of the store with the same generators, packages and lookup options, whose metas default to the given
@@ -264,25 +273,33 @@ export default class Store {
264
273
  const { registerToScope, customizeNamespace = (ns) => ns, usePackageName, filter, env, lookups = this.lookups, ...remainingOptions } = { ...this.lookupOptions, ...options };
265
274
  const lookupOptions = { ...remainingOptions, lookups };
266
275
  const generators = [];
276
+ const { nestedGenerators } = lookupOptions;
267
277
  lookupGeneratorsSync(lookupOptions, ({ packagePath, filePath, lookups }) => {
268
- const packageName = usePackageName ? this.getPackageJson(packagePath)?.name : undefined;
269
- let namespacePath;
270
- if (packageName) {
271
- namespacePath = join(packageName, relative(packagePath, filePath));
272
- }
273
- else {
274
- let repositoryPath = join(packagePath, '..');
275
- if (basename(repositoryPath).startsWith('@')) {
276
- // Scoped package
277
- repositoryPath = join(repositoryPath, '..');
278
- }
279
- namespacePath = relative(repositoryPath, filePath);
280
- }
281
- let namespace = customizeNamespace(asNamespace(namespacePath, { lookups }));
278
+ let namespace;
282
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);
283
300
  const resolved = realpathSync(filePath);
284
301
  if (!namespace) {
285
- namespace = customizeNamespace(asNamespace(resolved, { lookups }));
302
+ namespace = customizeNamespace(asNamespace(resolved, { lookups, nestedGenerators }));
286
303
  }
287
304
  namespace = namespace;
288
305
  if (registerToScope && !namespace.startsWith('@')) {
@@ -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.1.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",