Skip to content

Commit ae8fa31

Browse files
committed
#587 round trip support mainly working. Needs more testing.
1 parent 2241b4c commit ae8fa31

8 files changed

Lines changed: 118 additions & 20 deletions

File tree

web-designer/tcmenu-web-generator/src/main/java/com/thecoderscorner/menu/web/controller/GenerateCodeController.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.springframework.http.ResponseEntity;
3535
import org.springframework.util.FileSystemUtils;
3636
import org.springframework.util.ObjectUtils;
37+
import org.springframework.util.StringUtils;
3738
import org.springframework.web.bind.MethodArgumentNotValidException;
3839
import org.springframework.web.bind.annotation.*;
3940
import org.springframework.web.client.HttpClientErrorException;
@@ -212,7 +213,13 @@ public GenerationResponse generateCode(@RequestBody String requestData, HttpServ
212213
Path tempDir = null;
213214
try {
214215
tempDir = Files.createTempDirectory("tcmenu-gen");
215-
var fileName = VariableNameGenerator.makeNameFromVariable(menuWithOptions.getOptions().getApplicationName());
216+
String fileName = request.getProjectOverrideDir();
217+
if(!StringUtils.hasLength(fileName)) {
218+
fileName = VariableNameGenerator.makeNameFromVariable(menuWithOptions.getOptions().getApplicationName());
219+
logger.info("Generating project dir from name as: " + fileName);
220+
} else {
221+
logger.info("Using project override directory: " + fileName);
222+
}
216223
if(fileName.contains("..") || fileName.contains("/") || fileName.contains("\\")) {
217224
throw new IllegalArgumentException("Invalid filename - directory traversal not allowed");
218225
}
@@ -248,7 +255,7 @@ public GenerationResponse generateCode(@RequestBody String requestData, HttpServ
248255

249256
var howLong = Duration.ofMillis(System.currentTimeMillis() - millisStart);
250257
logger.info("Thank you for choosing TcMenu. Task completed in %02d.%03d seconds".formatted(howLong.toSecondsPart(), howLong.toMillisPart()));
251-
var response = GenerationResponse.okResponse(logger.filesInOutput, logger.logEntries, buildUuid);
258+
var response = GenerationResponse.okResponse(logger.filesInOutput, logger.logEntries, buildUuid, projectPath);
252259
codeBuildCache.put(buildUuid.toString(), new CodeBuildInfo(buildUuid.toString(), uuid, LocalDateTime.now(), response, tempDir));
253260
return response;
254261
} catch(Exception ex) {

web-designer/tcmenu-web-generator/src/main/java/com/thecoderscorner/menu/web/domain/GenerateCodeRequest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
@Data
1515
public class GenerateCodeRequest {
1616
private PersistedProject project;
17+
private String projectOverrideDir;
1718
private List<CreatorProperty> existingProperties;
1819
private List<GeneratedFile> requiredFiles;
1920
}

web-designer/tcmenu-web-generator/src/main/java/com/thecoderscorner/menu/web/domain/GenerationResponse.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ public static GenerationResponse badResponse(List<LogEntry> logs) {
2020
return new GenerationResponse(false, List.of(), logs, null);
2121
}
2222

23-
public static GenerationResponse okResponse(List<GeneratedFile> files, List<LogEntry> logs, UUID buildId) {
24-
return new GenerationResponse(true, sanitiseFilesInOutput(files), logs, buildId.toString());
23+
public static GenerationResponse okResponse(List<GeneratedFile> files, List<LogEntry> logs, UUID buildId, Path basePath) {
24+
return new GenerationResponse(true, sanitiseFilesInOutput(files, basePath), logs, buildId.toString());
2525
}
2626

27-
private static List<GeneratedFile> sanitiseFilesInOutput(List<GeneratedFile> files) {
27+
private static List<GeneratedFile> sanitiseFilesInOutput(List<GeneratedFile> files, Path basePath) {
2828
return files.stream()
29-
.map(gf -> new GeneratedFile(Path.of(gf.getFileName()).getFileName().toString(), gf.getContent(), gf.isAlwaysOverwrite()))
29+
.map(gf -> new GeneratedFile(basePath.relativize(Path.of(gf.getFileName())).toString(),
30+
gf.getContent(), gf.isAlwaysOverwrite()))
3031
.toList();
3132
}
3233

web-designer/tcmenugen/src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ export function getDirectoryHandle(): FileSystemDirectoryHandle|null {
3030
return globalDirectoryHandle;
3131
}
3232

33-
export const findFileWithExtension = async (directoryHandle: any, ext1: string, ext2: string = "undef"): Promise<any | null> => {
33+
export const findFileWithExtension = async (directoryHandle: any, ext1: string, ext2: string = ".undef"): Promise<any | null> => {
3434
for await (const entry of directoryHandle.values()) {
35-
if (entry.kind === 'file' && (entry.name.endsWith('.' + ext1) || entry.name.endsWith('.' + ext2))) {
35+
if (entry.kind === 'file' && (entry.name.endsWith(ext1) || entry.name.endsWith(ext2))) {
3636
return entry;
3737
}
3838
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import {GeneratedFile} from "./TcCodeGeneration";
2+
import {MenuTreeWithCodeOptions, RoundTripMode} from "../domain/ProjectStruct";
3+
import {getDirectoryHandle} from "../App";
4+
5+
export async function filePatcher(files: GeneratedFile[], project: MenuTreeWithCodeOptions) {
6+
if(project.roundTripMode !== RoundTripMode.DIRECTORY_IN_BROWSER) {
7+
throw new Error("Round trip mode must be enabled for file patching");
8+
}
9+
10+
const rootDirectory = getDirectoryHandle();
11+
if(rootDirectory == null) {
12+
throw new Error("The project directory is not resolvable. Please use the zip and patch manually");
13+
}
14+
15+
const permission = await (rootDirectory as any).queryPermission({ mode: "readwrite" });
16+
if(permission !== "granted") {
17+
const requestedPermission = await (rootDirectory as any).requestPermission({ mode: "readwrite" });
18+
if(requestedPermission !== "granted") {
19+
throw new Error("Write permission was not granted. Please use the zip and patch manually");
20+
}
21+
}
22+
23+
const resolvedFiles = files.map(file => {
24+
const pathSegments = file.fileName.split("/");
25+
26+
if(
27+
file.fileName.startsWith("/") ||
28+
file.fileName.includes("\\") ||
29+
pathSegments.some(segment => segment === "" || segment === "." || segment === "..")
30+
) {
31+
throw new Error("Invalid generated file path: " + file.fileName);
32+
}
33+
34+
const directorySegments = pathSegments.slice(0, -1);
35+
const fileName = pathSegments[pathSegments.length - 1];
36+
37+
if(directorySegments.length > 3) {
38+
throw new Error("Too many levels in the returned zip. Path=" + file.fileName);
39+
}
40+
41+
return {
42+
file,
43+
directorySegments,
44+
fileName
45+
};
46+
});
47+
48+
for(const resolvedFile of resolvedFiles) {
49+
let directoryHandle = rootDirectory;
50+
51+
for(const segment of resolvedFile.directorySegments) {
52+
directoryHandle = await directoryHandle.getDirectoryHandle(segment, { create: true });
53+
}
54+
55+
if(!resolvedFile.file.alwaysOverwrite) {
56+
try {
57+
await directoryHandle.getFileHandle(resolvedFile.fileName, { create: false });
58+
console.log(`File ${resolvedFile.fileName} is not overwritable and already exists, skipping`);
59+
continue;
60+
} catch(error) {
61+
console.log(`File ${resolvedFile.fileName} does not exist, creating`);
62+
}
63+
} else {
64+
console.log(`File ${resolvedFile.fileName} will be written out`);
65+
}
66+
67+
const fileHandle = await directoryHandle.getFileHandle(resolvedFile.fileName, { create: true });
68+
const writable = await (fileHandle as any).createWritable();
69+
70+
try {
71+
await writable.write(resolvedFile.file.content);
72+
} finally {
73+
await writable.close();
74+
}
75+
}
76+
}

web-designer/tcmenugen/src/generator/GenerateCodeView.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ export function GenerateCodeView() {
329329
if(project.roundTripMode === RoundTripMode.DIRECTORY_IN_BROWSER) {
330330
const dir = getDirectoryHandle();
331331
if(dir == null) throw new Error("No directory handle");
332-
const inoFile = await findFileWithExtension(dir, 'ino', 'cpp');
332+
const inoFile = await findFileWithExtension(dir, '.ino', '_main.cpp');
333333
if(inoFile !== null) {
334334
const inoActFile = await inoFile.getFile();
335335
ret.push({fileName: inoFile.name, content: await inoActFile.text(), alwaysOverwrite: true});
@@ -382,14 +382,10 @@ export function GenerateCodeView() {
382382

383383
if(!project) return;
384384

385-
runGenerateCode(project, await requiredFilesForBuild()).then(response => {
385+
const possibleDir = getDirectoryHandle()?.name ?? null;
386+
runGenerateCode(project, await requiredFilesForBuild(), possibleDir).then(response => {
386387
console.log("Success:", response.successful);
387388
console.log("Generated files:", response.generatedFiles ? response.generatedFiles.length : 0);
388-
if (response.generatedFiles) {
389-
for(const f of response.generatedFiles) {
390-
console.log(f.fileName);
391-
}
392-
}
393389
setGenerationResponse(response);
394390
}).catch(err => {
395391
console.error("Generation failed", err);
@@ -427,7 +423,7 @@ export function GenerateCodeView() {
427423
}
428424

429425
if (generationResponse) {
430-
return <GeneratorLogView response={generationResponse} onDismiss={() => setGenerationResponse(null)} />;
426+
return <GeneratorLogView response={generationResponse} menuProject={project} onDismiss={() => setGenerationResponse(null)} />;
431427
}
432428

433429
return (

web-designer/tcmenugen/src/generator/GeneratorLogView.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import React from "react";
22
import {GenerationResponse, LogEntry} from "./TcCodeGeneration";
33
import "./GeneratorLogView.css";
4+
import {MenuTreeWithCodeOptions, RoundTripMode} from "../domain/ProjectStruct";
5+
import {filePatcher} from "./FilePatcher";
46

57
export interface CodeGeneratedProperties {
68
response: GenerationResponse;
79
onDismiss: () => void;
10+
menuProject: MenuTreeWithCodeOptions;
811
}
912

10-
export const GeneratorLogView: React.FC<CodeGeneratedProperties> = ({response, onDismiss}) => {
13+
export const GeneratorLogView: React.FC<CodeGeneratedProperties> = ({response, onDismiss, menuProject}) => {
1114
const getLevelClass = (level: string) => {
1215
switch (level.toUpperCase()) {
1316
case 'ERROR': return 'log-level-error';
@@ -19,6 +22,15 @@ export const GeneratorLogView: React.FC<CodeGeneratedProperties> = ({response, o
1922
}
2023
};
2124

25+
async function onPatchFiles() {
26+
try {
27+
await filePatcher(response.generatedFiles, menuProject);
28+
alert("All files patched successfully!");
29+
} catch (error) {
30+
alert("Failed to patch files: " + error);
31+
}
32+
}
33+
2234
return (
2335
<div className="generator-log-container">
2436
<h3>We need your feedback on where to take things next!</h3>
@@ -41,6 +53,8 @@ export const GeneratorLogView: React.FC<CodeGeneratedProperties> = ({response, o
4153
Download Zip
4254
</a>
4355
)}
56+
{response.successful && menuProject.roundTripMode === RoundTripMode.DIRECTORY_IN_BROWSER &&
57+
<button type="button" className="download-button" style={{fontSize: "100%"}} onClick={onPatchFiles}>Apply Directly</button>}
4458
<button type="button" className="dismiss-button" onClick={onDismiss}>Dismiss</button>
4559
</div>
4660
</div>

web-designer/tcmenugen/src/generator/TcCodeGeneration.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,10 @@ export async function getActiveProfile(): Promise<string> {
139139
return "noenv";
140140
}
141141

142-
function jsonifyGenerationRequest(project: MenuTreeWithCodeOptions, requiredFiles: GeneratedFile[]): string {
142+
function jsonifyGenerationRequest(project: MenuTreeWithCodeOptions, requiredFiles: GeneratedFile[], projectOverrideDir: string|null): string {
143143
return JSON.stringify({
144144
project: projectToPersistedJson(project),
145+
projectOverrideDir: projectOverrideDir,
145146
existingProperties: project.options.lastProperties.map(p => ({
146147
name: p.name,
147148
subsystem: SubSystem[p.subsystem],
@@ -151,10 +152,12 @@ function jsonifyGenerationRequest(project: MenuTreeWithCodeOptions, requiredFile
151152
});
152153
}
153154

154-
export async function runGenerateCode(request: MenuTreeWithCodeOptions, requiredFile: GeneratedFile[]): Promise<GenerationResponse> {
155+
export async function runGenerateCode(request: MenuTreeWithCodeOptions,
156+
requiredFile: GeneratedFile[],
157+
projectOverrideDir: string|null = null): Promise<GenerationResponse> {
155158
let req = await fetch("/api/v1/generator/generate", {
156159
method: "POST", headers: {"Content-Type": "application/json", "Accept": "application/json"},
157-
body: jsonifyGenerationRequest(request, requiredFile)
160+
body: jsonifyGenerationRequest(request, requiredFile, projectOverrideDir)
158161
});
159162
if (!req.ok) {
160163
throw new Error(`Unable to generate code, status was ${req.status}`);

0 commit comments

Comments
 (0)