From 4117bc0849d728ea3412e2198041d26b25e2ee7e Mon Sep 17 00:00:00 2001
From: JulienChampagnol
Date: Sun, 18 Jan 2026 13:18:38 +0100
Subject: [PATCH 01/28] install config
---
app/utils/local.js | 1 +
package.json | 1 +
2 files changed, 2 insertions(+)
diff --git a/app/utils/local.js b/app/utils/local.js
index ce34bfc0..ebbc2d4e 100644
--- a/app/utils/local.js
+++ b/app/utils/local.js
@@ -5,6 +5,7 @@ import child_process from "child_process"
import WebSocket from "ws"
// Third party imports
+import config from "config"
import { getPort } from "get-port-please"
import isElectron from "is-electron"
import pTimeout from "p-timeout"
diff --git a/package.json b/package.json
index dc5cf654..863ef3b4 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"@vueuse/nuxt": "13.1.0",
"@vueuse/rxjs": "^14.1.0",
"ajv": "8.17.1",
+ "config": "4.2.0",
"dexie": "3.2.4",
"get-port-please": "3.2.0",
"is-electron": "2.2.2",
From 448cca42d01d6a0f739b3be559de26cdc11121dc Mon Sep 17 00:00:00 2001
From: JulienChampagnol
Date: Mon, 19 Jan 2026 14:24:52 +0100
Subject: [PATCH 02/28] test dynamic imports [skip ci]
---
app/stores/app.js | 2 ++
app/utils/local.js | 1 -
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/app/stores/app.js b/app/stores/app.js
index a17a2aec..2aa63cd2 100644
--- a/app/stores/app.js
+++ b/app/stores/app.js
@@ -144,6 +144,8 @@ export const useAppStore = defineStore("app", () => {
loadedExtensions.value.set(extensionId, extensionData)
console.log(`[AppStore] Extension loaded successfully: ${extensionId}`)
+ const config = await import("config")
+ config.extensions.push({ id: extensionId, path })
return extensionModule
} else {
diff --git a/app/utils/local.js b/app/utils/local.js
index ebbc2d4e..ce34bfc0 100644
--- a/app/utils/local.js
+++ b/app/utils/local.js
@@ -5,7 +5,6 @@ import child_process from "child_process"
import WebSocket from "ws"
// Third party imports
-import config from "config"
import { getPort } from "get-port-please"
import isElectron from "is-electron"
import pTimeout from "p-timeout"
From 69d30584c16d8dc0eeae292dcd0d1a6b29ff4c87 Mon Sep 17 00:00:00 2001
From: JulienChampagnol
Date: Tue, 20 Jan 2026 16:52:09 +0100
Subject: [PATCH 03/28] save [skip ci]
---
app/stores/app.js | 3 ---
app/utils/config.js | 35 +++++++++++++++++++++++++++++++++++
package.json | 2 +-
3 files changed, 36 insertions(+), 4 deletions(-)
create mode 100644 app/utils/config.js
diff --git a/app/stores/app.js b/app/stores/app.js
index 2aa63cd2..b2da9157 100644
--- a/app/stores/app.js
+++ b/app/stores/app.js
@@ -144,9 +144,6 @@ export const useAppStore = defineStore("app", () => {
loadedExtensions.value.set(extensionId, extensionData)
console.log(`[AppStore] Extension loaded successfully: ${extensionId}`)
- const config = await import("config")
- config.extensions.push({ id: extensionId, path })
-
return extensionModule
} else {
throw new Error("Extension must export an install function")
diff --git a/app/utils/config.js b/app/utils/config.js
new file mode 100644
index 00000000..ee838246
--- /dev/null
+++ b/app/utils/config.js
@@ -0,0 +1,35 @@
+import Conf from "conf"
+
+const projectConfigSchema = {
+ name: {
+ type: "string",
+ minLength: 1,
+ required: true,
+ },
+ path: {
+ type: "string",
+ format: "url",
+ required: true,
+ },
+ additionalProperties: false,
+ required: ["name", "path"],
+}
+
+function createProjectConfig(projectName) {
+ console.log(createProjectConfig.name, { config })
+
+ const config = new Conf({ projectName, schema: projectConfigSchema })
+ console.log(createProjectConfig.name, { config })
+ return config
+}
+
+function getProjectConfig(projectName) {
+ console.log(getProjectConfig.name, { projectName })
+
+ const config = config.get(projectName)
+ console.log(getProjectConfig.name, { config })
+
+ return config
+}
+
+export { createProjectConfig, getProjectConfig }
diff --git a/package.json b/package.json
index a06262c3..4c84f4b3 100644
--- a/package.json
+++ b/package.json
@@ -24,7 +24,7 @@
"@vueuse/nuxt": "13.1.0",
"@vueuse/rxjs": "^14.1.0",
"ajv": "8.17.1",
- "config": "4.2.0",
+ "conf": "15.0.2",
"dexie": "4.2.1",
"get-port-please": "3.2.0",
"is-electron": "2.2.2",
From 7947bee22a0c8db507ecfd7572c9ca3077a4fd6f Mon Sep 17 00:00:00 2001
From: JulienChampagnol
Date: Fri, 30 Jan 2026 10:58:36 +0100
Subject: [PATCH 04/28] save wip
---
app/utils/config.js | 35 ------------
app/utils/extension.js | 122 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 122 insertions(+), 35 deletions(-)
delete mode 100644 app/utils/config.js
create mode 100644 app/utils/extension.js
diff --git a/app/utils/config.js b/app/utils/config.js
deleted file mode 100644
index ee838246..00000000
--- a/app/utils/config.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import Conf from "conf"
-
-const projectConfigSchema = {
- name: {
- type: "string",
- minLength: 1,
- required: true,
- },
- path: {
- type: "string",
- format: "url",
- required: true,
- },
- additionalProperties: false,
- required: ["name", "path"],
-}
-
-function createProjectConfig(projectName) {
- console.log(createProjectConfig.name, { config })
-
- const config = new Conf({ projectName, schema: projectConfigSchema })
- console.log(createProjectConfig.name, { config })
- return config
-}
-
-function getProjectConfig(projectName) {
- console.log(getProjectConfig.name, { projectName })
-
- const config = config.get(projectName)
- console.log(getProjectConfig.name, { config })
-
- return config
-}
-
-export { createProjectConfig, getProjectConfig }
diff --git a/app/utils/extension.js b/app/utils/extension.js
new file mode 100644
index 00000000..0341fcb5
--- /dev/null
+++ b/app/utils/extension.js
@@ -0,0 +1,122 @@
+// Node.js imports
+import fs from "fs"
+import path from "path"
+import unzipper from "unzipper"
+
+// Third party imports
+import Conf from "conf"
+
+const projectConfigSchema = {
+ name: {
+ type: "string",
+ minLength: 1,
+ required: true,
+ },
+ path: {
+ type: "string",
+ format: "url",
+ required: true,
+ },
+ additionalProperties: false,
+ required: ["name", "path"],
+}
+
+function createProjectConfig(projectName) {
+ console.log(createProjectConfig.name, { config })
+
+ const config = new Conf({ projectName, schema: projectConfigSchema })
+ console.log(createProjectConfig.name, { config })
+ return config
+}
+
+function getProjectConfig(projectName) {
+ console.log(getProjectConfig.name, { projectName })
+
+ const config = config.get(projectName)
+ console.log(getProjectConfig.name, { config })
+
+ return config
+}
+
+// async function importExtension(vextFilePath, extensionsFolder) {
+// // Validate file exists
+// if (!fs.existsSync(vextFilePath)) {
+// throw new Error("File does not exist")
+// }
+
+// // Get and sanitize filename
+// const filename = path.basename(vextFilePath)
+
+// if (!filename.toLowerCase().endsWith(".vext")) {
+// throw new Error("File must be a .vext")
+// }
+
+// // Create extensions directory
+// await fs.promises.mkdir(extensionsFolder, { recursive: true })
+
+// // Extract extension name
+// const extensionName = filename.includes("-")
+// ? filename.substring(0, filename.lastIndexOf("-"))
+// : filename.replace(".vext", "")
+
+// const extensionPath = path.join(extensionsFolder, extensionName)
+
+// // Remove existing extension if present
+// if (fs.existsSync(extensionPath)) {
+// await fs.promises.rm(extensionPath, { recursive: true, force: true })
+// }
+
+// await fs.promises.mkdir(extensionPath, { recursive: true })
+
+// // Extract the .vext file (which is a zip archive)
+// await new Promise((resolve, reject) => {
+// fs.createReadStream(vextFilePath)
+// .pipe(unzipper.Extract({ path: extensionPath }))
+// .on("close", resolve)
+// .on("error", reject)
+// })
+
+// // Look for the backend executable and frontend JS
+// let backendExecutable = null
+// let frontendFile = null
+
+// const files = await fs.promises.readdir(extensionPath)
+
+// for (const file of files) {
+// const filePath = path.join(extensionPath, file)
+// const stats = await fs.promises.stat(filePath)
+
+// if (stats.isFile()) {
+// if (file.endsWith(".es.js")) {
+// frontendFile = filePath
+// } else if (!file.endsWith(".js") && !file.endsWith(".css")) {
+// backendExecutable = filePath
+// // Make executable (chmod 755)
+// await fs.promises.chmod(backendExecutable, 0o755)
+// }
+// }
+// }
+
+// if (!frontendFile) {
+// throw new Error("Invalid .vext file: missing frontend JavaScript")
+// }
+
+// if (!backendExecutable) {
+// throw new Error("Invalid .vext file: missing backend executable")
+// }
+
+// // Read frontend content
+// const frontendContent = await fs.promises.readFile(frontendFile, "utf-8")
+
+// return {
+// extension_name: extensionName,
+// frontend_content: frontendContent,
+// backend_path: backendExecutable,
+// }
+// }
+
+export {
+ createProjectConfig,
+ // importExtension,
+ getProjectConfig,
+}
From 5cacd6f8b9b61e3790ea0390d121dc24a5a94379 Mon Sep 17 00:00:00 2001
From: JulienChampagnol
Date: Wed, 11 Feb 2026 09:25:50 +0100
Subject: [PATCH 05/28] cleanup imports
---
app/utils/extension.js | 86 +-----------------------------------------
1 file changed, 1 insertion(+), 85 deletions(-)
diff --git a/app/utils/extension.js b/app/utils/extension.js
index 0341fcb5..85c9e6ec 100644
--- a/app/utils/extension.js
+++ b/app/utils/extension.js
@@ -1,7 +1,4 @@
// Node.js imports
-import fs from "fs"
-import path from "path"
-import unzipper from "unzipper"
// Third party imports
import Conf from "conf"
@@ -38,85 +35,4 @@ function getProjectConfig(projectName) {
return config
}
-// async function importExtension(vextFilePath, extensionsFolder) {
-// // Validate file exists
-// if (!fs.existsSync(vextFilePath)) {
-// throw new Error("File does not exist")
-// }
-
-// // Get and sanitize filename
-// const filename = path.basename(vextFilePath)
-
-// if (!filename.toLowerCase().endsWith(".vext")) {
-// throw new Error("File must be a .vext")
-// }
-
-// // Create extensions directory
-// await fs.promises.mkdir(extensionsFolder, { recursive: true })
-
-// // Extract extension name
-// const extensionName = filename.includes("-")
-// ? filename.substring(0, filename.lastIndexOf("-"))
-// : filename.replace(".vext", "")
-
-// const extensionPath = path.join(extensionsFolder, extensionName)
-
-// // Remove existing extension if present
-// if (fs.existsSync(extensionPath)) {
-// await fs.promises.rm(extensionPath, { recursive: true, force: true })
-// }
-
-// await fs.promises.mkdir(extensionPath, { recursive: true })
-
-// // Extract the .vext file (which is a zip archive)
-// await new Promise((resolve, reject) => {
-// fs.createReadStream(vextFilePath)
-// .pipe(unzipper.Extract({ path: extensionPath }))
-// .on("close", resolve)
-// .on("error", reject)
-// })
-
-// // Look for the backend executable and frontend JS
-// let backendExecutable = null
-// let frontendFile = null
-
-// const files = await fs.promises.readdir(extensionPath)
-
-// for (const file of files) {
-// const filePath = path.join(extensionPath, file)
-// const stats = await fs.promises.stat(filePath)
-
-// if (stats.isFile()) {
-// if (file.endsWith(".es.js")) {
-// frontendFile = filePath
-// } else if (!file.endsWith(".js") && !file.endsWith(".css")) {
-// backendExecutable = filePath
-// // Make executable (chmod 755)
-// await fs.promises.chmod(backendExecutable, 0o755)
-// }
-// }
-// }
-
-// if (!frontendFile) {
-// throw new Error("Invalid .vext file: missing frontend JavaScript")
-// }
-
-// if (!backendExecutable) {
-// throw new Error("Invalid .vext file: missing backend executable")
-// }
-
-// // Read frontend content
-// const frontendContent = await fs.promises.readFile(frontendFile, "utf-8")
-
-// return {
-// extension_name: extensionName,
-// frontend_content: frontendContent,
-// backend_path: backendExecutable,
-// }
-// }
-
-export {
- createProjectConfig,
- // importExtension,
- getProjectConfig,
-}
+export { createProjectConfig, getProjectConfig }
From 2c62ee432b961de210ca12d6cfd6c8bd00687272 Mon Sep 17 00:00:00 2001
From: JulienChampagnol
Date: Wed, 11 Feb 2026 18:01:03 +0100
Subject: [PATCH 06/28] wip unzip extension [skip ci]
---
app/components/Step.vue | 4 +-
app/stores/app.js | 2 +-
app/utils/extension.js | 109 ++++++++++++++++++++++++++++++++--------
app/utils/local.js | 39 ++++++++++++++
package.json | 3 +-
5 files changed, 132 insertions(+), 25 deletions(-)
diff --git a/app/components/Step.vue b/app/components/Step.vue
index e71ebccc..8b5b04c6 100644
--- a/app/components/Step.vue
+++ b/app/components/Step.vue
@@ -56,7 +56,7 @@
color="transparent"
class="d-flex flex-column justify-center ps-2"
>
-
{{ steps[step_index].step_title }}
-
+
{
})
finalURL = URL.createObjectURL(newBlob)
}
-
+ /* @vite-ignore */
const extensionModule = await import(finalURL)
if (finalURL !== path && finalURL.startsWith("blob:")) {
diff --git a/app/utils/extension.js b/app/utils/extension.js
index 85c9e6ec..3e9a90e5 100644
--- a/app/utils/extension.js
+++ b/app/utils/extension.js
@@ -2,37 +2,104 @@
// Third party imports
import Conf from "conf"
+import { getPort } from "get-port-please"
+import _ from "lodash"
+
+// Local imports
+import { unzipFile } from "./local.js"
const projectConfigSchema = {
- name: {
- type: "string",
- minLength: 1,
- required: true,
- },
- path: {
- type: "string",
- format: "url",
- required: true,
+ properties: {
+ projectName: {
+ type: "string",
+ minLength: 1,
+ },
},
additionalProperties: false,
- required: ["name", "path"],
+ required: ["projectName"],
}
-function createProjectConfig(projectName) {
- console.log(createProjectConfig.name, { config })
+const MIN_PORT = 5001
+const MAX_PORT = 5999
+const KILL_TIMEOUT = 2000
- const config = new Conf({ projectName, schema: projectConfigSchema })
- console.log(createProjectConfig.name, { config })
- return config
+const extensionProcesses = new Map()
+
+function projectConf(projectName) {
+ const projectConfig = new Conf({ projectName })
+ // schema: projectConfigSchema
+ console.log(projectConf.name, { projectConfig })
+ return projectConfig
}
-function getProjectConfig(projectName) {
- console.log(getProjectConfig.name, { projectName })
+function extensionsConf(projectConfig) {
+ const extensionsConfig = projectConfig.get("extensions")
+ if (_.isEqual(extensionsConfig, {})) {
+ projectConfig.set("extensions", {})
+ }
+ return extensionsConfig
+}
+
+async function runExtensions(projectName) {
+ const projectConfig = projectConf(projectName)
+ console.log(runExtensions.name, { projectConfig })
+ const extensionsConfig = extensionsConf(projectConfig)
+ console.log(runExtensions.name, { extensionsConfig })
+
+ if (_.isEqual(extensionsConfig, {})) {
+ return Promise.resolve()
+ }
+ console.log("Found extensions config for project:", projectName)
+ const promiseArray = []
+
+ for (const extensionId of Object.keys(extensionsConfig)) {
+ const { archivePath } = extensionsConfig[extensionId]
+ console.log(runExtensions.name, { archivePath })
- const config = config.get(projectName)
- console.log(getProjectConfig.name, { config })
+ await unzipFile(archivePath)
- return config
+ try {
+ const port = await getPort({ portRange: [MIN_PORT, MAX_PORT] })
+ console.log(`[Electron] Extension ${extensionId} will use port ${port}`)
+ extensionProcesses.set(extensionId, { process, port })
+
+ process.on("error", (error) => {
+ console.error(`[${extensionId}] Process error:`, error)
+ })
+
+ process.on("exit", (code) => {
+ console.log(`[${extensionId}] Process exited with code ${code}`)
+ extensionProcesses.delete(extensionId)
+ })
+
+ return { success: true, port }
+ } catch (error) {
+ console.error(
+ `[Extensions] Failed to launch extension ${extensionId}:`,
+ error,
+ )
+ return { success: false, error: error.message }
+ }
+ }
}
-export { createProjectConfig, getProjectConfig }
+function killExtensionMicroservices() {
+ return [...extensionProcesses.values()].map(async ({ process, _port }) => {
+ if (process && !process.killed) {
+ process.kill()
+ try {
+ // Wait for exit or timeout
+ await Promise.race([once(process, "exit"), setTimeout(KILL_TIMEOUT)])
+ } catch {
+ // Ignore errors during kill wait
+ }
+ }
+ })
+}
+
+export {
+ projectConf,
+ extensionsConf,
+ runExtensions,
+ killExtensionMicroservices,
+}
diff --git a/app/utils/local.js b/app/utils/local.js
index 54d29628..0bd8c383 100644
--- a/app/utils/local.js
+++ b/app/utils/local.js
@@ -5,6 +5,8 @@ import child_process from "child_process"
import WebSocket from "ws"
// Third party imports
+
+import JSZip from "jszip"
import { getPort } from "get-port-please"
import isElectron from "is-electron"
import pTimeout from "p-timeout"
@@ -334,6 +336,42 @@ async function run_browser(
})
}
+async function unzipFile(
+ zipFilePath,
+ outputDir = zipFilePath.replace(/\.[^/.]+$/, ""), // Remove the file extension
+) {
+ console.log("Unzipping file...", zipFilePath, outputDir)
+ try {
+ const data = await fs.promises.readFile(zipFilePath)
+ const zip = await JSZip.loadAsync(data)
+ await fs.promises.mkdir(outputDir, { recursive: true })
+ const promises = []
+
+ zip.forEach((relativePath, zipEntry) => {
+ const outputPath = path.join(outputDir, relativePath)
+
+ if (zipEntry.dir) {
+ promises.push(fs.promises.mkdir(outputPath, { recursive: true }))
+ } else {
+ promises.push(
+ zipEntry.async("nodebuffer").then(async (content) => {
+ await fs.promises.mkdir(path.dirname(outputPath), {
+ recursive: true,
+ })
+ await fs.promises.writeFile(outputPath, content)
+ }),
+ )
+ }
+ })
+
+ await Promise.all(promises)
+ console.log("Extraction complete!")
+ } catch (error) {
+ console.error("Error unzipping file:", error)
+ throw error
+ }
+}
+
export {
create_path,
delete_folder_recursive,
@@ -346,4 +384,5 @@ export {
run_back,
run_viewer,
run_browser,
+ unzipFile,
}
diff --git a/package.json b/package.json
index 4c84f4b3..e74401db 100644
--- a/package.json
+++ b/package.json
@@ -24,11 +24,12 @@
"@vueuse/nuxt": "13.1.0",
"@vueuse/rxjs": "^14.1.0",
"ajv": "8.17.1",
- "conf": "15.0.2",
+ "conf": "15.1.0",
"dexie": "4.2.1",
"get-port-please": "3.2.0",
"is-electron": "2.2.2",
"js-file-download": "0.4.12",
+ "jszip": "^3.10.1",
"nuxt": "4.2.2",
"p-timeout": "7.0.1",
"pinia": "3.0.4",
From 814eaba3d91f723cbb950d9290628d349a781802 Mon Sep 17 00:00:00 2001
From: JulienChampagnol
Date: Thu, 12 Feb 2026 12:07:12 +0100
Subject: [PATCH 07/28] wip js transform archive [skip ci]
---
app/utils/extension.js | 55 +++++++++++++++++++++++++++++++++++++++++-
app/utils/local.js | 1 +
2 files changed, 55 insertions(+), 1 deletion(-)
diff --git a/app/utils/extension.js b/app/utils/extension.js
index 3e9a90e5..ec3b81ca 100644
--- a/app/utils/extension.js
+++ b/app/utils/extension.js
@@ -1,4 +1,6 @@
// Node.js imports
+import fs from "node:fs"
+import path from "node:path"
// Third party imports
import Conf from "conf"
@@ -56,7 +58,49 @@ async function runExtensions(projectName) {
const { archivePath } = extensionsConfig[extensionId]
console.log(runExtensions.name, { archivePath })
- await unzipFile(archivePath)
+ const unzippedExtensionPath = await unzipFile(archivePath)
+
+ // Read and validate metadata.json
+ const metadataPath = path.join(unzippedExtensionPath, "metadata.json")
+ let metadata
+ try {
+ const metadataContent = await fs.promises.readFile(metadataPath, "utf-8")
+ metadata = JSON.parse(metadataContent)
+ } catch (error) {
+ throw new Error("Invalid .vext file: missing metadata.json")
+ }
+
+ // Look for backend executable and frontend JS
+ let backendExecutable = null
+ let frontendFile = null
+
+ const files = await fs.promises.readdir(unzippedExtensionPath)
+
+ for (const file of files) {
+ const filePath = path.join(unzippedExtensionPath, file)
+ const stats = await fs.promises.stat(filePath)
+
+ if (stats.isFile()) {
+ if (file.endsWith(".es.js")) {
+ frontendFile = filePath
+ } else if (!file.endsWith(".js") && !file.endsWith(".css")) {
+ backendExecutable = filePath
+ // Make executable (Unix-like systems)
+ await fs.promises.chmod(backendExecutable, 0o755)
+ }
+ }
+ }
+
+ // Validate required files
+ if (!frontendFile) {
+ throw new Error("Invalid .vext file: missing frontend JavaScript")
+ }
+ if (!backendExecutable) {
+ throw new Error("Invalid .vext file: missing backend executable")
+ }
+
+ // Read frontend content
+ const frontendContent = await fs.promises.readFile(frontendFile, "utf-8")
try {
const port = await getPort({ portRange: [MIN_PORT, MAX_PORT] })
@@ -72,6 +116,15 @@ async function runExtensions(projectName) {
extensionProcesses.delete(extensionId)
})
+ const test = {
+ extension_metadata: {
+ name: metadata.name,
+ version: metadata.version,
+ },
+ frontend_content: frontendContent,
+ backend_path: backendExecutable,
+ }
+ console.log("[Extensions] test return :", { test })
return { success: true, port }
} catch (error) {
console.error(
diff --git a/app/utils/local.js b/app/utils/local.js
index 0bd8c383..6451cb0f 100644
--- a/app/utils/local.js
+++ b/app/utils/local.js
@@ -366,6 +366,7 @@ async function unzipFile(
await Promise.all(promises)
console.log("Extraction complete!")
+ return outputDir
} catch (error) {
console.error("Error unzipping file:", error)
throw error
From 5cdbcf56f3a2f73c00d8f8e339ec037046dac4f9 Mon Sep 17 00:00:00 2001
From: JulienChampagnol
Date: Tue, 17 Feb 2026 09:44:57 +0100
Subject: [PATCH 08/28] unzip in projectFolderPath
---
app/utils/extension.js | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/app/utils/extension.js b/app/utils/extension.js
index ec3b81ca..76f670a3 100644
--- a/app/utils/extension.js
+++ b/app/utils/extension.js
@@ -42,7 +42,7 @@ function extensionsConf(projectConfig) {
return extensionsConfig
}
-async function runExtensions(projectName) {
+async function runExtensions(projectName, projectFolderPath) {
const projectConfig = projectConf(projectName)
console.log(runExtensions.name, { projectConfig })
const extensionsConfig = extensionsConf(projectConfig)
@@ -58,7 +58,10 @@ async function runExtensions(projectName) {
const { archivePath } = extensionsConfig[extensionId]
console.log(runExtensions.name, { archivePath })
- const unzippedExtensionPath = await unzipFile(archivePath)
+ const unzippedExtensionPath = await unzipFile(
+ archivePath,
+ projectFolderPath,
+ )
// Read and validate metadata.json
const metadataPath = path.join(unzippedExtensionPath, "metadata.json")
From debd97b28e08947ca4e85005923b43ab7fce33d4 Mon Sep 17 00:00:00 2001
From: JulienChampagnol
Date: Thu, 19 Feb 2026 10:37:16 +0100
Subject: [PATCH 09/28] uploadExtension
---
app/utils/extension.js | 34 ++++++++++++++++++++++++++++++++--
1 file changed, 32 insertions(+), 2 deletions(-)
diff --git a/app/utils/extension.js b/app/utils/extension.js
index 76f670a3..008404df 100644
--- a/app/utils/extension.js
+++ b/app/utils/extension.js
@@ -36,12 +36,41 @@ function projectConf(projectName) {
function extensionsConf(projectConfig) {
const extensionsConfig = projectConfig.get("extensions")
- if (_.isEqual(extensionsConfig, {})) {
- projectConfig.set("extensions", {})
+ console.log(extensionsConf.name, { extensionsConfig })
+ if (_.isEqual(extensionsConfig, undefined)) {
+ projectConfig.set("extensions", [])
}
return extensionsConfig
}
+async function uploadExtension(
+ archiveFileContent,
+ archiveFilename,
+ projectName,
+) {
+ console.log(uploadExtension.name, {
+ archiveFileContent,
+ archiveFilename,
+ projectName,
+ })
+ console.log("uploadExtension", { archiveFilename, projectName })
+ const projectConfig = projectConf(projectName)
+ console.log("uploadExtension", projectConfig.path)
+ const extensionsConfig = extensionsConf(projectConfig)
+ console.log("uploadExtension", { extensionsConfig })
+
+ const configFolderPath = path.dirname(projectConfig.path)
+ const outputPath = path.join(configFolderPath, archiveFilename)
+ console.log(uploadExtension.name, { outputPath })
+ await fs.promises.writeFile(outputPath, archiveFileContent)
+
+ extensionsConfig.push(outputPath)
+ console.log("uploadExtension", { extensionsConfig })
+ projectConfig.set("extensions", extensionsConfig)
+
+ console.log("Extension uploaded successfully", extensionsConfig)
+}
+
async function runExtensions(projectName, projectFolderPath) {
const projectConfig = projectConf(projectName)
console.log(runExtensions.name, { projectConfig })
@@ -156,6 +185,7 @@ function killExtensionMicroservices() {
export {
projectConf,
extensionsConf,
+ uploadExtension,
runExtensions,
killExtensionMicroservices,
}
From ab0d9d4e4fb1474bb1479ce73010ee58b4639621 Mon Sep 17 00:00:00 2001
From: JulienChampagnol
Date: Thu, 19 Feb 2026 17:05:10 +0100
Subject: [PATCH 10/28] cleanup [skip ci]
---
app/utils/extension.js | 81 ++++++++++++------------------------------
1 file changed, 23 insertions(+), 58 deletions(-)
diff --git a/app/utils/extension.js b/app/utils/extension.js
index 008404df..7b896575 100644
--- a/app/utils/extension.js
+++ b/app/utils/extension.js
@@ -48,44 +48,29 @@ async function uploadExtension(
archiveFilename,
projectName,
) {
- console.log(uploadExtension.name, {
- archiveFileContent,
- archiveFilename,
- projectName,
- })
- console.log("uploadExtension", { archiveFilename, projectName })
const projectConfig = projectConf(projectName)
- console.log("uploadExtension", projectConfig.path)
const extensionsConfig = extensionsConf(projectConfig)
- console.log("uploadExtension", { extensionsConfig })
-
const configFolderPath = path.dirname(projectConfig.path)
const outputPath = path.join(configFolderPath, archiveFilename)
- console.log(uploadExtension.name, { outputPath })
+
await fs.promises.writeFile(outputPath, archiveFileContent)
extensionsConfig.push(outputPath)
- console.log("uploadExtension", { extensionsConfig })
projectConfig.set("extensions", extensionsConfig)
-
- console.log("Extension uploaded successfully", extensionsConfig)
}
async function runExtensions(projectName, projectFolderPath) {
const projectConfig = projectConf(projectName)
- console.log(runExtensions.name, { projectConfig })
+ console.log("runExtensions", { projectConfig })
const extensionsConfig = extensionsConf(projectConfig)
- console.log(runExtensions.name, { extensionsConfig })
+ console.log("runExtensions", { extensionsConfig })
if (_.isEqual(extensionsConfig, {})) {
return Promise.resolve()
}
- console.log("Found extensions config for project:", projectName)
- const promiseArray = []
- for (const extensionId of Object.keys(extensionsConfig)) {
- const { archivePath } = extensionsConfig[extensionId]
- console.log(runExtensions.name, { archivePath })
+ for (const archivePath of extensionsConfig) {
+ console.log("runExtensions", { archivePath })
const unzippedExtensionPath = await unzipFile(
archivePath,
@@ -102,62 +87,42 @@ async function runExtensions(projectName, projectFolderPath) {
throw new Error("Invalid .vext file: missing metadata.json")
}
- // Look for backend executable and frontend JS
- let backendExecutable = null
- let frontendFile = null
-
- const files = await fs.promises.readdir(unzippedExtensionPath)
-
- for (const file of files) {
- const filePath = path.join(unzippedExtensionPath, file)
- const stats = await fs.promises.stat(filePath)
+ const { name, version, backendExecutablePath, frontendFilePath } = metadata
- if (stats.isFile()) {
- if (file.endsWith(".es.js")) {
- frontendFile = filePath
- } else if (!file.endsWith(".js") && !file.endsWith(".css")) {
- backendExecutable = filePath
- // Make executable (Unix-like systems)
- await fs.promises.chmod(backendExecutable, 0o755)
- }
- }
- }
-
- // Validate required files
- if (!frontendFile) {
+ if (!frontendFilePath) {
throw new Error("Invalid .vext file: missing frontend JavaScript")
}
- if (!backendExecutable) {
+ if (!backendExecutablePath) {
throw new Error("Invalid .vext file: missing backend executable")
}
-
- // Read frontend content
- const frontendContent = await fs.promises.readFile(frontendFile, "utf-8")
+ const frontendFileAbsolutePath = path.resolve(frontendFilePath)
+ const frontendContent = await fs.promises.readFile(
+ frontendFileAbsolutePath,
+ "utf-8",
+ )
try {
const port = await getPort({ portRange: [MIN_PORT, MAX_PORT] })
- console.log(`[Electron] Extension ${extensionId} will use port ${port}`)
- extensionProcesses.set(extensionId, { process, port })
+ console.log(`[Electron] Extension ${name} will use port ${port}`)
+ extensionProcesses.set(name, { process, port })
process.on("error", (error) => {
- console.error(`[${extensionId}] Process error:`, error)
+ console.error(`[${name}] Process error:`, error)
})
process.on("exit", (code) => {
- console.log(`[${extensionId}] Process exited with code ${code}`)
- extensionProcesses.delete(extensionId)
+ console.log(`[${name}] Process exited with code ${code}`)
+ extensionProcesses.delete(name)
})
const test = {
- extension_metadata: {
- name: metadata.name,
- version: metadata.version,
- },
- frontend_content: frontendContent,
- backend_path: backendExecutable,
+ success: true,
+ extension_metadata: { name, version },
+ frontendContent,
+ backendPort: port,
}
console.log("[Extensions] test return :", { test })
- return { success: true, port }
+ return test
} catch (error) {
console.error(
`[Extensions] Failed to launch extension ${extensionId}:`,
From d05084d84354ae7a1f70ddd031ed857edf28bc15 Mon Sep 17 00:00:00 2001
From: JulienChampagnol
Date: Thu, 19 Feb 2026 17:05:11 +0100
Subject: [PATCH 11/28] fixed dependency
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index e74401db..a7465cf7 100644
--- a/package.json
+++ b/package.json
@@ -29,7 +29,7 @@
"get-port-please": "3.2.0",
"is-electron": "2.2.2",
"js-file-download": "0.4.12",
- "jszip": "^3.10.1",
+ "jszip": "3.10.1",
"nuxt": "4.2.2",
"p-timeout": "7.0.1",
"pinia": "3.0.4",
From 510a9509a090b4cbac3f03194e5efbae7faa0c7e Mon Sep 17 00:00:00 2001
From: JulienChampagnol
Date: Fri, 20 Feb 2026 09:38:32 +0100
Subject: [PATCH 12/28] test separate conf
---
app/utils/config.js | 38 ++++++++++++++++++++++++++++++++++
app/utils/extension.js | 47 +++++++-----------------------------------
2 files changed, 45 insertions(+), 40 deletions(-)
create mode 100644 app/utils/config.js
diff --git a/app/utils/config.js b/app/utils/config.js
new file mode 100644
index 00000000..1ab792ff
--- /dev/null
+++ b/app/utils/config.js
@@ -0,0 +1,38 @@
+// Node.js imports
+
+// Third party imports
+import Conf from "conf"
+import _ from "lodash"
+
+// Local imports
+
+const projectConfigSchema = {
+ properties: {
+ projectName: {
+ type: "string",
+ minLength: 1,
+ },
+ },
+ additionalProperties: false,
+ required: ["projectName"],
+}
+
+const extensionProcesses = new Map()
+
+function projectConf(projectName) {
+ const projectConfig = new Conf({ projectName })
+ // schema: projectConfigSchema
+ console.log(projectConf.name, { projectConfig })
+ return projectConfig
+}
+
+function extensionsConf(projectConfig) {
+ const extensionsConfig = projectConfig.get("extensions")
+ console.log(extensionsConf.name, { extensionsConfig })
+ if (_.isEqual(extensionsConfig, undefined)) {
+ projectConfig.set("extensions", [])
+ }
+ return extensionsConfig
+}
+
+export { projectConf, extensionsConf, extensionProcesses }
diff --git a/app/utils/extension.js b/app/utils/extension.js
index 7b896575..a8982d58 100644
--- a/app/utils/extension.js
+++ b/app/utils/extension.js
@@ -3,45 +3,12 @@ import fs from "node:fs"
import path from "node:path"
// Third party imports
-import Conf from "conf"
import { getPort } from "get-port-please"
import _ from "lodash"
// Local imports
import { unzipFile } from "./local.js"
-
-const projectConfigSchema = {
- properties: {
- projectName: {
- type: "string",
- minLength: 1,
- },
- },
- additionalProperties: false,
- required: ["projectName"],
-}
-
-const MIN_PORT = 5001
-const MAX_PORT = 5999
-const KILL_TIMEOUT = 2000
-
-const extensionProcesses = new Map()
-
-function projectConf(projectName) {
- const projectConfig = new Conf({ projectName })
- // schema: projectConfigSchema
- console.log(projectConf.name, { projectConfig })
- return projectConfig
-}
-
-function extensionsConf(projectConfig) {
- const extensionsConfig = projectConfig.get("extensions")
- console.log(extensionsConf.name, { extensionsConfig })
- if (_.isEqual(extensionsConfig, undefined)) {
- projectConfig.set("extensions", [])
- }
- return extensionsConfig
-}
+import { projectConf, extensionsConf } from "./config"
async function uploadExtension(
archiveFileContent,
@@ -87,23 +54,23 @@ async function runExtensions(projectName, projectFolderPath) {
throw new Error("Invalid .vext file: missing metadata.json")
}
- const { name, version, backendExecutablePath, frontendFilePath } = metadata
+ const { name, version, backendExecutable, frontendFile } = metadata
- if (!frontendFilePath) {
+ if (!frontendFile) {
throw new Error("Invalid .vext file: missing frontend JavaScript")
}
- if (!backendExecutablePath) {
+ if (!backendExecutable) {
throw new Error("Invalid .vext file: missing backend executable")
}
- const frontendFileAbsolutePath = path.resolve(frontendFilePath)
+ const frontendFilePath = path.join(unzippedExtensionPath, frontendFile)
const frontendContent = await fs.promises.readFile(
- frontendFileAbsolutePath,
+ frontendFilePath,
"utf-8",
)
try {
const port = await getPort({ portRange: [MIN_PORT, MAX_PORT] })
- console.log(`[Electron] Extension ${name} will use port ${port}`)
+ console.log(`Extension ${name} will use port ${port}`)
extensionProcesses.set(name, { process, port })
process.on("error", (error) => {
From 45693d3e134ddc91c61f870d5b9c5d7de3385d54 Mon Sep 17 00:00:00 2001
From: JulienChampagnol
Date: Wed, 25 Feb 2026 11:08:44 +0100
Subject: [PATCH 13/28] save wip [skip ci]
---
app/utils/config.js | 45 +++---
app/utils/extension.js | 221 +++++++++++++-------------
app/utils/local.js | 40 +----
app/utils/server.js | 45 ++++++
nuxt.config.js | 6 +
package.json | 1 +
server/api/routes/extensions.put.js | 80 ++++++++++
server/api/routes/import_extension.js | 0
8 files changed, 270 insertions(+), 168 deletions(-)
create mode 100644 app/utils/server.js
create mode 100644 server/api/routes/extensions.put.js
create mode 100644 server/api/routes/import_extension.js
diff --git a/app/utils/config.js b/app/utils/config.js
index 1ab792ff..776988ee 100644
--- a/app/utils/config.js
+++ b/app/utils/config.js
@@ -1,4 +1,5 @@
// Node.js imports
+import path from "node:path"
// Third party imports
import Conf from "conf"
@@ -6,33 +7,37 @@ import _ from "lodash"
// Local imports
-const projectConfigSchema = {
- properties: {
- projectName: {
- type: "string",
- minLength: 1,
- },
- },
- additionalProperties: false,
- required: ["projectName"],
-}
-
-const extensionProcesses = new Map()
-
function projectConf(projectName) {
+ console.log("projectConf", { projectName })
const projectConfig = new Conf({ projectName })
- // schema: projectConfigSchema
console.log(projectConf.name, { projectConfig })
return projectConfig
}
-function extensionsConf(projectConfig) {
- const extensionsConfig = projectConfig.get("extensions")
- console.log(extensionsConf.name, { extensionsConfig })
- if (_.isEqual(extensionsConfig, undefined)) {
+function confFolderPath(projectName) {
+ console.log("confFolderPath", { projectName })
+ const projectConfig = projectConf(projectName)
+ console.log("confFolderPath", { projectConfig })
+ return path.dirname(projectConfig.path)
+}
+
+function extensionsConf(projectName) {
+ console.log("extensionsConf", { projectName })
+ const projectConfig = projectConf(projectName)
+ if (!projectConfig.has("extensions")) {
projectConfig.set("extensions", [])
}
- return extensionsConfig
+
+ const extensionsConfig = projectConfig.get("extensions")
+ console.log("extensionsConf", { extensionsConfig })
+ return { extensionsConfig, path: extensionsConfig }
+}
+
+function addExtensionToConf(projectName, extensionPath) {
+ const projectConfig = projectConf(projectName)
+ const { extensionsConfig } = extensionsConf(projectName)
+ extensionsConfig.push(extensionPath)
+ projectConfig.set("extensions", extensionsConfig)
}
-export { projectConf, extensionsConf, extensionProcesses }
+export { confFolderPath, projectConf, extensionsConf, addExtensionToConf }
diff --git a/app/utils/extension.js b/app/utils/extension.js
index a8982d58..99a47355 100644
--- a/app/utils/extension.js
+++ b/app/utils/extension.js
@@ -1,123 +1,126 @@
// Node.js imports
-import fs from "node:fs"
-import path from "node:path"
+// import fs from "node:fs"
+// import path from "node:path"
// Third party imports
-import { getPort } from "get-port-please"
+// import { getPort } from "get-port-please"
+import { $fetch } from "ofetch"
import _ from "lodash"
// Local imports
-import { unzipFile } from "./local.js"
-import { projectConf, extensionsConf } from "./config"
-
-async function uploadExtension(
- archiveFileContent,
- archiveFilename,
- projectName,
-) {
- const projectConfig = projectConf(projectName)
- const extensionsConfig = extensionsConf(projectConfig)
- const configFolderPath = path.dirname(projectConfig.path)
- const outputPath = path.join(configFolderPath, archiveFilename)
-
- await fs.promises.writeFile(outputPath, archiveFileContent)
-
- extensionsConfig.push(outputPath)
- projectConfig.set("extensions", extensionsConfig)
-}
-async function runExtensions(projectName, projectFolderPath) {
- const projectConfig = projectConf(projectName)
- console.log("runExtensions", { projectConfig })
- const extensionsConfig = extensionsConf(projectConfig)
- console.log("runExtensions", { extensionsConfig })
-
- if (_.isEqual(extensionsConfig, {})) {
- return Promise.resolve()
- }
-
- for (const archivePath of extensionsConfig) {
- console.log("runExtensions", { archivePath })
-
- const unzippedExtensionPath = await unzipFile(
- archivePath,
- projectFolderPath,
- )
-
- // Read and validate metadata.json
- const metadataPath = path.join(unzippedExtensionPath, "metadata.json")
- let metadata
- try {
- const metadataContent = await fs.promises.readFile(metadataPath, "utf-8")
- metadata = JSON.parse(metadataContent)
- } catch (error) {
- throw new Error("Invalid .vext file: missing metadata.json")
- }
-
- const { name, version, backendExecutable, frontendFile } = metadata
-
- if (!frontendFile) {
- throw new Error("Invalid .vext file: missing frontend JavaScript")
- }
- if (!backendExecutable) {
- throw new Error("Invalid .vext file: missing backend executable")
- }
- const frontendFilePath = path.join(unzippedExtensionPath, frontendFile)
- const frontendContent = await fs.promises.readFile(
- frontendFilePath,
- "utf-8",
- )
-
- try {
- const port = await getPort({ portRange: [MIN_PORT, MAX_PORT] })
- console.log(`Extension ${name} will use port ${port}`)
- extensionProcesses.set(name, { process, port })
-
- process.on("error", (error) => {
- console.error(`[${name}] Process error:`, error)
- })
-
- process.on("exit", (code) => {
- console.log(`[${name}] Process exited with code ${code}`)
- extensionProcesses.delete(name)
- })
-
- const test = {
- success: true,
- extension_metadata: { name, version },
- frontendContent,
- backendPort: port,
- }
- console.log("[Extensions] test return :", { test })
- return test
- } catch (error) {
- console.error(
- `[Extensions] Failed to launch extension ${extensionId}:`,
- error,
- )
- return { success: false, error: error.message }
- }
- }
-}
+const MIN_PORT = 5001
+const MAX_PORT = 5999
+const extensionProcesses = new Map()
+
+async function uploadExtension(archiveFileContent, archiveFilename) {
+ console.log("uploadExtension", {
+ archiveFileContent,
+ archiveFilename,
+ // projectName,
+ })
-function killExtensionMicroservices() {
- return [...extensionProcesses.values()].map(async ({ process, _port }) => {
- if (process && !process.killed) {
- process.kill()
- try {
- // Wait for exit or timeout
- await Promise.race([once(process, "exit"), setTimeout(KILL_TIMEOUT)])
- } catch {
- // Ignore errors during kill wait
- }
- }
+ const formData = new FormData()
+ formData.append("file", archiveFileContent)
+ formData.append("filename", archiveFilename)
+ // formData.append("projectName", projectName)
+ const response = await $fetch("/api/routes/extensions", {
+ method: "PUT",
+ body: formData,
})
+
+ console.log("uploadExtension", { response })
}
+// async function runExtensions(projectName, projectFolderPath) {
+// console.log("runExtensions", { projectName, projectFolderPath })
+
+// const extensionsConfig = extensionsConf(projectName)
+
+// console.log("runExtensions", { extensionsConfig })
+
+// if (_.isEqual(extensionsConfig, {})) {
+// return Promise.resolve()
+// }
+
+// for (const archivePath of extensionsConfig) {
+// console.log("runExtensions", { archivePath })
+
+// const unzippedExtensionPath = await unzipFile(
+// archivePath,
+// projectFolderPath,
+// )
+
+// // Read and validate metadata.json
+// const metadataPath = path.join(unzippedExtensionPath, "metadata.json")
+// let metadata
+// try {
+// const metadataContent = await fs.promises.readFile(metadataPath, "utf-8")
+// metadata = JSON.parse(metadataContent)
+// } catch (error) {
+// throw new Error("Invalid .vext file: missing metadata.json")
+// }
+
+// const { name, version, backendExecutable, frontendFile } = metadata
+
+// if (!frontendFile) {
+// throw new Error("Invalid .vext file: missing frontend JavaScript")
+// }
+// if (!backendExecutable) {
+// throw new Error("Invalid .vext file: missing backend executable")
+// }
+// const frontendFilePath = path.join(unzippedExtensionPath, frontendFile)
+// const frontendContent = await fs.promises.readFile(
+// frontendFilePath,
+// "utf-8",
+// )
+
+// try {
+// const port = await getPort({ portRange: [MIN_PORT, MAX_PORT] })
+// console.log(`Extension ${name} will use port ${port}`)
+// extensionProcesses.set(name, { process, port })
+
+// process.on("error", (error) => {
+// console.error(`[${name}] Process error:`, error)
+// })
+
+// process.on("exit", (code) => {
+// console.log(`[${name}] Process exited with code ${code}`)
+// extensionProcesses.delete(name)
+// })
+
+// const test = {
+// success: true,
+// extension_metadata: { name, version },
+// frontendContent,
+// backendPort: port,
+// }
+// console.log("[Extensions] test return :", { test })
+// return test
+// } catch (error) {
+// console.error(`[Extensions] Failed to launch extension ${name}:`, error)
+// return { success: false, error: error.message }
+// }
+// }
+// }
+
+// function killExtensionMicroservices() {
+// return [...extensionProcesses.values()].map(async ({ process, _port }) => {
+// if (process && !process.killed) {
+// process.kill()
+// try {
+// // Wait for exit or timeout
+// await Promise.race([once(process, "exit"), setTimeout(KILL_TIMEOUT)])
+// } catch {
+// // Ignore errors during kill wait
+// }
+// }
+// })
+// }
+
export {
- projectConf,
- extensionsConf,
uploadExtension,
- runExtensions,
- killExtensionMicroservices,
+ // runExtensions,
+ // killExtensionMicroservices,
+ extensionProcesses,
}
diff --git a/app/utils/local.js b/app/utils/local.js
index 5743fe1c..33efe598 100644
--- a/app/utils/local.js
+++ b/app/utils/local.js
@@ -4,7 +4,7 @@ import fs from "node:fs"
import path from "node:path"
// Third party imports
-import JSZip from "jszip"
+
import { WebSocket } from "ws"
import back_schemas from "@geode/opengeodeweb-back/opengeodeweb_back_schemas.json" with { type: "json" }
import { getPort } from "get-port-please"
@@ -346,43 +346,6 @@ async function run_browser(
return wait_nuxt(nuxt_process, back_port, viewer_port)
}
-async function unzipFile(
- zipFilePath,
- outputDir = zipFilePath.replace(/\.[^/.]+$/, ""), // Remove the file extension
-) {
- console.log("Unzipping file...", zipFilePath, outputDir)
- try {
- const data = await fs.promises.readFile(zipFilePath)
- const zip = await JSZip.loadAsync(data)
- await fs.promises.mkdir(outputDir, { recursive: true })
- const promises = []
-
- zip.forEach((relativePath, zipEntry) => {
- const outputPath = path.join(outputDir, relativePath)
-
- if (zipEntry.dir) {
- promises.push(fs.promises.mkdir(outputPath, { recursive: true }))
- } else {
- promises.push(
- zipEntry.async("nodebuffer").then(async (content) => {
- await fs.promises.mkdir(path.dirname(outputPath), {
- recursive: true,
- })
- await fs.promises.writeFile(outputPath, content)
- }),
- )
- }
- })
-
- await Promise.all(promises)
- console.log("Extraction complete!")
- return outputDir
- } catch (error) {
- console.error("Error unzipping file:", error)
- throw error
- }
-}
-
export {
create_path,
delete_folder_recursive,
@@ -395,5 +358,4 @@ export {
run_back,
run_viewer,
run_browser,
- unzipFile,
}
diff --git a/app/utils/server.js b/app/utils/server.js
new file mode 100644
index 00000000..122340aa
--- /dev/null
+++ b/app/utils/server.js
@@ -0,0 +1,45 @@
+// Node imports
+import fs from "node:fs"
+import path from "node:path"
+
+// Third party imports
+import JSZip from "jszip"
+
+async function unzipFile(
+ zipFilePath,
+ outputDir = zipFilePath.replace(/\.[^/.]+$/, ""), // Remove the file extension
+) {
+ console.log("Unzipping file...", zipFilePath, outputDir)
+ try {
+ const data = await fs.promises.readFile(zipFilePath)
+ const zip = await JSZip.loadAsync(data)
+ await fs.promises.mkdir(outputDir, { recursive: true })
+ const promises = []
+
+ zip.forEach((relativePath, zipEntry) => {
+ const outputPath = path.join(outputDir, relativePath)
+
+ if (zipEntry.dir) {
+ promises.push(fs.promises.mkdir(outputPath, { recursive: true }))
+ } else {
+ promises.push(
+ zipEntry.async("nodebuffer").then(async (content) => {
+ await fs.promises.mkdir(path.dirname(outputPath), {
+ recursive: true,
+ })
+ await fs.promises.writeFile(outputPath, content)
+ }),
+ )
+ }
+ })
+
+ await Promise.all(promises)
+ console.log("Extraction complete!")
+ return outputDir
+ } catch (error) {
+ console.error("Error unzipping file:", error)
+ throw error
+ }
+}
+
+export { unzipFile }
diff --git a/nuxt.config.js b/nuxt.config.js
index c9ef0257..7b41bfa3 100644
--- a/nuxt.config.js
+++ b/nuxt.config.js
@@ -51,4 +51,10 @@ export default defineNuxtConfig({
],
},
},
+
+ nitro: {
+ routeRules: {
+ "/api/routes/extensions": { bodySize: 100 * 1024 * 1024 }, // 100MB
+ },
+ },
})
diff --git a/package.json b/package.json
index a7465cf7..242339b6 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"@vueuse/nuxt": "13.1.0",
"@vueuse/rxjs": "^14.1.0",
"ajv": "8.17.1",
+ "busboy": "1.6.0",
"conf": "15.1.0",
"dexie": "4.2.1",
"get-port-please": "3.2.0",
diff --git a/server/api/routes/extensions.put.js b/server/api/routes/extensions.put.js
new file mode 100644
index 00000000..02557a30
--- /dev/null
+++ b/server/api/routes/extensions.put.js
@@ -0,0 +1,80 @@
+// Node imports
+import fs from "node:fs"
+import path from "node:path"
+import { Readable } from "node:stream"
+
+// Third party imports
+import busboy from "busboy"
+import { defineEventHandler, getRequestHeaders, getRequestWebStream } from "h3"
+
+// Local imports
+import {
+ confFolderPath,
+ addExtensionToConf,
+} from "../../../app/utils/config.js"
+// import { useRuntimeConfig } from "#imports"
+
+const CODE_201 = 201
+
+export default defineEventHandler(async (event) => {
+ console.log("[TIME] defineEventHandler", Date.now())
+ // const config = useRuntimeConfig(event).public
+ // const { APP_NAME } = config
+ // console.log("defineEventHandler", { APP_NAME })
+ const projectName = "vease"
+
+ const configFolderPath = confFolderPath(projectName)
+ // const params = await readMultipartFormData(event)
+ console.log("[TIME] readMultipartFormData", Date.now())
+
+ const headers = getRequestHeaders(event)
+
+ await new Promise((resolve, reject) => {
+ const bb = busboy({ headers })
+
+ bb.on("file", (name, stream, info) => {
+ const outputPath = path.join(confFolderPath("vease"), info.filename)
+ stream.pipe(fs.createWriteStream(outputPath))
+ })
+
+ bb.on("close", resolve)
+ bb.on("error", reject)
+
+ const rawStream = getRequestWebStream(event)
+ // pipe web stream to busboy
+ Readable.fromWeb(rawStream).pipe(bb)
+ })
+
+ // const fileData = params.find((item) => item.name === "fileContent")
+ // const nameData = params.find((item) => item.name === "filename")
+
+ // const fileContent = fileData?.data
+ // const filename = nameData?.data.toString() // Convert Buffer to string
+
+ // const fileContent = params.find((item) => {
+ // if (item.name === "fileContent") {
+ // return item.data
+ // }
+ // })
+ // const filename = params.find((item) => {
+ // if (item.name === "filename") {
+ // return item.data.toString()
+ // }
+ // })
+ // console.log("filename", { filename })
+ // console.log("defineEventHandler", {
+ // projectName,
+ // fileContent,
+ // filename,
+ // })
+ const outputPath = path.join(configFolderPath, filename)
+ console.log("[TIME] fs.promises.writeFile", Date.now())
+
+ await fs.promises.writeFile(outputPath, fileContent)
+ console.log("[TIME] fs.awaitpromises.writeFile", Date.now())
+
+ await addExtensionToConf(projectName, outputPath)
+ console.log("[TIME] addExtensionToConf", Date.now())
+
+ return { statusCode: CODE_201 }
+})
diff --git a/server/api/routes/import_extension.js b/server/api/routes/import_extension.js
new file mode 100644
index 00000000..e69de29b
From 93a685096f04c67afb6f11a78a9a638f4c1c5dd7 Mon Sep 17 00:00:00 2001
From: JulienChampagnol
Date: Thu, 26 Feb 2026 15:56:33 +0100
Subject: [PATCH 14/28] wip launch microservices [skip ci]
---
app/components/FileUploader.vue | 6 +-
app/stores/app.js | 50 ++++++
app/stores/geode.js | 32 +++-
app/stores/infra.js | 16 +-
app/stores/lambda.js | 3 +-
app/utils/extension.js | 148 +++++----------
app/utils/local.js | 168 +++++++++---------
{app => internal}/utils/upload_file.js | 13 +-
nuxt.config.js | 18 +-
server/api/routes/extensions.put.js | 80 ---------
server/api/routes/extensions/import.post.js | 92 ++++++++++
server/api/routes/extensions/index.put.js | 98 ++++++++++
server/api/routes/import_extension.js | 0
server/api/routes/microservices/index.get.js | 20 +++
.../microservices/run_back.post copy.js | 12 ++
.../api/routes/microservices/run_back.post.js | 10 ++
tests/integration/setup.js | 39 +---
tests/unit/stores/Lambda.nuxt.test.js | 4 +-
18 files changed, 482 insertions(+), 327 deletions(-)
rename {app => internal}/utils/upload_file.js (82%)
delete mode 100644 server/api/routes/extensions.put.js
create mode 100644 server/api/routes/extensions/import.post.js
create mode 100644 server/api/routes/extensions/index.put.js
delete mode 100644 server/api/routes/import_extension.js
create mode 100644 server/api/routes/microservices/index.get.js
create mode 100644 server/api/routes/microservices/run_back.post copy.js
create mode 100644 server/api/routes/microservices/run_back.post.js
diff --git a/app/components/FileUploader.vue b/app/components/FileUploader.vue
index b36ae79d..4fd089b3 100644
--- a/app/components/FileUploader.vue
+++ b/app/components/FileUploader.vue
@@ -1,7 +1,7 @@