From f5a65fcf109a302358ce7aa36961436075caf6f0 Mon Sep 17 00:00:00 2001 From: Alexander Gilin Date: Wed, 8 Jan 2025 21:20:04 +0200 Subject: [PATCH] refactor: revert of PR: https://github.com/SAP/yeoman-ui/pull/857 until better solution will found --- packages/backend/src/yeomanui.ts | 90 +++++-------- packages/backend/test/yeomanui.spec.ts | 172 ++++++------------------- 2 files changed, 67 insertions(+), 195 deletions(-) diff --git a/packages/backend/src/yeomanui.ts b/packages/backend/src/yeomanui.ts index 16a502fa..d6ceeded 100644 --- a/packages/backend/src/yeomanui.ts +++ b/packages/backend/src/yeomanui.ts @@ -1,5 +1,5 @@ import * as path from "path"; -import { promises, watch, statSync, FSWatcher } from "fs"; +import { promises } from "fs"; import * as _ from "lodash"; import * as inquirer from "inquirer"; import { ReplayUtils, ReplayState } from "./replayUtils"; @@ -171,23 +171,26 @@ export class YeomanUI { return { name: "Select Generator", questions: normalizedQuestions }; } - private startWatch(folderPath: string, targetMap: Map): FSWatcher { - return watch(folderPath, { recursive: true }, (eventType, filename) => { - try { - // file or directory added or removed - if (`rename` === eventType && filename) { - const fullPath = path.join(folderPath, filename); - const stat = statSync(fullPath); - targetMap.set(fullPath, { - isDirectory: stat.isDirectory(), - size: stat.size, - modifiedTime: stat.mtime, - }); + private async getChildDirectories(folderPath: string) { + const childDirs: string[] = []; + const result = { targetFolderPath: folderPath, childDirs }; + + try { + for (const file of await promises.readdir(folderPath)) { + const resourcePath: string = path.join(folderPath, file); + try { + if ((await promises.stat(resourcePath)).isDirectory()) { + result.childDirs.push(resourcePath); + } + } catch (e) { + // ignore : broken soft link or access denied } - } catch (error) { - // ignore : broken soft link or access denied } - }); + } catch (error) { + result.childDirs = []; + } + + return result; } private wsGet(key: string): string { @@ -196,15 +199,13 @@ export class YeomanUI { private async runGenerator(generatorNamespace: string) { this.generatorName = generatorNamespace; - let watcher: FSWatcher; - const targetFolderMap: Map = new Map(); - // let targetChanges: any; // TODO: should create and set target dir only after user has selected a generator; // see issue: https://github.com/yeoman/environment/issues/55 // process.chdir() doesn't work after environment has been created try { const targetFolder = this.getCwd(); await promises.mkdir(targetFolder, { recursive: true }); + const dirsBefore = await this.getChildDirectories(targetFolder); const options = { logger: this.logger.getChildLogger({ label: generatorNamespace }), @@ -235,18 +236,12 @@ export class YeomanUI { // handles generator errors this.handleErrors(envGen.env, this.gen, generatorNamespace); - const targetFolderAfter = resolve(this.getCwd(), this.gen.destinationRoot()); - watcher = this.startWatch(targetFolderAfter, targetFolderMap); - 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. - this.onGeneratorSuccess( - generatorNamespace, - { targetFolderPath: targetFolder }, - { targetFolderPath: targetFolderAfter, changeMap: targetFolderMap }, - ); + const dirsAfter = await this.getChildDirectories(resolve(this.getCwd(), this.gen.destinationRoot())); + this.onGeneratorSuccess(generatorNamespace, dirsBefore, dirsAfter); } } catch (error) { if (error instanceof GeneratorNotFoundError) { @@ -255,7 +250,6 @@ export class YeomanUI { this.onGeneratorFailure(generatorNamespace, this.getErrorWithAdditionalInfo(error, "runGenerator()")); } finally { this.onUncaughtException && process.removeListener("uncaughtException", this.onUncaughtException); - watcher?.close(); } } @@ -418,46 +412,22 @@ export class YeomanUI { return firstQuestionName ? _.startCase(firstQuestionName) : `Step ${this.promptCount}`; } - private onGeneratorSuccess( - generatorName: string, - resourcesBeforeGen?: { targetFolderPath: string }, - resourcesAfterGen?: { targetFolderPath: string; changeMap: Map }, - ) { - function _getChangesStat( - targetPath: string, - changeMap: Map, - ): { folders: string[]; files: string[] } { - const files: string[] = []; - const folders: string[] = []; // Top-level folders - - for (const [filePath, fileInfo] of changeMap) { - if (fileInfo.isDirectory) { - const relativePath = path.relative(targetPath, filePath).trim(); - const parts = relativePath.split(path.sep).filter(Boolean); - if (parts.length === 1) { - // Top-level folder - folders.push(filePath); - } - } else { - files.push(filePath); - } - } - return { folders, files }; - } - + private onGeneratorSuccess(generatorName: string, resourcesBeforeGen?: any, resourcesAfterGen?: any) { let targetFolderPath: string = null; // All the paths here absolute normilized paths. const targetFolderPathBeforeGen: string = _.get(resourcesBeforeGen, "targetFolderPath"); const targetFolderPathAfterGen: string = _.get(resourcesAfterGen, "targetFolderPath"); let hasNewDirs: boolean = true; if (targetFolderPathBeforeGen === targetFolderPathAfterGen) { - const stats = _getChangesStat(targetFolderPathAfterGen, resourcesAfterGen.changeMap); - if (_.size(stats.folders) === 1) { + const newDirs: string[] = _.difference( + _.get(resourcesAfterGen, "childDirs"), + _.get(resourcesBeforeGen, "childDirs"), + ); + if (_.size(newDirs) === 1) { // One folder added by generator and targetFolderPath/destinationRoot was not changed by generator. // ---> Fiori project generator flow. - targetFolderPath = stats.folders[0]; - } else if (_.size(stats.folders) === 0 && _.size(stats.files) === 0) { - // No files or folders added by generator. + targetFolderPath = newDirs[0]; + } else if (_.size(newDirs) === 0) { hasNewDirs = false; } //else { // _.size(newDirs) > 1 (5 folders) diff --git a/packages/backend/test/yeomanui.spec.ts b/packages/backend/test/yeomanui.spec.ts index 3a6f5e0a..73956f5d 100644 --- a/packages/backend/test/yeomanui.spec.ts +++ b/packages/backend/test/yeomanui.spec.ts @@ -1,8 +1,7 @@ import { vscode } from "./mockUtil"; -import { createSandbox, SinonSandbox, SinonMock, SinonStub } from "sinon"; +import { createSandbox, SinonSandbox, SinonMock } from "sinon"; const datauri = require("datauri"); // eslint-disable-line @typescript-eslint/no-var-requires -import * as fs from "fs"; -import * as path from "path"; +import { promises } from "fs"; import { expect } from "chai"; import * as _ from "lodash"; import { YeomanUI } from "../src/yeomanui"; @@ -10,6 +9,7 @@ import { ReplayUtils } from "../src/replayUtils"; import { YouiEvents } from "../src/youi-events"; import { IMethod, IPromiseCallbacks, IRpc } from "@sap-devx/webview-rpc/out.ext/rpc-common"; import { GeneratorFilter } from "../src/filter"; +import { homedir } from "os"; import messages from "../src/messages"; import { AnalyticsWrapper } from "../src/usage-report/usage-analytics-wrapper"; import { AppWizard, MessageType } from "@sap-devx/yeoman-ui-types"; @@ -141,14 +141,14 @@ describe("yeomanui unit test", () => { sandbox = createSandbox(); }); - afterEach(() => { + after(() => { sandbox.restore(); }); beforeEach(() => { envUtilsMock = sandbox.mock(Env); appWizardMock = sandbox.mock(appWizard); - fsPromisesMock = sandbox.mock(fs.promises); + fsPromisesMock = sandbox.mock(promises); datauriMock = sandbox.mock(datauri); rpcMock = sandbox.mock(rpc); loggerMock = sandbox.mock(testLogger); @@ -173,69 +173,15 @@ describe("yeomanui unit test", () => { envUtilsMock.verify(); }); - describe("startWatch", () => { - let stubStat: SinonStub; - let callBack: (event: string, file: string) => void; - const targetMap: Map = new Map(); - - beforeEach(() => { - stubStat = sandbox.stub(fs, "statSync"); - sandbox.stub(fs, "watch").value((p: string, o: any, cb: (event: string, file: string) => void) => { - callBack = cb; - return { - close: () => {}, - }; - }); - }); - - afterEach(() => { - targetMap.clear(); - }); - - it("startWatch, entire folder content added", () => { - const targetPath = path.join("/", "targetRoot"); - const folderPath = path.join("testPath"); - const subFolderPath = path.join(folderPath, "subFolderPath"); - const filePath = path.join(subFolderPath, "file1"); - stubStat.withArgs(path.join(targetPath, folderPath)).returns({ isDirectory: () => true }); - stubStat.withArgs(path.join(targetPath, subFolderPath)).returns({ isDirectory: () => true }); - stubStat.withArgs(path.join(targetPath, filePath)).returns({ isDirectory: () => false, size: 100, mtime: 1233 }); - expect(yeomanUi["startWatch"](targetPath, targetMap)).to.not.empty; - callBack("rename", folderPath); - callBack("rename", subFolderPath); - callBack("rename", filePath); - expect(targetMap.size).to.equal(3); - expect(targetMap.get(path.join(targetPath, filePath))).to.deep.equal({ - isDirectory: false, - size: 100, - modifiedTime: 1233, - }); - }); - - it("startWatch, 'change' event skipped", () => { - const targetPath = path.join("/", "targetRoot"); - const folderPath = path.join("testPath"); - stubStat.withArgs(path.join(targetPath, folderPath)).returns({ isDirectory: () => true }); - expect(yeomanUi["startWatch"](targetPath, targetMap)).to.not.empty; - callBack("change", folderPath); - expect(targetMap.size).to.equal(0); - }); + it("getChildDirectories", async () => { + const res = await yeomanUi["getChildDirectories"](homedir()); + expect(res.targetFolderPath).is.not.empty; + expect(res.childDirs).is.not.empty; - it("startWatch, 'statSync' throws exception", () => { - const targetPath = path.join("/", "targetRoot"); - const folderPath = path.join("testPath"); - stubStat.withArgs(path.join(targetPath, folderPath)).throws("statSync error"); - expect(yeomanUi["startWatch"](targetPath, targetMap)).to.not.empty; - callBack("rename", folderPath); - expect(targetMap.size).to.equal(0); - }); - - it("startWatch, filename not provided", () => { - const targetPath = path.join("/", "targetRoot"); - expect(yeomanUi["startWatch"](targetPath, targetMap)).to.not.empty; - callBack("rename", ""); - expect(targetMap.size).to.equal(0); - }); + const errorMessage = "readdir failure"; + fsPromisesMock.expects("readdir").throws(new Error(errorMessage)); + const resFail = await yeomanUi["getChildDirectories"](homedir()); + expect(resFail.childDirs).is.empty; }); describe("receiveIsWebviewReady", () => { @@ -282,15 +228,6 @@ describe("yeomanui unit test", () => { }, ]; - beforeEach(() => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - sandbox.stub(fs, "watch").value((p: string, o: any, cb: (event: string, file: string) => void) => { - return { - close: () => {}, - }; - }); - }); - it("flow is successfull", async () => { envUtilsMock.expects("getGeneratorsData").resolves(gensMeta); envUtilsMock @@ -311,9 +248,9 @@ describe("yeomanui unit test", () => { wsConfigMock.expects("get").withExactArgs("ApplicationWizard.HideGenerator").once().returns(""); rpcMock.expects("invoke").withArgs("showPrompt").resolves({ generator: "test1-project:app" }); rpcMock.expects("invoke").withExactArgs("setGenInWriting", [false]).resolves(); - trackerWrapperMock.expects("updateGeneratorStarted").once().returns(undefined); - trackerWrapperMock.expects("updateGeneratorSelected").withArgs("test1-project:app").returns(undefined); - trackerWrapperMock.expects("updateGeneratorEnded").once().returns(undefined); + trackerWrapperMock.expects("updateGeneratorStarted").once().resolves(); + trackerWrapperMock.expects("updateGeneratorSelected").withArgs("test1-project:app").resolves(); + trackerWrapperMock.expects("updateGeneratorEnded").once().resolves(); await yeomanUi["receiveIsWebviewReady"](); }); @@ -345,11 +282,11 @@ describe("yeomanui unit test", () => { trackerWrapperMock.expects("updateGeneratorEnded").once().resolves(); await yeomanUi["receiveIsWebviewReady"](); }); + }); - it("showPrompt without questions", async () => { - const answers = await yeomanUi.showPrompt([]); - expect(answers).to.be.empty; - }); + it("showPrompt without questions", async () => { + const answers = await yeomanUi.showPrompt([]); + expect(answers).to.be.empty; }); describe("executeCommand", () => { @@ -386,12 +323,6 @@ describe("yeomanui unit test", () => { await yeomanUi._notifyGeneratorsChange(); }); - it("running generator throws error and watcher does not exist", async () => { - wsConfigMock.expects("get").withExactArgs("ApplicationWizard.TargetFolder").throws("get error"); - sandbox.stub(yeomanUi as any, "onGeneratorFailure").returns({}); - await yeomanUi["runGenerator"]("test1-project:app"); - }); - describe("_notifyGeneratorsInstall", () => { it("called without args parameter or args = undefined", async () => { rpcMock.expects("invoke").withExactArgs("isGeneratorsPrompt").never(); @@ -1258,7 +1189,6 @@ describe("yeomanui unit test", () => { let doGeneratorDoneSpy: any; const create_and_close = "Create the project and close it for future use"; const open_in_new_ws = "Open the project in a stand-alone folder"; - const changes = new Map(); beforeEach(() => { doGeneratorDoneSpy = sandbox.spy(youiEvents, "doGeneratorDone"); @@ -1266,17 +1196,16 @@ describe("yeomanui unit test", () => { afterEach(() => { doGeneratorDoneSpy.restore(); - changes.clear(); }); it("onGeneratorSuccess - one dir was created", () => { const beforeGen = { - targetFolderPath: path.join("/", "testDestinationRoot"), + targetFolderPath: "testDestinationRoot", + childDirs: ["dirparh1"], }; - changes.set(path.join("/", "testDestinationRoot", "dirpath1"), { isDirectory: true }); const afterGen = { - targetFolderPath: path.join("/", "testDestinationRoot"), - changeMap: changes, + targetFolderPath: "testDestinationRoot", + childDirs: ["dirparh1", "dirpath2"], }; trackerWrapperMock.expects("updateGeneratorEnded").withArgs("testGenName").resolves(); yeomanUi["onGeneratorSuccess"]("testGenName", beforeGen, afterGen); @@ -1287,42 +1216,19 @@ describe("yeomanui unit test", () => { _.get(yeomanUi, "uiOptions.messages.artifact_with_name_generated", (a: string) => "")("testGenName"), create_and_close, "files", - path.join(beforeGen.targetFolderPath, "dirpath1"), + "dirpath2", ), ).to.be.true; }); it("onGeneratorSuccess - two dirs were created", () => { - const beforeGen = { - targetFolderPath: path.join("/", "testDestinationRoot"), - }; - changes.set(path.join("/", "testDestinationRoot", "dirpath2"), { isDirectory: true }); - changes.set(path.join("/", "testDestinationRoot", "dirpath3"), { isDirectory: true }); - const afterGen = { - targetFolderPath: path.join("/", "testDestinationRoot"), - changeMap: changes, - }; - trackerWrapperMock.expects("updateGeneratorEnded").withArgs("testGenName").resolves(); - yeomanUi["onGeneratorSuccess"]("testGenName", beforeGen, afterGen); - expect( - doGeneratorDoneSpy.calledWith( - true, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _.get(yeomanUi, "uiOptions.messages.artifact_with_name_generated", (a: string) => "")("testGenName"), - create_and_close, - "files", - null, - ), - ).to.be.true; - }); - - it("onGeneratorSuccess - zero dirs were created and no files generated", () => { const beforeGen = { targetFolderPath: "testDestinationRoot", + childDirs: ["dirparh1"], }; const afterGen = { targetFolderPath: "testDestinationRoot", - changeMap: changes, + childDirs: ["dirparh1", "dirpath2", "dirpath3"], }; trackerWrapperMock.expects("updateGeneratorEnded").withArgs("testGenName").resolves(); yeomanUi["onGeneratorSuccess"]("testGenName", beforeGen, afterGen); @@ -1332,21 +1238,20 @@ describe("yeomanui unit test", () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars _.get(yeomanUi, "uiOptions.messages.artifact_with_name_generated", (a: string) => "")("testGenName"), create_and_close, - "", + "files", null, ), ).to.be.true; }); - it("onGeneratorSuccess - zero dirs were created but files generated", () => { + it("onGeneratorSuccess - zero dirs were created", () => { const beforeGen = { targetFolderPath: "testDestinationRoot", + childDirs: ["dirparh1"], }; - changes.set(path.join("/", "testDestinationRoot", "webapp", "changes"), { isDirectory: true }); - changes.set(path.join("/", "testDestinationRoot", "webapp", "changes", "file1"), { isDirectory: false }); const afterGen = { targetFolderPath: "testDestinationRoot", - changeMap: changes, + childDirs: ["dirparh1"], }; trackerWrapperMock.expects("updateGeneratorEnded").withArgs("testGenName").resolves(); yeomanUi["onGeneratorSuccess"]("testGenName", beforeGen, afterGen); @@ -1356,7 +1261,7 @@ describe("yeomanui unit test", () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars _.get(yeomanUi, "uiOptions.messages.artifact_with_name_generated", (a: string) => "")("testGenName"), create_and_close, - "files", + "", null, ), ).to.be.true; @@ -1365,8 +1270,7 @@ describe("yeomanui unit test", () => { it("onGeneratorSuccess - targetFolderPath was changed by generator", () => { const beforeGen = { targetFolderPath: "testDestinationRoot" }; const afterGen = { - targetFolderPath: path.join("/", "testDestinationRoot", "generatedProject"), - changeMap: changes, + targetFolderPath: "testDestinationRoot/generatedProject", }; trackerWrapperMock.expects("updateGeneratorEnded").withArgs("testGenName").resolves(); yeomanUi["onGeneratorSuccess"]("testGenName", beforeGen, afterGen); @@ -1377,7 +1281,7 @@ describe("yeomanui unit test", () => { _.get(yeomanUi, "uiOptions.messages.artifact_with_name_generated", (a: string) => "")("testGenName"), create_and_close, "files", - afterGen.targetFolderPath, + "testDestinationRoot/generatedProject", ), ).to.be.true; }); @@ -1395,8 +1299,7 @@ describe("yeomanui unit test", () => { yeomanUi["typesMap"].set("foodq:app", "project"); const beforeGen = { targetFolderPath: "testDestinationRoot" }; const afterGen = { - targetFolderPath: path.join("testDestinationRoot", "generatedProject"), - changeMap: changes, + targetFolderPath: "testDestinationRoot/generatedProject", }; trackerWrapperMock.expects("updateGeneratorEnded").withArgs("foodq:app").resolves(); yeomanUi["onGeneratorSuccess"]("foodq:app", beforeGen, afterGen); @@ -1407,7 +1310,7 @@ describe("yeomanui unit test", () => { _.get(yeomanUi, "uiOptions.messages.artifact_with_name_generated", (a: string) => "")("foodq:app"), open_in_new_ws, "project", - afterGen.targetFolderPath, + "testDestinationRoot/generatedProject", ), ).to.be.true; }); @@ -1419,7 +1322,6 @@ describe("yeomanui unit test", () => { const beforeGen = { targetFolderPath: "testDestinationRoot" }; const afterGen = { targetFolderPath: "testDestinationRoot/generatedProject", - changeMap: changes, }; trackerWrapperMock.expects("updateGeneratorEnded").withArgs("fiori-generator:app").resolves(); yeomanUi["onGeneratorSuccess"]("fiori-generator:app", beforeGen, afterGen); @@ -1430,7 +1332,7 @@ describe("yeomanui unit test", () => { _.get(yeomanUi, "uiOptions.messages.artifact_with_name_generated", (a: string) => "")("fiori-generator:app"), create_and_close, "project", - afterGen.targetFolderPath, + "testDestinationRoot/generatedProject", ), ).to.be.true; });