Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 100 additions & 14 deletions projects/yeoman-ui/packages/backend/src/utils/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export type EnvGen = {
gen: any;
};

export type PrepareEnvGen = (
env: Environment,
gen: any
) => void | Promise<void>;

export type GeneratorData = {
generatorMeta: LookupGeneratorMeta;
generatorPackageJson: any;
Expand Down Expand Up @@ -57,12 +62,20 @@ class EnvUtil {

public isEnvIncompatibilityError(error: unknown): boolean {
return (
(error as Error)?.message?.startsWith(
(error as Error)?.message?.includes(
Constants.ENV_INCOMPATIBILITY_MESSAGE_PREFIX
) ?? false
);
}

private isV3RuntimeIncompatibilityError(error: unknown): boolean {
const message = (error as Error)?.message ?? "";
return (
message.includes("requires yeoman-environment") ||

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does the error requires yeoman-environment include more details?
as in the version required?

message.includes("object is not extensible")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"object is not extensible" is not a yeoman specific error.
Is this due to ESM/CJS compatibility? or?

In what scenario do we get this error?

);
}

public loadNpmPath(_force = false) {
return this;
}
Expand Down Expand Up @@ -180,19 +193,7 @@ class EnvUtil {
);

try {
const v6Env: Environment = this.createEnvInstance(
{ sharedOptions: { forwardErrorToEnvironment: true } as any },
adapter
);

v6Env.register(meta.resolved!, {
namespace: genNamespace,
packagePath: meta.packagePath,
});

const gen: any = await v6Env.create(genNamespace, { options } as any);

return { env: v6Env, gen };
return await this.createV6EnvAndGen(genNamespace, meta, options, adapter);
} catch (v6Error) {
const shouldFallbackToV3 = this.isEnvIncompatibilityError(v6Error);
if (!shouldFallbackToV3) {
Expand Down Expand Up @@ -229,6 +230,91 @@ class EnvUtil {
}
}

public async createRunGen(
genNamespace: string,
options: any,
adapter: any,
prepare: PrepareEnvGen
): Promise<void> {
const meta: LookupGeneratorMeta = await this.getGenMetadata(genNamespace);

this.unloadGeneratorModules(genNamespace);
let v3EnvGen: EnvGen | undefined;
try {
v3EnvGen = this.createLegacyV3EnvAndGen(
genNamespace,
meta,
options,
adapter
);
} catch (v3CreateError) {
if (this.isV3RuntimeIncompatibilityError(v3CreateError)) {
this.logger?.info(
`generator ${genNamespace} needs yeoman-environment v6; instantiation on v3 was rejected`,
{ error: (v3CreateError as Error)?.message }
);
} else {
this.logger?.debug(
`generator ${genNamespace} failed to instantiate on yeoman-environment v3; surfacing the error (not a v6-runtime signal)`,
{ error: (v3CreateError as Error)?.message }
);
throw v3CreateError;
}
}

if (v3EnvGen) {
this.logger?.debug(
`routing generator ${genNamespace} to yeoman-environment v3`
);
await this.prepareAndRun(v3EnvGen.env, v3EnvGen.gen, adapter, prepare);
return;
}

this.logger?.debug(
`routing generator ${genNamespace} to yeoman-environment v6`
);
this.unloadGeneratorModules(genNamespace);
const { env, gen } = await this.createV6EnvAndGen(
genNamespace,
meta,
options,
adapter
);
await this.prepareAndRun(env, gen, adapter, prepare);
}

private async prepareAndRun(
env: Environment,
gen: any,
adapter: any,
prepare: PrepareEnvGen
): Promise<void> {
adapter?.resetSignal?.();
await prepare(env, gen);
await Promise.resolve(env.runGenerator(gen));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think This:

await Promise.resolve(env.runGenerator(gen));
is effectively the same as:
await env.runGenerator(gen);

}

private async createV6EnvAndGen(
genNamespace: string,
meta: LookupGeneratorMeta,
options: any,
adapter: any
): Promise<EnvGen> {
const v6Env: Environment = this.createEnvInstance(
{ sharedOptions: { forwardErrorToEnvironment: true } as any },
adapter
);

v6Env.register(meta.resolved!, {
namespace: genNamespace,
packagePath: meta.packagePath,
});

const gen: any = await v6Env.create(genNamespace, { options } as any);

return { env: v6Env, gen };
}

private createLegacyV3EnvAndGen(
genNamespace: string,
meta: LookupGeneratorMeta,
Expand Down
57 changes: 27 additions & 30 deletions projects/yeoman-ui/packages/backend/src/yeomanui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,7 @@ import { IPrompt, MessageType } from "@sap-devx/yeoman-ui-types";
import { AnalyticsWrapper } from "./usage-report/usage-analytics-wrapper.js";
import { Output } from "./output.js";
import { resolve } from "path";
import {
Env,
EnvGen,
GeneratorData,
GeneratorNotFoundError,
} from "./utils/env.js";
import { Env, GeneratorData, GeneratorNotFoundError } from "./utils/env.js";
import { namespaceToName } from "./utils/namespace.js";
import { vscode, getVscode } from "./utils/vscodeProxy.js";
import Generator from "yeoman-generator";
Expand Down Expand Up @@ -238,32 +233,35 @@ export class YeomanUI {
appWizard: this.youiEvents.getAppWizard(),
};

const envGen: EnvGen = await Env.createEnvAndGen(
const prepare = (env: Environment, gen: any) => {
this.errorThrown = false;

// check if generator defined a helper function called setPromptsCallback()
const setPromptsCallback = _.get(gen, "setPromptsCallback");
if (setPromptsCallback) {
setPromptsCallback(this.setPromptList.bind(this));
}

this.promptCount = 0;
this.gen = gen as Generator;
// do not add second parameter with value true
// some generators rely on fact that this.env.cwd and
// the current working directory is changed.
this.gen.destinationRoot(targetFolder);
// notifies ui wether generator is in writing state
this.setGenInWriting(this.gen);
// handles generator install step if exists
this.onGenInstall(this.gen);
// handles generator errors
this.handleErrors(env, this.gen, generatorNamespace);
};

await Env.createRunGen(
generatorNamespace,
options,
this.youiAdapter
this.youiAdapter,
prepare
);

// check if generator defined a helper function called setPromptsCallback()
const setPromptsCallback = _.get(envGen.gen, "setPromptsCallback");
if (setPromptsCallback) {
setPromptsCallback(this.setPromptList.bind(this));
}

this.promptCount = 0;
this.gen = envGen.gen as Generator;
// do not add second parameter with value true
// some generators rely on fact that this.env.cwd and
// the current working directory is changed.
this.gen.destinationRoot(targetFolder);
// notifies ui wether generator is in writing state
this.setGenInWriting(this.gen);
// handles generator install step if exists
this.onGenInstall(this.gen);
// handles generator errors
this.handleErrors(envGen.env, this.gen, generatorNamespace);

await envGen.env.runGenerator(envGen.gen);
if (!this.errorThrown) {
// Without resolve this code worked only for absolute paths without / at the end.
// Generator can put a relative path, path including . and .. and / at the end.
Expand Down Expand Up @@ -316,7 +314,6 @@ export class YeomanUI {
generatorName,
this.getErrorWithAdditionalInfo(error, `env.on(${errorEventName})`)
);
env.emit(errorEventName, error);
});

gen.on(errorEventName, (error: any) =>
Expand Down
27 changes: 27 additions & 0 deletions projects/yeoman-ui/packages/backend/src/youi-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const { get, isFunction } = lodash;

export class YouiAdapter {
private yeomanui: YeomanUI;
private abortController = new AbortController();

constructor(
private readonly youiEvents: YouiEvents,
Expand All @@ -26,6 +27,32 @@ export class YouiAdapter {
this.log = yoUiLog(this.output, this.yeomanui);
}

// --- yeoman-environment v6 adapter contract ---

get signal(): AbortSignal {
return this.abortController.signal;
}

public resetSignal(): void {
this.abortController = new AbortController();
}

public abort(reason?: unknown): void {
if (!this.abortController.signal.aborted) {
this.abortController.abort(reason);
}
}

public onIdle(): Promise<void> {
return Promise.resolve();
}

public async progress<T>(
fn: (progress: { step: (...args: any[]) => void }) => T | Promise<T>
): Promise<T> {
return fn({ step: () => undefined });
}

get colorDiffAdded() {
return chalk.black.bgGreen;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"use strict";

const Generator = require("yeoman-generator-v5");
const Base = Generator.default || Generator;

module.exports = class ComposeSubGenerator extends Base {
writing() {
// Signal on the shared env options so the test can observe the sub ran
if (this.options && this.options.composeMarker) {
this.options.composeMarker.subRan = true;
}
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "generator-compose-sub",
"type": "commonjs"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use strict";

const Generator = require("yeoman-generator-v5");
const Base = Generator.default || Generator;

module.exports = class ComposeTopGenerator extends Base {
writing() {
this.composeWith(
{
Generator: require("../../../generator-compose-sub/generators/app/index.js"),
path: require.resolve(
"../../../generator-compose-sub/generators/app/index.js"
),
},
this.options
);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "generator-compose-top",
"type": "commonjs"
}
Loading
Loading