@gxp-dev/tools 2.0.87 → 2.0.89

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,22 +214,38 @@ 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
 
221
+ // Surface which toolkit install is being used. resolveGxPaths() prefers
222
+ // <project>/node_modules/@gxp-dev/tools (local) and falls back to the
223
+ // CLI's own install location (global / npm link / workspace).
224
+ const localToolkitDir = path.join(
225
+ projectPath,
226
+ "node_modules",
227
+ "@gxp-dev",
228
+ "tools",
229
+ )
230
+ const installLocation =
231
+ paths.packageRoot === localToolkitDir ? "local" : "package"
232
+ logger.info(
233
+ `📦 Using ${installLocation} toolkit install: ${paths.packageRoot}`,
234
+ )
235
+
102
236
  // Load .env file if it exists for default values
103
237
  const envPath = path.join(projectPath, ".env")
104
238
  const envExamplePath = path.join(projectPath, ".env.example")
105
239
 
106
240
  // Load .env file into process.env
107
241
  if (fs.existsSync(envPath)) {
108
- console.log("📋 Loading environment variables from .env file")
242
+ logger.info("📋 Loading environment variables from .env file")
109
243
  dotenv.config({ path: envPath })
110
244
  } else if (fs.existsSync(envExamplePath)) {
111
- console.log(
245
+ logger.info(
112
246
  "💡 Tip: Create .env file from .env.example to customize your environment settings",
113
247
  )
114
- console.log(" cp .env.example .env")
248
+ logger.info(" cp .env.example .env")
115
249
  }
116
250
 
117
251
  // Check for SSL certificates unless explicitly disabled
@@ -124,32 +258,32 @@ function devCommand(argv) {
124
258
  const existingCerts = findExistingCertificates(certsDir)
125
259
 
126
260
  if (!existingCerts) {
127
- console.log(
261
+ logger.warn(
128
262
  "⚠ SSL certificates not found. Run 'npm run setup-ssl' to enable HTTPS",
129
263
  )
130
- console.log("🌐 Starting HTTP development server...")
264
+ logger.info("🌐 Starting HTTP development server...")
131
265
  useHttps = false
132
266
  } else {
133
- console.log("🔒 Starting HTTPS development server...")
134
- console.log(
267
+ logger.info("🔒 Starting HTTPS development server...")
268
+ logger.info(
135
269
  `📁 Using certificate: ${path.basename(existingCerts.certPath)}`,
136
270
  )
137
- console.log(`🔑 Using key: ${path.basename(existingCerts.keyPath)}`)
271
+ logger.info(`🔑 Using key: ${path.basename(existingCerts.keyPath)}`)
138
272
  certPath = existingCerts.certPath
139
273
  keyPath = existingCerts.keyPath
140
274
  }
141
275
  } else {
142
- console.log("🌐 Starting HTTP development server...")
276
+ logger.info("🌐 Starting HTTP development server...")
143
277
  }
144
278
 
145
279
  // Determine final port value (priority: CLI arg > .env > default)
146
280
  const finalPort = argv.port || process.env.NODE_PORT || 3000
147
- console.log(`🌐 Development server will start on port: ${finalPort}`)
281
+ logger.info(`🌐 Development server will start on port: ${finalPort}`)
148
282
 
149
283
  // Check if mock API should be enabled
150
284
  const withMock = argv["with-mock"]
151
285
  if (withMock) {
152
- console.log("🎭 Mock API will be enabled")
286
+ logger.info("🎭 Mock API will be enabled")
153
287
  }
154
288
 
155
289
  // Socket server starts by default unless --no-socket is passed
@@ -159,15 +293,15 @@ function devCommand(argv) {
159
293
  // Check for local server.js first, then runtime directory
160
294
  const serverJs = resolveFilePath("server.cjs", "", "runtime")
161
295
  if (!fs.existsSync(serverJs.path)) {
162
- console.warn("⚠ server.js not found. Skipping Socket.IO server.")
296
+ logger.warn("⚠ server.js not found. Skipping Socket.IO server.")
163
297
  } else {
164
298
  serverJsPath = serverJs.path
165
- console.log(
299
+ logger.info(
166
300
  `📡 Starting Socket.IO server with nodemon... (${
167
301
  serverJs.isLocal ? "local" : "package"
168
302
  } version)`,
169
303
  )
170
- console.log(`📁 Using: ${serverJsPath}`)
304
+ logger.info(`📁 Using: ${serverJsPath}`)
171
305
  }
172
306
  }
173
307
 
@@ -184,16 +318,16 @@ function devCommand(argv) {
184
318
  fs.existsSync(path.join(projectPath, "vite.extend.mjs"))
185
319
 
186
320
  if (hasLocalIndexHtml) {
187
- console.log("📁 Using local index.html")
321
+ logger.info("📁 Using local index.html")
188
322
  }
189
323
  if (hasLocalMainJs) {
190
- console.log("📁 Using local main.js")
324
+ logger.info("📁 Using local main.js")
191
325
  }
192
326
  if (hasLocalExtend) {
193
- console.log("🧩 Extending vite config from vite.extend.js")
327
+ logger.info("🧩 Extending vite config from vite.extend.js")
194
328
  }
195
329
  if (!hasLocalIndexHtml && !hasLocalMainJs && !hasLocalExtend) {
196
- console.log(
330
+ logger.info(
197
331
  "📦 Using runtime dev files (create vite.extend.js to customize)",
198
332
  )
199
333
  }
@@ -234,11 +368,11 @@ function devCommand(argv) {
234
368
  port: finalPort,
235
369
  })
236
370
  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}`)
371
+ logger.info("🦊 Firefox extension will launch with dev server")
372
+ logger.info(`📁 Extension path: ${firefoxConfig.extensionPath}`)
373
+ logger.info(`🌐 Start URL: ${firefoxConfig.startUrl}`)
240
374
  } else {
241
- console.warn("⚠️ Firefox extension not found, skipping")
375
+ logger.warn("⚠️ Firefox extension not found, skipping")
242
376
  }
243
377
  }
244
378
 
@@ -248,11 +382,11 @@ function devCommand(argv) {
248
382
  port: finalPort,
249
383
  })
250
384
  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}`)
385
+ logger.info("🚀 Chrome extension will launch with dev server")
386
+ logger.info(`📁 Extension path: ${chromeConfig.extensionPath}`)
387
+ logger.info(`🌐 Start URL: ${chromeConfig.startUrl}`)
254
388
  } else {
255
- console.warn("⚠️ Chrome extension not found, skipping")
389
+ logger.warn("⚠️ Chrome extension not found, skipping")
256
390
  }
257
391
  }
258
392
 
@@ -261,54 +395,58 @@ function devCommand(argv) {
261
395
  process.env.CHROME_EXTENSION_PATH = chromeConfig.extensionPath
262
396
  }
263
397
 
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
398
  // Normalize path separators to forward slashes for cross-platform shell compatibility
273
399
  const normalizedViteConfigPath = viteConfigPath.replace(/\\/g, "/")
274
400
 
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")
401
+ // Build the canonical service list (raw commands, no concurrently wrapping)
402
+ const services = []
403
+ services.push({
404
+ name: "VITE",
405
+ color: "cyan",
406
+ command: `npx vite dev --config "${normalizedViteConfigPath}"`,
407
+ })
280
408
 
281
- // Socket server (on by default, skip if --no-socket or server.js not found)
282
409
  if (serverJsPath) {
283
410
  const normalizedServerPath = serverJsPath.replace(/\\/g, "/")
284
- processes.push(`"npx nodemon \\"${normalizedServerPath}\\""`)
285
- names.push("SOCKET")
286
- colors.push("green")
411
+ services.push({
412
+ name: "SOCKET",
413
+ color: "green",
414
+ command: `npx nodemon "${normalizedServerPath}"`,
415
+ })
287
416
  }
288
417
 
289
- // Firefox extension (optional)
290
418
  if (firefoxConfig) {
291
- processes.push(`"${firefoxConfig.command}"`)
292
- names.push(firefoxConfig.name)
293
- colors.push(firefoxConfig.color)
419
+ services.push({
420
+ name: firefoxConfig.name,
421
+ color: firefoxConfig.color,
422
+ command: firefoxConfig.command,
423
+ })
294
424
  }
295
425
 
296
- // Chrome extension (optional)
297
426
  if (chromeConfig) {
298
- processes.push(`"${chromeConfig.command}"`)
299
- names.push(chromeConfig.name)
300
- colors.push(chromeConfig.color)
427
+ services.push({
428
+ name: chromeConfig.name,
429
+ color: chromeConfig.color,
430
+ command: chromeConfig.command,
431
+ })
301
432
  }
302
433
 
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(" ")}`
434
+ // In JSON mode we orchestrate the children ourselves so we can wrap every
435
+ // stdout/stderr line as NDJSON. Concurrently's prefixed output would defeat
436
+ // that. Outside JSON mode, keep the legacy concurrently-based behavior.
437
+ if (logger.jsonMode) {
438
+ runServicesJson(services, logger)
439
+ return
440
+ }
441
+
442
+ let command
443
+ if (services.length > 1) {
444
+ const quoted = services.map((s) => `"${s.command}"`).join(" ")
445
+ const names = services.map((s) => s.name).join(",")
446
+ const colors = services.map((s) => s.color).join(",")
447
+ command = `npx concurrently --names "${names}" --prefix-colors "${colors}" ${quoted}`
309
448
  } else {
310
- // Just run Vite dev server alone
311
- command = `npx vite dev --config "${normalizedViteConfigPath}"`
449
+ command = services[0].command
312
450
  }
313
451
 
314
452
  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.89",
4
4
  "description": "Dev tools to create platform plugins",
5
5
  "type": "commonjs",
6
6
  "publishConfig": {
@@ -172,6 +172,22 @@ export default defineConfig(async (ctx) => {
172
172
  console.log(`📄 index.html: ${useLocalIndex ? "local" : "runtime"}`)
173
173
  console.log(`📄 main.js: ${useLocalMain ? "local" : "runtime"}`)
174
174
 
175
+ // Build /@fs/ URLs that work regardless of whether the toolkit is inside
176
+ // the project's node_modules or installed globally. Vite's /@fs/ handler
177
+ // serves these via server.fs.allow (which already includes toolkitPath),
178
+ // so absolute paths outside process.cwd() just work.
179
+ const realRuntimeDir = (() => {
180
+ try {
181
+ return fs.realpathSync(runtimeDir)
182
+ } catch {
183
+ return runtimeDir
184
+ }
185
+ })().replace(/\\/g, "/")
186
+ const toFsUrl = (relPath) =>
187
+ `/@fs${realRuntimeDir.startsWith("/") ? "" : "/"}${realRuntimeDir}/${relPath}`
188
+ const runtimeMainFsUrl = toFsUrl("main.js")
189
+ const runtimeLogoFsUrl = toFsUrl("logo.png")
190
+
175
191
  // Create plugin to serve runtime files (index.html and main.js) if no local ones exist
176
192
  const runtimeFilesPlugin = {
177
193
  name: "runtime-files",
@@ -205,15 +221,21 @@ export default defineConfig(async (ctx) => {
205
221
  ) {
206
222
  const runtimeIndexPath = path.join(runtimeDir, "index.html")
207
223
  if (fs.existsSync(runtimeIndexPath)) {
208
- // Read and transform the runtime index.html
224
+ // Rewrite hard-coded references to runtime assets so they
225
+ // resolve via Vite's /@fs/ handler instead of a guessed
226
+ // /node_modules/... path. This is what makes the same
227
+ // runtime work for local, linked, and global installs.
228
+ let html = fs.readFileSync(runtimeIndexPath, "utf-8")
229
+ html = html
230
+ .split("/node_modules/@gxp-dev/tools/runtime/logo.png")
231
+ .join(runtimeLogoFsUrl)
232
+ .split("/@gx-runtime/main.js")
233
+ .join(useLocalMain ? "/main.js" : runtimeMainFsUrl)
209
234
  server
210
- .transformIndexHtml(
211
- rawUrl,
212
- fs.readFileSync(runtimeIndexPath, "utf-8"),
213
- )
214
- .then((html) => {
235
+ .transformIndexHtml(rawUrl, html)
236
+ .then((transformed) => {
215
237
  res.setHeader("Content-Type", "text/html")
216
- res.end(html)
238
+ res.end(transformed)
217
239
  })
218
240
  .catch((err) => {
219
241
  console.error("Error transforming index.html:", err)
@@ -223,32 +245,24 @@ export default defineConfig(async (ctx) => {
223
245
  }
224
246
  }
225
247
 
226
- // Serve runtime main.js for @gx-runtime/main.js requests (unless local main.js is opted in)
248
+ // Back-compat: anything still hitting the legacy
249
+ // `/@gx-runtime/main.js` URL (e.g. a hand-rolled index.html)
250
+ // is redirected to the /@fs/ URL. The old transformRequest
251
+ // approach passed an absolute filesystem path and only worked
252
+ // when runtimeDir was inside process.cwd() — i.e. for local
253
+ // installs but not global ones.
227
254
  if (
228
255
  !useLocalMain &&
229
- (req.url === "/@gx-runtime/main.js" ||
230
- req.url?.startsWith("/@gx-runtime/main.js?"))
256
+ (urlPath === "/@gx-runtime/main.js" ||
257
+ urlPath.startsWith("/@gx-runtime/main.js"))
231
258
  ) {
232
- const runtimeMainPath = path.join(runtimeDir, "main.js")
233
- if (fs.existsSync(runtimeMainPath)) {
234
- // Use the real path to handle symlinks correctly
235
- const realMainPath = fs.realpathSync(runtimeMainPath)
236
- server
237
- .transformRequest(realMainPath)
238
- .then((result) => {
239
- if (result) {
240
- res.setHeader("Content-Type", "application/javascript")
241
- res.end(result.code)
242
- } else {
243
- next()
244
- }
245
- })
246
- .catch((err) => {
247
- console.error("Error transforming main.js:", err)
248
- next(err)
249
- })
250
- return
251
- }
259
+ const query = rawUrl.includes("?")
260
+ ? rawUrl.slice(rawUrl.indexOf("?"))
261
+ : ""
262
+ res.statusCode = 302
263
+ res.setHeader("Location", runtimeMainFsUrl + query)
264
+ res.end()
265
+ return
252
266
  }
253
267
 
254
268
  next()