@geode/opengeodeweb-front 10.36.0-rc.3 → 10.36.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -181,7 +181,8 @@ var api = {
181
181
  "name": { "type": "string" },
182
182
  "version": { "type": "string" },
183
183
  "frontendContent": { "type": "string" },
184
- "port": { "type": "integer" }
184
+ "port": { "type": "integer" },
185
+ "serverPort": { "type": "integer" }
185
186
  },
186
187
  "required": [
187
188
  "id",
@@ -9,9 +9,10 @@ import child_process from "node:child_process";
9
9
  import back_schemas from "@geode/opengeodeweb-back/opengeodeweb_back_schemas.json" with { type: "json" };
10
10
  //#region server/utils/microservices.ts
11
11
  const MILLISECONDS_PER_SECOND = 1e3;
12
- const DEFAULT_TIMEOUT_SECONDS = 45;
12
+ const DEFAULT_TIMEOUT_SECONDS = 120;
13
13
  const MAX_PORT_RETRIES = 1;
14
14
  const DEFAULT_RUN_ARGS = { projectFolderPath: "" };
15
+ const LISTENING_LOG_PATTERN = /^Listening on https?:\/\/.+:\d+\/?\s*$/mu;
15
16
  async function runScript(execPath, execName, args, expectedResponse, timeoutSeconds = DEFAULT_TIMEOUT_SECONDS) {
16
17
  const command = executablePath(execPath, execName);
17
18
  consola.debug("runScript", command, args);
@@ -132,6 +133,47 @@ async function runExtension(extensionId, execName, execPath, args = DEFAULT_RUN_
132
133
  }
133
134
  throw new Error(`runExtension failed to run ${extensionId}`);
134
135
  }
136
+ async function waitUntilListening(child) {
137
+ child.stderr.on("data", (data) => {
138
+ consola.info(`[${child.spawnfile}] STDERR:`, data.toString().trim());
139
+ });
140
+ child.on("close", (code) => {
141
+ consola.info(`[${child.spawnfile}] exited with code ${code}`);
142
+ });
143
+ for await (const chunk of child.stdout) {
144
+ const output = String(chunk);
145
+ consola.info(`[${child.spawnfile}] STDOUT:`, output.trim());
146
+ if (LISTENING_LOG_PATTERN.test(output)) {
147
+ child.stdout.on("data", (data) => {
148
+ consola.info(`[${child.spawnfile}] STDOUT:`, data.toString().trim());
149
+ });
150
+ return;
151
+ }
152
+ }
153
+ throw new Error(`${child.spawnfile} exited before becoming ready`);
154
+ }
155
+ async function runExtensionServer(entryPath, attempts = 0) {
156
+ let port = void 0;
157
+ try {
158
+ port = await getAvailablePort();
159
+ consola.info("runExtensionServer", entryPath, port);
160
+ await waitUntilListening(child_process.spawn("node", [entryPath], { env: {
161
+ ...process.env,
162
+ PORT: String(port)
163
+ } }));
164
+ return port;
165
+ } catch (error) {
166
+ if (!isPortInUseError(error)) {
167
+ consola.error("runExtensionServer error", error);
168
+ throw error;
169
+ }
170
+ if (attempts <= MAX_PORT_RETRIES) {
171
+ consola.warn("Retrying runExtensionServer on conflicting port", port);
172
+ return await runExtensionServer(entryPath, attempts + 1);
173
+ }
174
+ }
175
+ throw new Error(`runExtensionServer failed to run ${entryPath}`);
176
+ }
135
177
  function addMicroserviceMetadatas(projectFolderPath, serviceObj) {
136
178
  const microservices = projectMicroservices(projectFolderPath);
137
179
  let enriched = { ...serviceObj };
@@ -151,4 +193,4 @@ function addMicroserviceMetadatas(projectFolderPath, serviceObj) {
151
193
  fs.writeFileSync(microservicesMetadatasPath(projectFolderPath), JSON.stringify({ microservices }, void 0, 2));
152
194
  }
153
195
  //#endregion
154
- export { addMicroserviceMetadatas, runBack, runExtension, runViewer };
196
+ export { addMicroserviceMetadatas, runBack, runExtension, runExtensionServer, runViewer };
@@ -105,5 +105,8 @@ function extensionBackendPath(unzippedExtensionPath, backendExecutableName) {
105
105
  consola.debug("runExtensions", { backendExecutablePath });
106
106
  return backendExecutablePath;
107
107
  }
108
+ function extensionServerEntryPath(unzippedExtensionPath, entry) {
109
+ return path.join(unzippedExtensionPath, entry);
110
+ }
108
111
  //#endregion
109
- export { createPath, executableName, executablePath, extensionBackendPath, extensionFolderPath, extensionFrontendPath, generateProjectFolderPath };
112
+ export { createPath, executableName, executablePath, extensionBackendPath, extensionFolderPath, extensionFrontendPath, extensionServerEntryPath, generateProjectFolderPath };
@@ -3,6 +3,7 @@ import { createServerWsRpcClient } from "./ws_client.js";
3
3
  const stringStorage = /* @__PURE__ */ new Map();
4
4
  const booleanStorage = /* @__PURE__ */ new Map();
5
5
  const clientStorage = /* @__PURE__ */ new Map();
6
+ const extensionServerPorts = /* @__PURE__ */ new Map();
6
7
  function getAppBaseUrl() {
7
8
  const value = stringStorage.get("APP_BASE_URL");
8
9
  if (value === void 0) throw new Error("APP_BASE_URL is not set");
@@ -47,5 +48,14 @@ async function getViewerWebSocketClient() {
47
48
  if (viewerClient !== void 0 && viewerClient.isOpen()) return viewerClient;
48
49
  return await setViewerWebSocketClient(getViewerBaseUrl());
49
50
  }
51
+ function setExtensionServerPort(extensionId, port) {
52
+ extensionServerPorts.set(extensionId, port);
53
+ }
54
+ function getExtensionServerPort(extensionId) {
55
+ return extensionServerPorts.get(extensionId);
56
+ }
57
+ function getExtensionServerPorts() {
58
+ return extensionServerPorts;
59
+ }
50
60
  //#endregion
51
- export { getAppBaseUrl, getBackBaseUrl, getIsAppReady, getViewerBaseUrl, getViewerWebSocketClient, setAppBaseUrl, setBackBaseUrl, setIsAppReady, setViewerBaseUrl, setViewerWebSocketClient };
61
+ export { getAppBaseUrl, getBackBaseUrl, getExtensionServerPort, getExtensionServerPorts, getIsAppReady, getViewerBaseUrl, getViewerWebSocketClient, setAppBaseUrl, setBackBaseUrl, setExtensionServerPort, setIsAppReady, setViewerBaseUrl, setViewerWebSocketClient };
@@ -59,6 +59,7 @@ export function useAppExtensions(): {
59
59
  async function loadExtension(
60
60
  path: string,
61
61
  extensionPort: string,
62
+ serverPort?: string,
62
63
  backendPath?: string,
63
64
  ): Promise<ExtensionModule> {
64
65
  try {
@@ -77,7 +78,10 @@ export function useAppExtensions(): {
77
78
  // oxlint-disable-next-line no-inline-comments, no-unsafe-assignment
78
79
  const extensionModule: ExtensionModule = await import(/* @vite-ignore */ finalURL);
79
80
  const store = extensionModule.metadata.store();
80
- store.$patch?.({ default_local_port: extensionPort });
81
+ store.$patch?.({
82
+ default_local_port: extensionPort,
83
+ ...(serverPort === undefined ? {} : { nitro_port: serverPort }),
84
+ });
81
85
 
82
86
  if (finalURL !== path && finalURL.startsWith("blob:")) {
83
87
  URL.revokeObjectURL(finalURL);
@@ -15,6 +15,7 @@ interface ExtensionDescriptor {
15
15
  version: string;
16
16
  frontendContent: string;
17
17
  port: number;
18
+ serverPort?: number;
18
19
  }
19
20
 
20
21
  interface DownloadExtensionParams {
@@ -87,12 +88,16 @@ async function registerRunningExtensions(): Promise<RegisteredExtension[]> {
87
88
  const { extensionsArray } = await runExtensions();
88
89
  return Promise.all(
89
90
  extensionsArray.map(async (extension: ExtensionDescriptor) => {
90
- const { id, name, version, frontendContent, port } = extension;
91
+ const { id, name, version, frontendContent, port, serverPort } = extension;
91
92
  const blob = new Blob([frontendContent], {
92
93
  type: "application/javascript",
93
94
  });
94
95
  const blobUrl = URL.createObjectURL(blob);
95
- const extensionModule = await appStore.loadExtension(blobUrl, String(port));
96
+ const extensionModule = await appStore.loadExtension(
97
+ blobUrl,
98
+ String(port),
99
+ serverPort === undefined ? undefined : String(serverPort),
100
+ );
96
101
  consola.info("[ExtensionManager] Extension loaded:", id);
97
102
  const storeFactory = extensionModule.metadata.store;
98
103
  const store = storeFactory();
@@ -80,6 +80,9 @@ async function importFile(filename: string, geode_object_type: string): Promise<
80
80
  }
81
81
 
82
82
  async function importWorkflow(files: readonly FileToImport[]): Promise<string[]> {
83
+ const hybridViewerStore = useHybridViewerStore();
84
+ // Files are imported concurrently and the viewer only frames the first actor to arrive, so the camera is reset once everything is loaded
85
+ const wasSceneEmpty = Object.keys(hybridViewerStore.hybridDb).length === 0;
83
86
  const chunk_size = 5;
84
87
  const chunks: FileToImport[][] = [];
85
88
  for (let i = 0; i < files.length; i += chunk_size) {
@@ -102,7 +105,10 @@ async function importWorkflow(files: readonly FileToImport[]): Promise<string[]>
102
105
  await processChunk(chunkIndex + 1);
103
106
  }
104
107
  await processChunk(0);
105
- const hybridViewerStore = useHybridViewerStore();
108
+ const isSceneFilled = Object.keys(hybridViewerStore.hybridDb).length > 0;
109
+ if (wasSceneEmpty && isSceneFilled) {
110
+ hybridViewerStore.resetCamera();
111
+ }
106
112
  await hybridViewerStore.remoteRender();
107
113
  return results;
108
114
  }
@@ -241,6 +241,9 @@
241
241
  },
242
242
  "port": {
243
243
  "type": "integer"
244
+ },
245
+ "serverPort": {
246
+ "type": "integer"
244
247
  }
245
248
  },
246
249
  "required": ["id", "name", "version", "frontendContent", "port"],
@@ -90,6 +90,7 @@ export interface LocalExtensionsRunResponseExtensionsArray {
90
90
  id: string;
91
91
  name: string;
92
92
  port: number;
93
+ serverPort?: number;
93
94
  version: string;
94
95
  }
95
96
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geode/opengeodeweb-front",
3
- "version": "10.36.0-rc.3",
3
+ "version": "10.36.0-rc.4",
4
4
  "description": "OpenSource Vue/Nuxt/Pinia/Vuetify framework for web applications",
5
5
  "homepage": "https://github.com/Geode-solutions/OpenGeodeWeb-Front",
6
6
  "bugs": {
@@ -7,54 +7,73 @@ import fs from "node:fs";
7
7
  import {
8
8
  addMicroserviceMetadatas,
9
9
  runBack,
10
+ runExtensionServer,
10
11
  } from "@geode/opengeodeweb-front/server/utils/microservices.ts";
11
12
  import {
12
13
  extensionBackendPath,
13
14
  extensionFolderPath,
15
+ extensionServerEntryPath,
14
16
  } from "@geode/opengeodeweb-front/server/utils/path.ts";
15
17
  import {
16
18
  readExtensionFrontend,
17
19
  readExtensionMetadata,
18
20
  } from "@geode/opengeodeweb-front/server/utils/extension.ts";
19
21
  import { extensionsConf } from "@geode/opengeodeweb-front/server/utils/app_config.ts";
22
+ import { setExtensionServerPort } from "@geode/opengeodeweb-front/server/utils/server_config.ts";
20
23
  import { unzipFile } from "@geode/opengeodeweb-front/server/utils/server.ts";
21
24
 
22
25
  import { defineTypedEventHandler } from "@geode/opengeodeweb-front/server/utils/typed_handler.ts";
23
26
  import schemas from "@geode/opengeodeweb-front/opengeodeweb_front_typed_schemas.js";
24
27
 
28
+ async function runSingleExtension(
29
+ extensionPath: string,
30
+ extensionId: string,
31
+ projectFolderPath: string,
32
+ ): Promise<{
33
+ id: string;
34
+ name: string;
35
+ version: string;
36
+ frontendContent: string;
37
+ port: number;
38
+ serverPort: number | undefined;
39
+ }> {
40
+ const unzippedExtensionPath = await unzipFile(
41
+ extensionPath,
42
+ extensionFolderPath(projectFolderPath, extensionId),
43
+ );
44
+ const { id, name, version, backendExecutable, server, frontendFile } =
45
+ await readExtensionMetadata(unzippedExtensionPath);
46
+ const frontendContent = await readExtensionFrontend(unzippedExtensionPath, frontendFile, id);
47
+
48
+ fs.chmodSync(extensionBackendPath(unzippedExtensionPath, backendExecutable), "755");
49
+ const port = await runBack(backendExecutable, unzippedExtensionPath, {
50
+ projectFolderPath,
51
+ });
52
+ addMicroserviceMetadatas(projectFolderPath, {
53
+ type: "back",
54
+ name,
55
+ port,
56
+ });
57
+
58
+ const serverPort =
59
+ server === undefined
60
+ ? undefined
61
+ : await runExtensionServer(extensionServerEntryPath(unzippedExtensionPath, server.entry));
62
+ if (serverPort !== undefined) {
63
+ setExtensionServerPort(id, serverPort);
64
+ }
65
+
66
+ return { id, name, version, frontendContent, port, serverPort };
67
+ }
68
+
25
69
  export default defineTypedEventHandler(
26
70
  schemas.api.local.extensions.run,
27
71
  async ({ projectFolderPath, projectName }) => {
28
72
  const extensionsConfig = extensionsConf(projectName);
29
73
  const extensionsArray = await Promise.all(
30
74
  Object.entries(extensionsConfig).map(async ([extensionId, { path: extensionPath }]) => {
31
- const unzippedExtensionPath = await unzipFile(
32
- extensionPath,
33
- extensionFolderPath(projectFolderPath, extensionId),
34
- );
35
- const { id, name, version, backendExecutable, frontendFile } =
36
- await readExtensionMetadata(unzippedExtensionPath);
37
- const frontendContent = await readExtensionFrontend(
38
- unzippedExtensionPath,
39
- frontendFile,
40
- id,
41
- );
42
- fs.chmodSync(extensionBackendPath(unzippedExtensionPath, backendExecutable), "755");
43
- const port = await runBack(backendExecutable, unzippedExtensionPath, {
44
- projectFolderPath,
45
- });
46
- addMicroserviceMetadatas(projectFolderPath, {
47
- type: "back",
48
- name,
49
- port,
50
- });
51
- return {
52
- id,
53
- name,
54
- version,
55
- frontendContent,
56
- port,
57
- };
75
+ const result = await runSingleExtension(extensionPath, extensionId, projectFolderPath);
76
+ return result;
58
77
  }),
59
78
  );
60
79
 
@@ -37,6 +37,9 @@
37
37
  },
38
38
  "port": {
39
39
  "type": "integer"
40
+ },
41
+ "serverPort": {
42
+ "type": "integer"
40
43
  }
41
44
  },
42
45
  "required": ["id", "name", "version", "frontendContent", "port"],
@@ -10,6 +10,10 @@ import { createError } from "h3";
10
10
  // Local imports
11
11
  import { extensionFrontendPath } from "@geode/opengeodeweb-front/server/utils/path.ts";
12
12
 
13
+ const extensionServerMetadataSchema = z.object({
14
+ entry: z.string(),
15
+ });
16
+
13
17
  const extensionMetadataSchema = z
14
18
  .object({
15
19
  id: z.string(),
@@ -17,6 +21,7 @@ const extensionMetadataSchema = z
17
21
  version: z.string(),
18
22
  frontendFile: z.string(),
19
23
  backendExecutable: z.string(),
24
+ server: extensionServerMetadataSchema.optional(),
20
25
  })
21
26
  .loose();
22
27
 
@@ -20,9 +20,10 @@ interface RunArgs {
20
20
  }
21
21
 
22
22
  const MILLISECONDS_PER_SECOND = 1000;
23
- const DEFAULT_TIMEOUT_SECONDS = 45;
23
+ const DEFAULT_TIMEOUT_SECONDS = 120;
24
24
  const MAX_PORT_RETRIES = 1;
25
25
  const DEFAULT_RUN_ARGS: RunArgs = { projectFolderPath: "" };
26
+ const LISTENING_LOG_PATTERN = /^Listening on https?:\/\/.+:\d+\/?\s*$/mu;
26
27
 
27
28
  async function runScript(
28
29
  execPath: string,
@@ -183,6 +184,56 @@ async function runExtension(
183
184
  }
184
185
  throw new Error(`runExtension failed to run ${extensionId}`);
185
186
  }
187
+ async function waitUntilListening(
188
+ child: child_process.ChildProcessWithoutNullStreams,
189
+ ): Promise<void> {
190
+ child.stderr.on("data", (data: Buffer) => {
191
+ consola.info(`[${child.spawnfile}] STDERR:`, data.toString().trim());
192
+ });
193
+ child.on("close", (code) => {
194
+ consola.info(`[${child.spawnfile}] exited with code ${code}`);
195
+ });
196
+ // oxlint-disable-next-line typescript/no-unnecessary-condition
197
+ for await (const chunk of child.stdout) {
198
+ const output = String(chunk);
199
+ consola.info(`[${child.spawnfile}] STDOUT:`, output.trim());
200
+ if (LISTENING_LOG_PATTERN.test(output)) {
201
+ child.stdout.on("data", (data: Buffer) => {
202
+ consola.info(`[${child.spawnfile}] STDOUT:`, data.toString().trim());
203
+ });
204
+ return;
205
+ }
206
+ }
207
+ throw new Error(`${child.spawnfile} exited before becoming ready`);
208
+ }
209
+
210
+ async function runExtensionServer(entryPath: string, attempts = 0): Promise<number> {
211
+ let port: number | undefined = undefined;
212
+ try {
213
+ port = await getAvailablePort();
214
+ consola.info("runExtensionServer", entryPath, port);
215
+ const child = child_process.spawn("node", [entryPath], {
216
+ env: {
217
+ ...process.env,
218
+ PORT: String(port),
219
+ },
220
+ });
221
+ await waitUntilListening(child);
222
+ return port;
223
+ } catch (error) {
224
+ if (!isPortInUseError(error)) {
225
+ consola.error("runExtensionServer error", error);
226
+ throw error;
227
+ }
228
+ if (attempts <= MAX_PORT_RETRIES) {
229
+ consola.warn("Retrying runExtensionServer on conflicting port", port);
230
+ const newPort = await runExtensionServer(entryPath, attempts + 1);
231
+ return newPort;
232
+ }
233
+ }
234
+ throw new Error(`runExtensionServer failed to run ${entryPath}`);
235
+ }
236
+
186
237
  function addMicroserviceMetadatas(projectFolderPath: string, serviceObj: Microservice): void {
187
238
  const microservices = projectMicroservices(projectFolderPath);
188
239
  let enriched: Microservice = { ...serviceObj };
@@ -213,4 +264,4 @@ function addMicroserviceMetadatas(projectFolderPath: string, serviceObj: Microse
213
264
  );
214
265
  }
215
266
 
216
- export { addMicroserviceMetadatas, runBack, runExtension, runViewer };
267
+ export { addMicroserviceMetadatas, runBack, runExtension, runExtensionServer, runViewer };
@@ -163,11 +163,15 @@ function extensionBackendPath(
163
163
  });
164
164
  return backendExecutablePath;
165
165
  }
166
+ function extensionServerEntryPath(unzippedExtensionPath: string, entry: string): string {
167
+ return path.join(unzippedExtensionPath, entry);
168
+ }
166
169
  export {
167
170
  createPath,
168
171
  extensionBackendPath,
169
172
  extensionFrontendPath,
170
173
  extensionFolderPath,
174
+ extensionServerEntryPath,
171
175
  executablePath,
172
176
  executableName,
173
177
  generateProjectFolderPath,
@@ -3,6 +3,7 @@ import { type ServerWsRpcClient, createServerWsRpcClient } from "./ws_client.js"
3
3
  const stringStorage = new Map<string, string>();
4
4
  const booleanStorage = new Map<string, boolean>();
5
5
  const clientStorage = new Map<string, ServerWsRpcClient>();
6
+ const extensionServerPorts = new Map<string, number>();
6
7
 
7
8
  function getAppBaseUrl(): string {
8
9
  const value = stringStorage.get("APP_BASE_URL");
@@ -60,14 +61,27 @@ async function getViewerWebSocketClient(): Promise<ServerWsRpcClient> {
60
61
  const client = await setViewerWebSocketClient(viewerBaseUrl);
61
62
  return client;
62
63
  }
64
+ function setExtensionServerPort(extensionId: string, port: number): void {
65
+ extensionServerPorts.set(extensionId, port);
66
+ }
67
+ function getExtensionServerPort(extensionId: string): number | undefined {
68
+ return extensionServerPorts.get(extensionId);
69
+ }
70
+ function getExtensionServerPorts(): ReadonlyMap<string, number> {
71
+ return extensionServerPorts;
72
+ }
73
+
63
74
  export {
64
75
  getAppBaseUrl,
65
76
  getBackBaseUrl,
77
+ getExtensionServerPort,
78
+ getExtensionServerPorts,
66
79
  getIsAppReady,
67
80
  getViewerBaseUrl,
68
81
  getViewerWebSocketClient,
69
82
  setAppBaseUrl,
70
83
  setBackBaseUrl,
84
+ setExtensionServerPort,
71
85
  setIsAppReady,
72
86
  setViewerBaseUrl,
73
87
  setViewerWebSocketClient,