@gxp-dev/tools 2.0.87 → 2.0.88

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.
package/bin/lib/cli.js CHANGED
@@ -157,6 +157,12 @@ const cli = yargs
157
157
  default: false,
158
158
  alias: "m",
159
159
  },
160
+ json: {
161
+ describe:
162
+ "Emit all dev-server logs as newline-delimited JSON (one record per line) for cloud log collectors",
163
+ type: "boolean",
164
+ default: false,
165
+ },
160
166
  },
161
167
  devCommand,
162
168
  )
@@ -7,6 +7,7 @@
7
7
 
8
8
  const path = require("path")
9
9
  const fs = require("fs")
10
+ const { spawn } = require("child_process")
10
11
  const shell = require("shelljs")
11
12
  const dotenv = require("dotenv")
12
13
  const {
@@ -16,6 +17,123 @@ const {
16
17
  findExistingCertificates,
17
18
  } = require("../utils")
18
19
 
20
+ const ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]/g
21
+
22
+ /**
23
+ * Create a logger that either emits NDJSON lines or plain text.
24
+ * In JSON mode, every record is a single line: {timestamp, service, level, message}
25
+ */
26
+ function createLogger(jsonMode) {
27
+ function emit(level, service, message) {
28
+ if (jsonMode) {
29
+ process.stdout.write(
30
+ JSON.stringify({
31
+ timestamp: new Date().toISOString(),
32
+ service,
33
+ level,
34
+ message,
35
+ }) + "\n",
36
+ )
37
+ return
38
+ }
39
+ const stream =
40
+ level === "error" || level === "warn" ? process.stderr : process.stdout
41
+ stream.write(message + "\n")
42
+ }
43
+ return {
44
+ jsonMode,
45
+ info: (message, service = "GXDEV") => emit("info", service, message),
46
+ warn: (message, service = "GXDEV") => emit("warn", service, message),
47
+ error: (message, service = "GXDEV") => emit("error", service, message),
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Spawn a service and pipe each line of its stdout/stderr through the logger.
53
+ * Returns the child process.
54
+ */
55
+ function spawnService(name, command, logger) {
56
+ const child = spawn(command, {
57
+ shell: true,
58
+ stdio: ["ignore", "pipe", "pipe"],
59
+ env: process.env,
60
+ })
61
+
62
+ function pipe(stream, level) {
63
+ let buffer = ""
64
+ stream.setEncoding("utf8")
65
+ stream.on("data", (chunk) => {
66
+ buffer += chunk
67
+ let idx
68
+ while ((idx = buffer.indexOf("\n")) !== -1) {
69
+ const line = buffer.slice(0, idx).replace(/\r$/, "")
70
+ buffer = buffer.slice(idx + 1)
71
+ const clean = line.replace(ANSI_REGEX, "")
72
+ if (clean.length > 0) {
73
+ logger[level](clean, name)
74
+ }
75
+ }
76
+ })
77
+ stream.on("end", () => {
78
+ if (buffer.length > 0) {
79
+ const clean = buffer.replace(ANSI_REGEX, "").trim()
80
+ if (clean) {
81
+ logger[level](clean, name)
82
+ }
83
+ buffer = ""
84
+ }
85
+ })
86
+ }
87
+
88
+ pipe(child.stdout, "info")
89
+ pipe(child.stderr, "error")
90
+ return child
91
+ }
92
+
93
+ /**
94
+ * Run a list of services concurrently, wiring up line-by-line JSON logging
95
+ * and best-effort shutdown when any one exits or the user hits Ctrl+C.
96
+ */
97
+ function runServicesJson(services, logger) {
98
+ const children = services.map((svc) => {
99
+ logger.info(`starting ${svc.name}: ${svc.command}`, svc.name)
100
+ return { svc, child: spawnService(svc.name, svc.command, logger) }
101
+ })
102
+
103
+ let shuttingDown = false
104
+ function shutdown(code) {
105
+ if (shuttingDown) {
106
+ return
107
+ }
108
+ shuttingDown = true
109
+ for (const { child } of children) {
110
+ if (!child.killed && child.exitCode === null) {
111
+ child.kill("SIGTERM")
112
+ }
113
+ }
114
+ process.exit(code ?? 0)
115
+ }
116
+
117
+ for (const { svc, child } of children) {
118
+ child.on("exit", (code, signal) => {
119
+ logger.info(
120
+ `${svc.name} exited (code=${code ?? "null"}${
121
+ signal ? `, signal=${signal}` : ""
122
+ })`,
123
+ svc.name,
124
+ )
125
+ shutdown(code ?? 0)
126
+ })
127
+ child.on("error", (err) => {
128
+ logger.error(`${svc.name} failed to spawn: ${err.message}`, svc.name)
129
+ shutdown(1)
130
+ })
131
+ }
132
+
133
+ process.on("SIGINT", () => shutdown(130))
134
+ process.on("SIGTERM", () => shutdown(143))
135
+ }
136
+
19
137
  /**
20
138
  * Get browser extension paths and commands
21
139
  * @param {string} browser - "firefox" or "chrome"
@@ -96,6 +214,7 @@ function getBrowserExtensionConfig(browser, projectPath, paths, options = {}) {
96
214
  * Development command - starts the dev server
97
215
  */
98
216
  function devCommand(argv) {
217
+ const logger = createLogger(!!argv.json)
99
218
  const paths = resolveGxPaths()
100
219
  const projectPath = findProjectRoot()
101
220
 
@@ -105,13 +224,13 @@ function devCommand(argv) {
105
224
 
106
225
  // Load .env file into process.env
107
226
  if (fs.existsSync(envPath)) {
108
- console.log("📋 Loading environment variables from .env file")
227
+ logger.info("📋 Loading environment variables from .env file")
109
228
  dotenv.config({ path: envPath })
110
229
  } else if (fs.existsSync(envExamplePath)) {
111
- console.log(
230
+ logger.info(
112
231
  "💡 Tip: Create .env file from .env.example to customize your environment settings",
113
232
  )
114
- console.log(" cp .env.example .env")
233
+ logger.info(" cp .env.example .env")
115
234
  }
116
235
 
117
236
  // Check for SSL certificates unless explicitly disabled
@@ -124,32 +243,32 @@ function devCommand(argv) {
124
243
  const existingCerts = findExistingCertificates(certsDir)
125
244
 
126
245
  if (!existingCerts) {
127
- console.log(
246
+ logger.warn(
128
247
  "⚠ SSL certificates not found. Run 'npm run setup-ssl' to enable HTTPS",
129
248
  )
130
- console.log("🌐 Starting HTTP development server...")
249
+ logger.info("🌐 Starting HTTP development server...")
131
250
  useHttps = false
132
251
  } else {
133
- console.log("🔒 Starting HTTPS development server...")
134
- console.log(
252
+ logger.info("🔒 Starting HTTPS development server...")
253
+ logger.info(
135
254
  `📁 Using certificate: ${path.basename(existingCerts.certPath)}`,
136
255
  )
137
- console.log(`🔑 Using key: ${path.basename(existingCerts.keyPath)}`)
256
+ logger.info(`🔑 Using key: ${path.basename(existingCerts.keyPath)}`)
138
257
  certPath = existingCerts.certPath
139
258
  keyPath = existingCerts.keyPath
140
259
  }
141
260
  } else {
142
- console.log("🌐 Starting HTTP development server...")
261
+ logger.info("🌐 Starting HTTP development server...")
143
262
  }
144
263
 
145
264
  // Determine final port value (priority: CLI arg > .env > default)
146
265
  const finalPort = argv.port || process.env.NODE_PORT || 3000
147
- console.log(`🌐 Development server will start on port: ${finalPort}`)
266
+ logger.info(`🌐 Development server will start on port: ${finalPort}`)
148
267
 
149
268
  // Check if mock API should be enabled
150
269
  const withMock = argv["with-mock"]
151
270
  if (withMock) {
152
- console.log("🎭 Mock API will be enabled")
271
+ logger.info("🎭 Mock API will be enabled")
153
272
  }
154
273
 
155
274
  // Socket server starts by default unless --no-socket is passed
@@ -159,15 +278,15 @@ function devCommand(argv) {
159
278
  // Check for local server.js first, then runtime directory
160
279
  const serverJs = resolveFilePath("server.cjs", "", "runtime")
161
280
  if (!fs.existsSync(serverJs.path)) {
162
- console.warn("⚠ server.js not found. Skipping Socket.IO server.")
281
+ logger.warn("⚠ server.js not found. Skipping Socket.IO server.")
163
282
  } else {
164
283
  serverJsPath = serverJs.path
165
- console.log(
284
+ logger.info(
166
285
  `📡 Starting Socket.IO server with nodemon... (${
167
286
  serverJs.isLocal ? "local" : "package"
168
287
  } version)`,
169
288
  )
170
- console.log(`📁 Using: ${serverJsPath}`)
289
+ logger.info(`📁 Using: ${serverJsPath}`)
171
290
  }
172
291
  }
173
292
 
@@ -184,16 +303,16 @@ function devCommand(argv) {
184
303
  fs.existsSync(path.join(projectPath, "vite.extend.mjs"))
185
304
 
186
305
  if (hasLocalIndexHtml) {
187
- console.log("📁 Using local index.html")
306
+ logger.info("📁 Using local index.html")
188
307
  }
189
308
  if (hasLocalMainJs) {
190
- console.log("📁 Using local main.js")
309
+ logger.info("📁 Using local main.js")
191
310
  }
192
311
  if (hasLocalExtend) {
193
- console.log("🧩 Extending vite config from vite.extend.js")
312
+ logger.info("🧩 Extending vite config from vite.extend.js")
194
313
  }
195
314
  if (!hasLocalIndexHtml && !hasLocalMainJs && !hasLocalExtend) {
196
- console.log(
315
+ logger.info(
197
316
  "📦 Using runtime dev files (create vite.extend.js to customize)",
198
317
  )
199
318
  }
@@ -234,11 +353,11 @@ function devCommand(argv) {
234
353
  port: finalPort,
235
354
  })
236
355
  if (firefoxConfig) {
237
- console.log("🦊 Firefox extension will launch with dev server")
238
- console.log(`📁 Extension path: ${firefoxConfig.extensionPath}`)
239
- console.log(`🌐 Start URL: ${firefoxConfig.startUrl}`)
356
+ logger.info("🦊 Firefox extension will launch with dev server")
357
+ logger.info(`📁 Extension path: ${firefoxConfig.extensionPath}`)
358
+ logger.info(`🌐 Start URL: ${firefoxConfig.startUrl}`)
240
359
  } else {
241
- console.warn("⚠️ Firefox extension not found, skipping")
360
+ logger.warn("⚠️ Firefox extension not found, skipping")
242
361
  }
243
362
  }
244
363
 
@@ -248,11 +367,11 @@ function devCommand(argv) {
248
367
  port: finalPort,
249
368
  })
250
369
  if (chromeConfig) {
251
- console.log("🚀 Chrome extension will launch with dev server")
252
- console.log(`📁 Extension path: ${chromeConfig.extensionPath}`)
253
- console.log(`🌐 Start URL: ${chromeConfig.startUrl}`)
370
+ logger.info("🚀 Chrome extension will launch with dev server")
371
+ logger.info(`📁 Extension path: ${chromeConfig.extensionPath}`)
372
+ logger.info(`🌐 Start URL: ${chromeConfig.startUrl}`)
254
373
  } else {
255
- console.warn("⚠️ Chrome extension not found, skipping")
374
+ logger.warn("⚠️ Chrome extension not found, skipping")
256
375
  }
257
376
  }
258
377
 
@@ -261,54 +380,58 @@ function devCommand(argv) {
261
380
  process.env.CHROME_EXTENSION_PATH = chromeConfig.extensionPath
262
381
  }
263
382
 
264
- // Build the command based on what's requested
265
- let command
266
-
267
- // Collect all processes to run
268
- const processes = []
269
- const names = []
270
- const colors = []
271
-
272
383
  // Normalize path separators to forward slashes for cross-platform shell compatibility
273
384
  const normalizedViteConfigPath = viteConfigPath.replace(/\\/g, "/")
274
385
 
275
- // Vite is always included
276
- const viteCommand = `npx vite dev --config "${normalizedViteConfigPath}"`
277
- processes.push(`"${viteCommand}"`)
278
- names.push("VITE")
279
- colors.push("cyan")
386
+ // Build the canonical service list (raw commands, no concurrently wrapping)
387
+ const services = []
388
+ services.push({
389
+ name: "VITE",
390
+ color: "cyan",
391
+ command: `npx vite dev --config "${normalizedViteConfigPath}"`,
392
+ })
280
393
 
281
- // Socket server (on by default, skip if --no-socket or server.js not found)
282
394
  if (serverJsPath) {
283
395
  const normalizedServerPath = serverJsPath.replace(/\\/g, "/")
284
- processes.push(`"npx nodemon \\"${normalizedServerPath}\\""`)
285
- names.push("SOCKET")
286
- colors.push("green")
396
+ services.push({
397
+ name: "SOCKET",
398
+ color: "green",
399
+ command: `npx nodemon "${normalizedServerPath}"`,
400
+ })
287
401
  }
288
402
 
289
- // Firefox extension (optional)
290
403
  if (firefoxConfig) {
291
- processes.push(`"${firefoxConfig.command}"`)
292
- names.push(firefoxConfig.name)
293
- colors.push(firefoxConfig.color)
404
+ services.push({
405
+ name: firefoxConfig.name,
406
+ color: firefoxConfig.color,
407
+ command: firefoxConfig.command,
408
+ })
294
409
  }
295
410
 
296
- // Chrome extension (optional)
297
411
  if (chromeConfig) {
298
- processes.push(`"${chromeConfig.command}"`)
299
- names.push(chromeConfig.name)
300
- colors.push(chromeConfig.color)
412
+ services.push({
413
+ name: chromeConfig.name,
414
+ color: chromeConfig.color,
415
+ command: chromeConfig.command,
416
+ })
301
417
  }
302
418
 
303
- // Build the final command
304
- if (processes.length > 1) {
305
- // Use concurrently to run multiple processes
306
- command = `npx concurrently --names "${names.join(
307
- ",",
308
- )}" --prefix-colors "${colors.join(",")}" ${processes.join(" ")}`
419
+ // In JSON mode we orchestrate the children ourselves so we can wrap every
420
+ // stdout/stderr line as NDJSON. Concurrently's prefixed output would defeat
421
+ // that. Outside JSON mode, keep the legacy concurrently-based behavior.
422
+ if (logger.jsonMode) {
423
+ runServicesJson(services, logger)
424
+ return
425
+ }
426
+
427
+ let command
428
+ if (services.length > 1) {
429
+ const quoted = services.map((s) => `"${s.command}"`).join(" ")
430
+ const names = services.map((s) => s.name).join(",")
431
+ const colors = services.map((s) => s.color).join(",")
432
+ command = `npx concurrently --names "${names}" --prefix-colors "${colors}" ${quoted}`
309
433
  } else {
310
- // Just run Vite dev server alone
311
- command = `npx vite dev --config "${normalizedViteConfigPath}"`
434
+ command = services[0].command
312
435
  }
313
436
 
314
437
  shell.exec(command)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gxp-dev/tools",
3
- "version": "2.0.87",
3
+ "version": "2.0.88",
4
4
  "description": "Dev tools to create platform plugins",
5
5
  "type": "commonjs",
6
6
  "publishConfig": {