create-thingworx-widget2 1.0.1 → 1.0.3

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.
@@ -2,9 +2,10 @@
2
2
 
3
3
  "use strict";
4
4
 
5
- const fs = require("fs");
6
- const path = require("path");
7
- const readline = require("readline");
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import readline from 'readline';
8
+ import { execSync } from 'child_process';
8
9
 
9
10
  // ─── ANSI colours ────────────────────────────────────────────────────────────
10
11
  const c = {
@@ -22,7 +23,7 @@ const log = (...a) => console.log(...a);
22
23
  const info = (msg) => log(` ${c.cyan}>${c.reset} ${msg}`);
23
24
  const ok = (msg) => log(` ${c.green}✔${c.reset} ${msg}`);
24
25
  const warn = (msg) => log(` ${c.yellow}!${c.reset} ${msg}`);
25
- const err = (msg) => log(` ${c.red}✖${c.reset} ${msg}`);
26
+ const fail = (msg) => log(` ${c.red}✖${c.reset} ${msg}`);
26
27
 
27
28
  // ─── Banner ───────────────────────────────────────────────────────────────────
28
29
  function banner() {
@@ -34,11 +35,11 @@ function banner() {
34
35
  log(`${c.blue} ██║ ██║ ██║██║██║ ╚████║╚██████╔╝╚███╔███╔╝╚██████╔╝██║ ██║██╔╝ ██╗${c.reset}`);
35
36
  log(`${c.blue} ╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══╝╚══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝${c.reset}`);
36
37
  log("");
37
- log(`${c.bold} Widget Scaffolder${c.reset} ${c.dim}— Gulp + Babel ${c.reset}`);
38
+ log(`${c.bold} ThingWorx Widget Scaffolder${c.reset} ${c.dim}— Gulp 5 · Babel · undici · External Dependencies${c.reset}`);
38
39
  log("");
39
40
  }
40
41
 
41
- // ─── Prompt helper ────────────────────────────────────────────────────────────
42
+ // ─── Prompt helpers ───────────────────────────────────────────────────────────
42
43
  function prompt(rl, question, defaultVal) {
43
44
  return new Promise(resolve => {
44
45
  const hint = defaultVal ? `${c.dim}(${defaultVal})${c.reset} ` : "";
@@ -48,31 +49,57 @@ function prompt(rl, question, defaultVal) {
48
49
  });
49
50
  }
50
51
 
51
- function promptSelect(rl, question, choices) {
52
- return new Promise(resolve => {
53
- log(` ${c.cyan}?${c.reset} ${question}`);
54
- choices.forEach((ch, i) => log(` ${c.dim}${i + 1}.${c.reset} ${ch}`));
55
- rl.question(` ${c.cyan} Enter number [1-${choices.length}]${c.reset}: `, ans => {
56
- const idx = parseInt(ans.trim(), 10) - 1;
57
- resolve(choices[Math.max(0, Math.min(choices.length - 1, isNaN(idx) ? 0 : idx))]);
58
- });
59
- });
60
- }
61
-
62
- // ─── toPascalCase ─────────────────────────────────────────────────────────────
63
- function toPascal(str) {
64
- return str.replace(/(^\w|-\w)/g, m => m.replace("-", "").toUpperCase());
52
+ // ─── Resolve node_modules main entry for a dep ───────────────────────────────
53
+ // Used at scaffold-time to find the JS file path (for README guidance only).
54
+ // Real resolution happens at build-time in gulpfile via require().
55
+ function resolveDepMain(dep) {
56
+ try {
57
+ const pkgPath = require.resolve(`${dep}/package.json`, { paths: [process.cwd()] });
58
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
59
+ return pkg.main || "index.js";
60
+ } catch (_) {
61
+ return null; // not installed yet — that's fine
62
+ }
65
63
  }
66
64
 
67
- // ─── File writers (boilerplate matching LinearGradient project) ───────────────
65
+ // ─────────────────────────────────────────────────────────────────────────────
66
+ // FILE WRITERS
67
+ // ─────────────────────────────────────────────────────────────────────────────
68
68
 
69
69
  function writePackageJson(root, cfg) {
70
+ // Build dependencies object: echarts always + extra deps from user input
71
+ const deps = {};
72
+ for (const dep of cfg.extraDeps) {
73
+ deps[dep] = "*"; // user can pin versions manually
74
+ }
75
+
76
+ const devDeps = {
77
+ "@babel/core": "^7.0.0-beta.46",
78
+ "@babel/preset-env": "^7.0.0-beta.46",
79
+ "@types/jquery": "^3.3.1",
80
+ "@types/node": "^8.10.11",
81
+ "babel-plugin-remove-import-export": "^1.1.0",
82
+ "del": "^5.0.0",
83
+ "delete-empty": "^3.0.0",
84
+ "eslint": "^10.4.1",
85
+ "eslint-config-prettier": "^10.1.8",
86
+ "form-data": "^4.0.5",
87
+ "gulp": "^5.0.1",
88
+ "gulp-babel": "^8.0.0",
89
+ "gulp-concat": "^2.6.1",
90
+ "gulp-terser": "^1.2.0",
91
+ "gulp-zip": "^5.0.0",
92
+ "prettier": "^3.8.3",
93
+ "undici": "^7.26.0",
94
+ "xml2js": "^0.6.2",
95
+ };
96
+
70
97
  const content = {
71
- name: cfg.packageName.toLowerCase().replace(/\s+/g, "-"),
72
- packageName: cfg.packageName,
73
- moduleName: cfg.moduleName,
74
- version: cfg.version,
75
- description: cfg.description,
98
+ name: cfg.packageName.toLowerCase().replace(/\s+/g, "-"),
99
+ packageName: cfg.packageName,
100
+ moduleName: cfg.moduleName,
101
+ version: cfg.version,
102
+ description: cfg.description,
76
103
  thingworxServer: cfg.twxServer,
77
104
  thingworxUser: cfg.twxUser,
78
105
  thingworxPassword: cfg.twxPassword,
@@ -87,39 +114,29 @@ function writePackageJson(root, cfg) {
87
114
  buildProduction: "gulp --p",
88
115
  upload: "gulp upload",
89
116
  uploadProduction: "gulp upload --p",
117
+ format: "prettier --write src/**/*.js",
118
+ "format:check": "prettier --check src/**/*.js",
90
119
  },
91
120
  license: "MIT",
92
121
  files: ["lib", "build", "LICENSE"],
93
- devDependencies: {
94
- "@babel/core": "^7.0.0-beta.46",
95
- "@babel/preset-env": "^7.0.0-beta.46",
96
- "@types/jquery": "^3.3.1",
97
- "@types/node": "^8.10.11",
98
- "gulp": "^4.0.2",
99
- "gulp-concat": "^2.6.1",
100
- "gulp-terser": "^1.2.0",
101
- "gulp-zip": "^5.0.0",
102
- "gulp-babel": "^8.0.0",
103
- "babel-plugin-remove-import-export": "^1.1.0",
104
- "delete-empty": "^3.0.0",
105
- "del": "^5.0.0",
106
- "request": "^2.85.0",
107
- "xml2js": "^0.4.19",
108
- },
122
+ devDependencies: devDeps,
123
+ dependencies: deps,
109
124
  };
125
+
110
126
  fs.writeFileSync(path.join(root, "package.json"), JSON.stringify(content, null, 4));
111
127
  }
112
128
 
113
129
  function writeMetadataXml(root, cfg) {
114
- const wName = cfg.packageName;
115
- const jsIde = `${cfg.widgetClass}.ide.js`;
116
- const jsRuntime= `${cfg.widgetClass}.runtime.js`;
117
- const cssIde = `${cfg.widgetClass}.ide.css`;
118
- const cssRuntime=`${cfg.widgetClass}.runtime.css`;
119
-
120
- const libLine = cfg.library === "echarts"
121
- ? `\n <FileResource type="JS" file="echarts.min.js" description="" isDevelopment="false" isRuntime="true"/>\n`
122
- : "";
130
+ const wName = cfg.packageName;
131
+ const jsIde = `ide\\${wName}.ide.js`;
132
+ const jsRuntime = `runtime\\${wName}.runtime.js`;
133
+ const cssIde = `assets\\styles\\${wName}.ide.css`;
134
+ const cssRuntime = `assets\\styles\\${wName}.runtime.css`;
135
+
136
+ // Build one FileResource line per extra dependency
137
+ const depLines = cfg.extraDeps.map(dep =>
138
+ ` <FileResource type="JS" file="${dep}.js"\n description="" isDevelopment="false" isRuntime="true"/>`
139
+ ).join("\n");
123
140
 
124
141
  const content = `<?xml version="1.0" encoding="UTF-8"?>
125
142
  <Entities>
@@ -134,7 +151,7 @@ function writeMetadataXml(root, cfg) {
134
151
  </ExtensionPackages>
135
152
 
136
153
  <Widgets>
137
- <Widget name="${wName}">${libLine}
154
+ <Widget name="${wName}">
138
155
  <UIResources>
139
156
 
140
157
  <FileResource type="CSS" file="${cssIde}"
@@ -146,6 +163,17 @@ function writeMetadataXml(root, cfg) {
146
163
  description="" isDevelopment="false" isRuntime="true"/>
147
164
  <FileResource type="JS" file="${jsRuntime}"
148
165
  description="" isDevelopment="false" isRuntime="true"/>
166
+ ${depLines ? depLines + "\n" : ""}
167
+ <!--
168
+ EXTERNAL DEPENDENCIES NOTE:
169
+ Each library listed above must be manually added to metadata.xml.
170
+ To find the correct JS file path for a library installed via npm:
171
+ 1. Look in node_modules/<package-name>/package.json → "main" field
172
+ 2. Copy that file to your src/ folder (gulp copies it to build automatically)
173
+ 3. Add a FileResource entry here pointing to just the filename, e.g.:
174
+ <FileResource type="JS" file="echarts.js"
175
+ description="" isDevelopment="false" isRuntime="true"/>
176
+ -->
149
177
 
150
178
  </UIResources>
151
179
  </Widget>
@@ -156,18 +184,24 @@ function writeMetadataXml(root, cfg) {
156
184
  }
157
185
 
158
186
  function writeGulpfile(root, cfg) {
159
- const content = `const path = require('path');
160
- const fs = require('fs');
161
- const xml2js = require('xml2js');
162
- const del = require('del');
187
+ // FIX: uses undici + form-data (matching the working widget project)
188
+ // FIX: upgraded from gulp 4 to gulp 5 compatible syntax
189
+ // FIX: removed deprecated 'request' package
190
+ const content = `const path = require('path');
191
+ const fs = require('fs');
192
+ const xml2js = require('xml2js');
193
+ const del = require('del');
163
194
  const deleteEmpty = require('delete-empty');
195
+ const FormData = require('form-data');
164
196
 
165
197
  const { series, src, dest } = require('gulp');
166
198
  const zip = require('gulp-zip');
167
199
  const concat = require('gulp-concat');
168
200
  const terser = require('gulp-terser');
169
201
  const babel = require('gulp-babel');
170
- const request = require('request');
202
+
203
+ // undici replaces deprecated 'request' — zero deps, Node.js official HTTP client
204
+ const { request: undiciRequest } = require('undici');
171
205
 
172
206
  const packageJson = require('./package.json');
173
207
 
@@ -176,7 +210,7 @@ const removeFiles = [];
176
210
 
177
211
  /**
178
212
  * CLI args:
179
- * --p Production build (concat + minify)
213
+ * --p Production build (concat + minify + compress)
180
214
  * --l Library mode (unsupported)
181
215
  */
182
216
  const args = (argList => {
@@ -192,12 +226,13 @@ const args = (argList => {
192
226
 
193
227
  if (args.l) throw new Error('Argument --l is unsupported for this project.');
194
228
 
195
- const outPath = \`build/ui/\${packageJson.packageName}\`;
196
- const libPath = 'lib';
229
+ const outPath = \`build/ui/\${packageJson.packageName}\`;
197
230
  const packageKind = args.p ? 'min' : 'dev';
198
- const zipName = \`\${packageJson.packageName}-\${packageKind}-\${packageJson.version}.zip\`;
231
+ const zipName = \`\${packageJson.packageName}-\${packageKind}-\${packageJson.version}.zip\`;
199
232
 
200
- /** Clean build, zip, and lib directories. */
233
+ // ─────────────────────────────────────────────
234
+ // TASK 1 — Clean build / zip / lib directories
235
+ // ─────────────────────────────────────────────
201
236
  async function cleanBuildDir(cb) {
202
237
  await del('build');
203
238
  await del('zip');
@@ -214,7 +249,9 @@ async function cleanBuildDir(cb) {
214
249
  cb();
215
250
  }
216
251
 
217
- /** Copy src files + metadata.xml into build. */
252
+ // ─────────────────────────────────────────────
253
+ // TASK 2 — Copy src files + metadata.xml
254
+ // ─────────────────────────────────────────────
218
255
  function copy(cb) {
219
256
  src('src/**')
220
257
  .pipe(dest(\`\${outPath}/\`))
@@ -224,18 +261,31 @@ function copy(cb) {
224
261
  });
225
262
  }
226
263
 
227
- /** Transpile, optionally minify, then zip. */
264
+ // ─────────────────────────────────────────────
265
+ // TASK 3 — Copy deps → Transpile → Compress → ZIP
266
+ // ─────────────────────────────────────────────
228
267
  async function prepareBuild(cb) {
229
268
  if (removeFiles.length) await del(removeFiles.map(f => \`\${outPath}/\${f}\`));
230
269
 
231
270
  // Copy npm dependencies declared in package.json "dependencies"
271
+ // Each dep's main JS file is copied to the widget output root,
272
+ // and must also be listed in metadata.xml as a FileResource.
232
273
  for (const dep in (packageJson.dependencies || {})) {
233
- const depPkg = require(\`./node_modules/\${dep}/package.json\`);
274
+ const depPkgPath = require.resolve(\`\${dep}/package.json\`);
275
+ const depPkg = JSON.parse(fs.readFileSync(depPkgPath, 'utf8'));
276
+ const mainFile = depPkg.main || 'index.js';
277
+ const destName = \`\${dep}.js\`;
278
+
234
279
  await new Promise(resolve =>
235
- src(\`node_modules/\${dep}/\${depPkg.main}\`).pipe(dest(outPath)).on('end', resolve));
280
+ src(\`node_modules/\${dep}/\${mainFile}\`)
281
+ .pipe(concat(destName)) // rename to <depname>.js for predictable metadata.xml path
282
+ .pipe(dest(outPath))
283
+ .on('end', resolve)
284
+ );
285
+ console.log(\`📦 Copied dependency: \${dep} → \${outPath}/\${destName}\`);
236
286
  }
237
287
 
238
- // Babel transpile (removes ES module import/export for TWX)
288
+ // Babel transpile removes ES module import/export for ThingWorx compatibility
239
289
  await new Promise(resolve => {
240
290
  src(\`\${outPath}/**/*.js\`)
241
291
  .pipe(babel({ plugins: ['remove-import-export'] }))
@@ -244,6 +294,7 @@ async function prepareBuild(cb) {
244
294
  });
245
295
 
246
296
  if (args.p) {
297
+ // ── Production: concat + aggressive terser compression ──
247
298
  const metadataFile = await new Promise(resolve =>
248
299
  fs.readFile('build/metadata.xml', 'utf8', (e, d) => resolve(d)));
249
300
  const metadataXML = await new Promise(resolve =>
@@ -251,6 +302,11 @@ async function prepareBuild(cb) {
251
302
 
252
303
  const fileResources = metadataXML.Entities.Widgets[0].Widget[0].UIResources[0].FileResource;
253
304
 
305
+ // Dependency JS files (from package.json "dependencies") must NOT be concat'd —
306
+ // they are copied as-is and must survive the production build unchanged.
307
+ const depFileNames = Object.keys(packageJson.dependencies || {}).map(dep => \`\${dep}.js\`);
308
+ const depResources = fileResources.filter(r => r.$.type === 'JS' && depFileNames.includes(r.$.file));
309
+
254
310
  const fileGroups = [
255
311
  { isDevelopment: true, isRuntime: true, extension: 'min' },
256
312
  { isDevelopment: false, isRuntime: true, extension: 'runtime' },
@@ -259,6 +315,8 @@ async function prepareBuild(cb) {
259
315
 
260
316
  for (const group of fileGroups) {
261
317
  group.files = fileResources.filter(r => {
318
+ // Never concat dependency files — they are kept as standalone entries
319
+ if (depFileNames.includes(r.$.file)) return false;
262
320
  const include =
263
321
  (group.isDevelopment ? r.$.isDevelopment === 'true' : r.$.isDevelopment !== 'true') &&
264
322
  (group.isRuntime ? r.$.isRuntime === 'true' : r.$.isRuntime !== 'true') &&
@@ -271,24 +329,47 @@ async function prepareBuild(cb) {
271
329
  }).map(r => r.$.file);
272
330
  }
273
331
 
332
+ // Keep only non-JS entries (CSS etc.) — dep + concat JS entries are added back below
274
333
  metadataXML.Entities.Widgets[0].Widget[0].UIResources[0].FileResource =
275
334
  fileResources.filter(r => r.$.type !== 'JS' || !r.$.file);
276
335
 
336
+ // Re-add dependency entries unchanged — they must always load as separate files
337
+ for (const depRes of depResources) {
338
+ metadataXML.Entities.Widgets[0].Widget[0].UIResources[0].FileResource.push(depRes);
339
+ }
340
+
277
341
  for (const group of fileGroups) {
278
342
  if (!group.files.length) continue;
279
343
  const name = \`\${packageJson.packageName}.\${group.extension}\`;
344
+
280
345
  await new Promise(resolve => {
281
346
  src(group.files.map(f => \`\${outPath}/\${f}\`))
282
347
  .pipe(concat(\`\${name}.js\`))
283
- .pipe(terser({ compress: true, mangle: true }))
348
+ .pipe(terser({
349
+ compress: {
350
+ dead_code : true,
351
+ drop_console: false,
352
+ passes : 2,
353
+ pure_getters: true,
354
+ unsafe : false,
355
+ },
356
+ mangle : true,
357
+ format : { comments: false },
358
+ }))
284
359
  .pipe(dest(outPath))
285
360
  .on('end', resolve);
286
361
  });
362
+
287
363
  await del(group.files.map(f => \`\${outPath}/\${f}\`));
364
+
288
365
  metadataXML.Entities.Widgets[0].Widget[0].UIResources[0].FileResource.push({
289
- $: { type: 'JS', file: \`\${name}.js\`, description: '',
290
- isDevelopment: group.isDevelopment.toString(),
291
- isRuntime: group.isRuntime.toString() }
366
+ $: {
367
+ type : 'JS',
368
+ file : \`\${name}.js\`,
369
+ description : '',
370
+ isDevelopment : group.isDevelopment.toString(),
371
+ isRuntime : group.isRuntime.toString(),
372
+ }
292
373
  });
293
374
  }
294
375
 
@@ -296,49 +377,96 @@ async function prepareBuild(cb) {
296
377
  metadataXML.Entities.ExtensionPackages[0].ExtensionPackage[0].$.buildNumber = JSON.stringify(packageJson.autoUpdate);
297
378
 
298
379
  const builder = new xml2js.Builder();
299
- await new Promise(resolve => fs.writeFile('build/metadata.xml', builder.buildObject(metadataXML), resolve));
380
+ await new Promise(resolve =>
381
+ fs.writeFile('build/metadata.xml', builder.buildObject(metadataXML), resolve));
382
+
300
383
  await deleteEmpty(\`\${outPath}/\`);
301
384
  }
302
385
 
386
+ // ZIP the final build output
303
387
  const zipStream = src('build/**').pipe(zip(zipName)).pipe(dest('zip'));
304
388
  await new Promise(resolve => zipStream.on('end', resolve));
389
+
390
+ const zipStats = fs.statSync(path.join('zip', zipName));
391
+ console.log(\`📦 ZIP created: \${zipName} (\${(zipStats.size / 1024).toFixed(1)} KB)\`);
392
+
305
393
  cb();
306
394
  }
307
395
 
308
- /** Upload extension zip to a ThingWorx server (requires valid credentials in package.json). */
396
+ // ─────────────────────────────────────────────
397
+ // TASK 4 — Upload to ThingWorx via undici
398
+ // ─────────────────────────────────────────────
309
399
  async function upload(cb) {
310
400
  const host = packageJson.thingworxServer;
311
401
  const user = packageJson.thingworxUser;
312
402
  const password = packageJson.thingworxPassword;
313
403
 
314
- return new Promise((resolve, reject) => {
315
- request.post({
316
- url: \`\${host}/Thingworx/Subsystems/PlatformSubsystem/Services/DeleteExtensionPackage\`,
317
- headers: { 'X-XSRF-TOKEN': 'TWX-XSRF-TOKEN-VALUE', Accept: 'application/json',
318
- 'Content-Type': 'application/json', 'X-THINGWORX-SESSION': 'true' },
319
- body: { packageName: packageJson.packageName },
320
- json: true,
321
- }, function (err, httpResponse, body) {
322
- const formData = { file: fs.createReadStream(path.join('zip', zipName)) };
323
- request.post({
324
- url: \`\${host}/Thingworx/ExtensionPackageUploader?purpose=import\`,
325
- headers: { 'X-XSRF-TOKEN': 'TWX-XSRF-TOKEN-VALUE' },
326
- formData,
327
- }, function (err2, resp2) {
328
- if (err2) { reject(err2); return; }
329
- if (resp2.statusCode !== 200) {
330
- reject(\`Upload failed: \${resp2.statusCode} \${resp2.statusMessage}\`);
331
- } else {
332
- console.log(\`Uploaded \${packageJson.packageName} v\${packageJson.version} to ThingWorx!\`);
333
- resolve();
334
- }
335
- }).auth(user, password);
404
+ const basicAuth = 'Basic ' + Buffer.from(\`\${user}:\${password}\`).toString('base64');
405
+
406
+ const commonHeaders = {
407
+ 'X-XSRF-TOKEN' : 'TWX-XSRF-TOKEN-VALUE',
408
+ 'X-THINGWORX-SESSION': 'true',
409
+ 'Authorization' : basicAuth,
410
+ 'Accept' : 'application/json',
411
+ };
412
+
413
+ // Step 1: Delete old extension (non-fatal)
414
+ try {
415
+ const { statusCode, body: deleteBody } = await undiciRequest(
416
+ \`\${host}/Thingworx/Subsystems/PlatformSubsystem/Services/DeleteExtensionPackage\`,
417
+ {
418
+ method : 'POST',
419
+ headers: { ...commonHeaders, 'Content-Type': 'application/json' },
420
+ body : JSON.stringify({ packageName: packageJson.packageName }),
421
+ }
422
+ );
423
+ await deleteBody.dump();
424
+ console.log(\`🗑️ Delete previous version — HTTP \${statusCode}\`);
425
+ } catch (e) {
426
+ console.warn('⚠️ Delete skipped:', e.message);
427
+ }
336
428
 
337
- if (err) console.error('Failed to delete previous widget version:', err);
338
- }).auth(user, password);
429
+ // Step 2: Upload ZIP stream-based multipart
430
+ const zipPath = path.join('zip', zipName);
431
+ const form = new FormData();
432
+ form.append('file', fs.createReadStream(zipPath), {
433
+ filename : zipName,
434
+ contentType : 'application/zip',
435
+ knownLength : fs.statSync(zipPath).size,
339
436
  });
437
+
438
+ await new Promise((resolve, reject) => {
439
+ form.submit(
440
+ {
441
+ host : new URL(host).hostname,
442
+ port : new URL(host).port || 80,
443
+ path : '/Thingworx/ExtensionPackageUploader?purpose=import',
444
+ method : 'POST',
445
+ headers : { ...commonHeaders, ...form.getHeaders() },
446
+ timeout : 120000,
447
+ },
448
+ (e, res) => {
449
+ if (e) { reject(e); return; }
450
+ let data = '';
451
+ res.on('data', chunk => (data += chunk));
452
+ res.on('end', () => {
453
+ if (res.statusCode === 200) {
454
+ console.log(\`✅ Uploaded \${packageJson.packageName} v\${packageJson.version} to ThingWorx!\`);
455
+ resolve();
456
+ } else {
457
+ reject(new Error(\`Upload failed — HTTP \${res.statusCode}: \${data}\`));
458
+ }
459
+ });
460
+ }
461
+ );
462
+ });
463
+
464
+ cb();
340
465
  }
341
466
 
467
+ // ─────────────────────────────────────────────
468
+ // Exports
469
+ // ─────────────────────────────────────────────
342
470
  exports.default = series(cleanBuildDir, copy, prepareBuild);
343
471
  exports.upload = series(cleanBuildDir, copy, prepareBuild, upload);
344
472
  `;
@@ -346,193 +474,218 @@ exports.upload = series(cleanBuildDir, copy, prepareBuild, upload);
346
474
  }
347
475
 
348
476
  function writeIdeJs(root, cfg) {
349
- const wClass = cfg.widgetClass;
350
- const wName = cfg.packageName;
351
- const lib = cfg.library;
352
-
353
- const libComment = lib === "echarts"
354
- ? `\n // ECharts is loaded globally via metadata.xml → echarts.min.js\n // Use: var chart = echarts.init(container);\n`
355
- : "";
477
+ const wName = cfg.packageName;
356
478
 
357
479
  const content = `/* ── ${wName} — IDE (Composer) ── */
358
480
 
359
481
  TW.IDE.Widgets.${wName} = function () {
360
482
 
361
483
  this.widgetIconUrl = function () {
362
- return "../Common/extensions/${wName}/ui/${wName}/icon.png";
484
+ return '../Common/extensions/${wName}/ui/${wName}/icon.png';
363
485
  };
364
486
 
365
487
  this.widgetProperties = function () {
366
488
  return {
367
- name: "${cfg.moduleName}",
368
- description: "${cfg.description}",
369
- category: ["Common"],
489
+ name: '${cfg.moduleName}',
490
+ description: '${cfg.description}',
491
+ category: ['Common'],
370
492
  properties: {
371
493
 
372
494
  // ── Data binding ──────────────────────────────────────────
373
495
  Data: {
374
- description: "Infotable data source.",
375
- baseType: "INFOTABLE",
496
+ description: 'Infotable data source.',
497
+ baseType: 'INFOTABLE',
376
498
  isBindingTarget: true,
377
499
  isEditable: false,
378
500
  },
379
501
 
380
502
  // ── Dimensions ────────────────────────────────────────────
381
503
  Width: {
382
- description: "Widget width in pixels.",
383
- baseType: "NUMBER",
504
+ description: 'Widget width in pixels.',
505
+ baseType: 'NUMBER',
384
506
  defaultValue: 600,
385
507
  },
386
508
  Height: {
387
- description: "Widget height in pixels.",
388
- baseType: "NUMBER",
509
+ description: 'Widget height in pixels.',
510
+ baseType: 'NUMBER',
389
511
  defaultValue: 400,
390
512
  },
391
513
 
392
514
  // ── Display options ───────────────────────────────────────
393
515
  Title: {
394
- description: "Chart title displayed above the widget.",
395
- baseType: "STRING",
396
- defaultValue: "${cfg.moduleName}",
516
+ description: 'Chart title displayed above the widget.',
517
+ baseType: 'STRING',
518
+ defaultValue: '${cfg.moduleName}',
397
519
  isBindingTarget: true,
398
520
  isEditable: true,
399
521
  },
400
522
  ShowLegend: {
401
- description: "Show the chart legend.",
402
- baseType: "BOOLEAN",
523
+ description: 'Show the chart legend.',
524
+ baseType: 'BOOLEAN',
403
525
  defaultValue: true,
404
526
  },
405
527
  Theme: {
406
- description: "Widget colour theme.",
407
- baseType: "STRING",
408
- defaultValue: "light",
528
+ description: 'Widget colour theme.',
529
+ baseType: 'STRING',
530
+ defaultValue: 'light',
409
531
  },
410
- }
532
+ },
411
533
  };
412
534
  };
413
535
 
414
536
  this.widgetServices = function () {
415
537
  return {
416
- Refresh: { description: "Re-renders the widget with current data." },
538
+ Refresh: { description: 'Re-renders the widget with current data.' },
417
539
  };
418
540
  };
419
541
 
420
542
  this.widgetEvents = function () {
421
543
  return {
422
- SelectionChanged: { description: "Fired when a data point is selected." },
544
+ SelectionChanged: { description: 'Fired when a data point is selected.' },
423
545
  };
424
546
  };
425
- ${libComment}
547
+
426
548
  this.renderHtml = function () {
427
549
  return [
428
- '<div class="widget-content ${wName.toLowerCase()}-ide-wrapper">',
550
+ '<div class="${wName.toLowerCase()}-ide-wrapper">',
429
551
  ' <span class="${wName.toLowerCase()}-ide-label">📊 ${cfg.moduleName}</span>',
430
552
  '</div>',
431
553
  ].join('');
432
554
  };
433
555
 
434
- this.afterRender = function () { /* IDE after-render hook */ };
435
- this.beforeSave = function () { /* validate before Composer save */ };
436
- this.beforeDestroy= function () { /* clean up IDE resources */ };
556
+ this.afterRender = function () { /* IDE after-render hook */ };
557
+ this.beforeSave = function () { /* validate before Composer save */ };
558
+ this.beforeDestroy = function () { /* clean up IDE resources */ };
437
559
  };
438
560
  `;
439
- fs.writeFileSync(path.join(root, "src", `${wClass}.ide.js`), content);
561
+ const ideDir = path.join(root, "src", "ide");
562
+ fs.mkdirSync(ideDir, { recursive: true });
563
+ fs.writeFileSync(path.join(ideDir, `${wName}.ide.js`), content);
440
564
  }
441
565
 
442
566
  function writeRuntimeJs(root, cfg) {
443
- const wName = cfg.packageName;
444
- const wClass = cfg.widgetClass;
567
+ const wName = cfg.packageName;
568
+ const wLow = wName.toLowerCase();
445
569
 
446
- const libInit = cfg.library === "echarts"
447
- ? ` // Initialise ECharts\n var container = widget.jqElement.find(".${wName.toLowerCase()}-chart")[0];\n chart = echarts.init(container);`
448
- : ` // Initialise your chart library here\n var container = widget.jqElement.find(".${wName.toLowerCase()}-chart")[0];`;
570
+ // Build dep-awareness comment
571
+ const depComment = cfg.extraDeps.length
572
+ ? `\n // External dependencies available as globals (loaded via metadata.xml):\n` +
573
+ cfg.extraDeps.map(d => ` // ${d} (from node_modules → copied to build as ${d}.js)`).join("\n") + "\n"
574
+ : "";
449
575
 
450
576
  const content = `/* ── ${wName} — Runtime (Mashup) ── */
451
577
 
452
578
  TW.Runtime.Widgets.${wName} = function () {
453
-
454
579
  var widget = this;
455
580
  var chart = null;
456
581
  var cachedData = null;
582
+ ${depComment}
583
+ console.log('[${wName}] ── Widget constructor called');
457
584
 
458
585
  // ── Render shell HTML ─────────────────────────────────────────────────
459
586
  this.renderHtml = function () {
587
+ console.log('[${wName}] renderHtml() called');
460
588
  return [
461
- '<div class="widget-content ${wName.toLowerCase()}-wrapper">',
462
- ' <div class="${wName.toLowerCase()}-toolbar">',
463
- ' <span class="${wName.toLowerCase()}-title"></span>',
589
+ '<div class="widget-content ${wLow}-wrapper">',
590
+ ' <div class="${wLow}-toolbar">',
591
+ ' <span class="${wLow}-title"></span>',
464
592
  ' </div>',
465
- ' <div class="${wName.toLowerCase()}-chart"></div>',
466
- ' <div class="${wName.toLowerCase()}-error" style="display:none;"></div>',
593
+ ' <div class="${wLow}-chart"></div>',
594
+ ' <div class="${wLow}-error" style="display:none;"></div>',
467
595
  '</div>',
468
596
  ].join('');
469
597
  };
470
598
 
471
- // ── After render — attach events and initialise the chart ─────────────
599
+ // ── After render ──────────────────────────────────────────────────────
472
600
  this.afterRender = function () {
473
- // Update title from property
474
- widget.jqElement.find(".${wName.toLowerCase()}-title")
475
- .text(widget.getProperty("Title") || "${cfg.moduleName}");
601
+ console.log('[${wName}] afterRender() called');
476
602
 
477
- ${libInit}
603
+ var title = widget.getProperty('Title') || '${cfg.moduleName}';
604
+ widget.jqElement.find('.${wLow}-title').text(title);
605
+
606
+ var container = widget.jqElement.find('.${wLow}-chart')[0];
607
+ console.log('[${wName}] afterRender() → chart container =', container);
478
608
 
479
609
  _renderChart();
480
610
  };
481
611
 
482
- // ── Called by ThingWorx when a bound data service returns ─────────────
612
+ // ── Called by ThingWorx when bound data service returns ───────────────
483
613
  this.updateProperty = function (info) {
484
- if (info.TargetProperty === "Data") {
614
+ console.log('[${wName}] updateProperty() → TargetProperty =', info.TargetProperty);
615
+
616
+ if (info.TargetProperty === 'Data') {
485
617
  cachedData = info.ActualDataRows || [];
618
+ console.log('[${wName}] updateProperty() → Data rows =', cachedData.length);
486
619
  _renderChart();
487
620
  }
488
- if (info.TargetProperty === "Title") {
489
- widget.jqElement.find(".${wName.toLowerCase()}-title").text(info.SinglePropertyValue);
621
+
622
+ if (info.TargetProperty === 'Title') {
623
+ widget.jqElement.find('.${wLow}-title').text(info.SinglePropertyValue);
490
624
  }
491
625
  };
492
626
 
493
- // ── Service invocations (e.g., Refresh) ───────────────────────────────
627
+ // ── Service invocations ───────────────────────────────────────────────
494
628
  this.serviceInvoked = function (name) {
495
- if (name === "Refresh") _renderChart();
629
+ console.log('[${wName}] serviceInvoked() → name =', name);
630
+ if (name === 'Refresh') _renderChart();
496
631
  };
497
632
 
498
- // ── Clean up on destroy ───────────────────────────────────────────────
633
+ // ── Clean up ──────────────────────────────────────────────────────────
499
634
  this.beforeDestroy = function () {
500
- if (chart && typeof chart.dispose === "function") chart.dispose();
501
- chart = null;
635
+ console.log('[${wName}] beforeDestroy() called');
636
+ if (chart && typeof chart.dispose === 'function') {
637
+ chart.dispose();
638
+ }
639
+ chart = null;
640
+ cachedData = null;
502
641
  };
503
642
 
504
- // ── Private: build / update the chart ────────────────────────────────
643
+ // ── Private: render chart ─────────────────────────────────────────────
505
644
  function _renderChart() {
506
645
  if (!cachedData || !cachedData.length) {
507
- _showError("No data bound. Connect a data service to the Data property.");
646
+ _showError('No data bound. Connect a data service to the Data property.');
508
647
  return;
509
648
  }
510
649
  _hideError();
511
650
 
512
- // TODO: build your chart options from cachedData
513
- // Example with ECharts:
514
- // var option = { ... };
515
- // if (chart) chart.setOption(option);
651
+ // TODO: implement your chart rendering here.
652
+ // Example using ECharts (loaded globally via metadata.xml):
653
+ //
654
+ // if (!chart) {
655
+ // var container = widget.jqElement.find('.${wLow}-chart')[0];
656
+ // chart = echarts.init(container);
657
+ // }
658
+ //
659
+ // var option = {
660
+ // title: { text: widget.getProperty('Title') || '${cfg.moduleName}' },
661
+ // tooltip: { trigger: 'axis' },
662
+ // xAxis: { type: 'category', data: cachedData.map(r => r.Label || '') },
663
+ // yAxis: { type: 'value' },
664
+ // series: [{ type: 'bar', data: cachedData.map(r => r.Value || 0) }],
665
+ // };
666
+ // chart.setOption(option);
516
667
  }
517
668
 
518
669
  function _showError(msg) {
519
- widget.jqElement.find(".${wName.toLowerCase()}-error")
520
- .text(msg).show();
670
+ console.error('[${wName}] _showError() →', msg);
671
+ widget.jqElement.find('.${wLow}-error').text(msg).show();
521
672
  }
522
673
 
523
674
  function _hideError() {
524
- widget.jqElement.find(".${wName.toLowerCase()}-error").hide();
675
+ widget.jqElement.find('.${wLow}-error').hide();
525
676
  }
526
677
  };
527
678
  `;
528
- fs.writeFileSync(path.join(root, "src", `${wClass}.runtime.js`), content);
679
+ const runtimeDir = path.join(root, "src", "runtime");
680
+ fs.mkdirSync(runtimeDir, { recursive: true });
681
+ fs.writeFileSync(path.join(runtimeDir, `${wName}.runtime.js`), content);
529
682
  }
530
683
 
531
684
  function writeIdeCss(root, cfg) {
532
- const wName = cfg.packageName.toLowerCase();
685
+ const wLow = cfg.packageName.toLowerCase();
533
686
  const content = `/* ── ${cfg.packageName} — IDE (Design Time) styles ── */
534
687
 
535
- .${wName}-ide-wrapper {
688
+ .${wLow}-ide-wrapper {
536
689
  width: 100%;
537
690
  height: 100%;
538
691
  display: flex;
@@ -547,19 +700,21 @@ function writeIdeCss(root, cfg) {
547
700
  border-radius: 4px;
548
701
  }
549
702
 
550
- .${wName}-ide-label {
703
+ .${wLow}-ide-label {
551
704
  font-weight: 600;
552
705
  letter-spacing: 0.5px;
553
706
  }
554
707
  `;
555
- fs.writeFileSync(path.join(root, "src", `${cfg.widgetClass}.ide.css`), content);
708
+ const stylesDir = path.join(root, "src", "assets", "styles");
709
+ fs.mkdirSync(stylesDir, { recursive: true });
710
+ fs.writeFileSync(path.join(stylesDir, `${cfg.packageName}.ide.css`), content);
556
711
  }
557
712
 
558
713
  function writeRuntimeCss(root, cfg) {
559
- const wName = cfg.packageName.toLowerCase();
714
+ const wLow = cfg.packageName.toLowerCase();
560
715
  const content = `/* ── ${cfg.packageName} — Runtime styles ── */
561
716
 
562
- .${wName}-wrapper {
717
+ .${wLow}-wrapper {
563
718
  width: 100%;
564
719
  height: 100%;
565
720
  display: flex;
@@ -570,7 +725,7 @@ function writeRuntimeCss(root, cfg) {
570
725
  background: #ffffff;
571
726
  }
572
727
 
573
- .${wName}-toolbar {
728
+ .${wLow}-toolbar {
574
729
  display: flex;
575
730
  align-items: center;
576
731
  gap: 8px;
@@ -580,24 +735,24 @@ function writeRuntimeCss(root, cfg) {
580
735
  flex-shrink: 0;
581
736
  }
582
737
 
583
- .${wName}-title {
738
+ .${wLow}-title {
584
739
  font-size: 14px;
585
740
  font-weight: 600;
586
741
  color: #2c3e50;
587
742
  }
588
743
 
589
- .${wName}-chart {
744
+ .${wLow}-chart {
590
745
  flex: 1;
591
746
  min-height: 0;
592
747
  position: relative;
593
748
  }
594
749
 
595
- .${wName}-chart canvas {
750
+ .${wLow}-chart canvas {
596
751
  width: 100% !important;
597
752
  height: 100% !important;
598
753
  }
599
754
 
600
- .${wName}-error {
755
+ .${wLow}-error {
601
756
  background: #fadbd8;
602
757
  color: #c0392b;
603
758
  padding: 8px 12px;
@@ -607,7 +762,74 @@ function writeRuntimeCss(root, cfg) {
607
762
  text-align: center;
608
763
  }
609
764
  `;
610
- fs.writeFileSync(path.join(root, "src", `${cfg.widgetClass}.runtime.css`), content);
765
+ const stylesDir = path.join(root, "src", "assets", "styles");
766
+ fs.mkdirSync(stylesDir, { recursive: true });
767
+ fs.writeFileSync(path.join(stylesDir, `${cfg.packageName}.runtime.css`), content);
768
+ }
769
+
770
+ function writeEslintrc(root) {
771
+ const content = `{
772
+ "env": {
773
+ "browser": true,
774
+ "es6": true
775
+ },
776
+ "extends": [
777
+ "eslint:recommended",
778
+ "prettier"
779
+ ],
780
+ "globals": {
781
+ "TW": "readonly",
782
+ "echarts": "readonly",
783
+ "$": "readonly",
784
+ "jQuery": "readonly"
785
+ },
786
+ "rules": {
787
+ "no-unused-vars": ["warn", { "vars": "all", "args": "after-used" }],
788
+ "no-console": "off"
789
+ }
790
+ }
791
+ `;
792
+ fs.writeFileSync(path.join(root, ".eslintrc"), content);
793
+ }
794
+
795
+ function writePrettierrc(root) {
796
+ const content = `{
797
+ "printWidth" : 100,
798
+ "tabWidth" : 4,
799
+ "useTabs" : false,
800
+ "semi" : true,
801
+ "singleQuote" : true,
802
+ "quoteProps" : "as-needed",
803
+ "trailingComma" : "es5",
804
+ "bracketSpacing" : true,
805
+ "bracketSameLine" : false,
806
+ "arrowParens" : "avoid",
807
+ "endOfLine" : "lf",
808
+
809
+ "overrides": [
810
+ {
811
+ "files": ["*.json"],
812
+ "options": {
813
+ "printWidth" : 80,
814
+ "tabWidth" : 2
815
+ }
816
+ },
817
+ {
818
+ "files": ["*.xml"],
819
+ "options": {
820
+ "printWidth" : 200,
821
+ "tabWidth" : 4
822
+ }
823
+ }
824
+ ]
825
+ }
826
+ `;
827
+ fs.writeFileSync(path.join(root, ".prettierrc"), content);
828
+ }
829
+
830
+ function writePrettierIgnore(root) {
831
+ fs.writeFileSync(path.join(root, ".prettierignore"),
832
+ `build/\nzip/\nlib/\nnode_modules/\n*.min.js\n`);
611
833
  }
612
834
 
613
835
  function writeGitignore(root) {
@@ -616,6 +838,16 @@ function writeGitignore(root) {
616
838
  }
617
839
 
618
840
  function writeReadme(root, cfg) {
841
+ const wName = cfg.packageName;
842
+
843
+ // Build dep table rows
844
+ const depRows = cfg.extraDeps.length
845
+ ? cfg.extraDeps.map(dep => {
846
+ const mainFile = resolveDepMain(dep) || "(see node_modules/" + dep + "/package.json → main)";
847
+ return `| \`${dep}\` | \`node_modules/${dep}/${mainFile}\` |`;
848
+ }).join("\n")
849
+ : "| _(none)_ | — |";
850
+
619
851
  const content = `# ${cfg.moduleName} — ThingWorx Custom Widget
620
852
 
621
853
  > ${cfg.description}
@@ -631,17 +863,25 @@ npm run buildProduction # minified build → zip/
631
863
  ## Project Structure
632
864
 
633
865
  \`\`\`
634
- ${cfg.packageName}/
866
+ ${wName}/
635
867
  ├── src/
636
- │ ├── ${cfg.widgetClass}.ide.js ← Composer (IDE) behaviour & properties
637
- ├── ${cfg.widgetClass}.runtime.js Mashup runtime rendering & data
638
- │ ├── ${cfg.widgetClass}.ide.css ← Composer styles
639
- │ └── ${cfg.widgetClass}.runtime.css Runtime styles
640
- ├── build/ ← Gulp output (gitignored)
641
- ├── zip/ ← Extension .zip ready for TWX import
642
- ├── metadata.xml ThingWorx extension manifest
643
- ├── gulpfile.js Build pipeline
644
- └── package.json
868
+ │ ├── ide/
869
+ │ └── ${wName}.ide.js Composer (IDE) behaviour & properties
870
+ │ ├── runtime/
871
+ └── ${wName}.runtime.js Mashup runtime rendering & data
872
+ │ └── assets/
873
+ │ └── styles/
874
+ ├── ${wName}.ide.css Composer styles
875
+ │ └── ${wName}.runtime.css Runtime styles
876
+ ├── build/ ← Gulp output (gitignored)
877
+ ├── zip/ ← Extension .zip ready for TWX import
878
+ ├── metadata.xml ← ThingWorx extension manifest
879
+ ├── gulpfile.js ← Build pipeline
880
+ ├── package.json
881
+ ├── .eslintrc
882
+ ├── .prettierrc
883
+ ├── .prettierignore
884
+ └── .gitignore
645
885
  \`\`\`
646
886
 
647
887
  ## npm Scripts
@@ -652,6 +892,8 @@ ${cfg.packageName}/
652
892
  | \`npm run buildProduction\` | Production build (concat + minify) |
653
893
  | \`npm run upload\` | Dev build + upload to TWX |
654
894
  | \`npm run uploadProduction\` | Production build + upload to TWX |
895
+ | \`npm run format\` | Format source files with Prettier |
896
+ | \`npm run format:check\` | Check formatting without writing |
655
897
 
656
898
  ## Deploying to ThingWorx
657
899
 
@@ -664,20 +906,82 @@ ${cfg.packageName}/
664
906
  Edit \`package.json\` and set:
665
907
  - \`thingworxServer\` — e.g. \`http://localhost:8085\`
666
908
  - \`thingworxUser\` / \`thingworxPassword\`
909
+
910
+ ---
911
+
912
+ ## ⚠️ External Dependencies — Manual metadata.xml Step Required
913
+
914
+ Dependencies listed in \`package.json → dependencies\` are **automatically copied to the build** by Gulp (using the \`main\` field from each package's \`package.json\`).
915
+
916
+ However, **each dependency must also be manually declared in \`metadata.xml\`** as a \`<FileResource>\` entry so ThingWorx knows to load it in the Mashup runtime.
917
+
918
+ ### Your declared dependencies
919
+
920
+ | Package | Source file (from node_modules) |
921
+ |---|---|
922
+ ${depRows}
923
+
924
+ ### How to find the correct JS path for any library
925
+
926
+ \`\`\`bash
927
+ # 1. Install the package
928
+ npm install <package-name>
929
+
930
+ # 2. Find its main file
931
+ node -e "const p = require('<package-name>/package.json'); console.log(p.main)"
932
+
933
+ # Example for echarts:
934
+ node -e "const p = require('echarts/package.json'); console.log(p.main)"
935
+ # → dist/echarts.esm.js (or similar)
936
+ \`\`\`
937
+
938
+ ### metadata.xml snippet to add
939
+
940
+ \`\`\`xml
941
+ <!-- Add inside <UIResources>, with isDevelopment="false" isRuntime="true" -->
942
+ <FileResource type="JS" file="echarts.js"
943
+ description="" isDevelopment="false" isRuntime="true"/>
944
+ \`\`\`
945
+
946
+ > The Gulp build renames each dependency to \`<package-name>.js\` in the output,
947
+ > so the \`file\` attribute should always be \`<package-name>.js\`.
948
+
949
+ ---
950
+
951
+ ## How the Build Pipeline Works
952
+
953
+ 1. **\`cleanBuildDir\`** — wipes \`build/\`, \`zip/\`, \`lib/\`
954
+ 2. **\`copy\`** — copies \`src/**\` → \`build/ui/${wName}/\` and \`metadata.xml\` → \`build/\`
955
+ 3. **\`prepareBuild\`**
956
+ - Copies each \`dependencies\` entry from \`node_modules/<dep>/<main>\` → \`build/ui/${wName}/<dep>.js\`
957
+ - Babel transpiles all JS (removes ES module \`import\`/\`export\` for TWX compatibility)
958
+ - In \`--p\` mode: groups JS files by IDE/Runtime, concatenates + minifies with Terser, updates \`metadata.xml\`
959
+ - Zips \`build/\` → \`zip/${wName}-dev|min-<version>.zip\`
960
+ 4. **\`upload\`** (optional) — POSTs the zip to ThingWorx via the Extension Package Uploader servlet using \`undici\`
667
961
  `;
668
962
  fs.writeFileSync(path.join(root, "README.md"), content);
669
963
  }
670
964
 
671
- // ─── Main scaffold ────────────────────────────────────────────────────────────
965
+ // ─────────────────────────────────────────────────────────────────────────────
966
+ // SCAFFOLD ORCHESTRATOR
967
+ // ─────────────────────────────────────────────────────────────────────────────
672
968
  async function scaffold(cfg) {
673
969
  const root = path.resolve(process.cwd(), cfg.packageName);
674
970
 
675
971
  if (fs.existsSync(root)) {
676
- err(`Directory "${cfg.packageName}" already exists. Aborting.`);
972
+ fail(`Directory "${cfg.packageName}" already exists. Aborting.`);
677
973
  process.exit(1);
678
974
  }
679
975
 
680
- fs.mkdirSync(path.join(root, "src"), { recursive: true });
976
+ // Create all directories upfront
977
+ [
978
+ root,
979
+ path.join(root, "src", "ide"),
980
+ path.join(root, "src", "runtime"),
981
+ path.join(root, "src", "assets", "styles"),
982
+ path.join(root, "src", "assets", "images"),
983
+ path.join(root, "src", "lib"),
984
+ ].forEach(d => fs.mkdirSync(d, { recursive: true }));
681
985
 
682
986
  writePackageJson(root, cfg);
683
987
  writeMetadataXml(root, cfg);
@@ -686,35 +990,42 @@ async function scaffold(cfg) {
686
990
  writeRuntimeJs(root, cfg);
687
991
  writeIdeCss(root, cfg);
688
992
  writeRuntimeCss(root, cfg);
993
+ writeEslintrc(root);
994
+ writePrettierrc(root);
995
+ writePrettierIgnore(root);
689
996
  writeGitignore(root);
690
997
  writeReadme(root, cfg);
691
998
 
692
- // Placeholder for library file
693
- if (cfg.library === "echarts") {
694
- fs.writeFileSync(
695
- path.join(root, "src", "echarts.min.js"),
696
- `/* Place echarts.min.js here — download from https://echarts.apache.org/en/download.html */\n`
697
- );
698
- ok("src/echarts.min.js (placeholder — replace with real echarts.min.js)");
699
- }
999
+ // Placeholder lib/A.js (mirrors sample project)
1000
+ fs.writeFileSync(path.join(root, "src", "lib", "A.js"),
1001
+ `/* Shared utilities — add reusable helpers here */\n`);
700
1002
 
701
1003
  return root;
702
1004
  }
703
1005
 
704
- // ─── Print file tree ──────────────────────────────────────────────────────────
1006
+ // ─────────────────────────────────────────────────────────────────────────────
1007
+ // PRINT TREE
1008
+ // ─────────────────────────────────────────────────────────────────────────────
705
1009
  function printTree(root, cfg) {
706
- const rel = f => path.relative(root, f);
1010
+ const wName = cfg.packageName;
707
1011
  const files = [
708
- "package.json", "gulpfile.js", "metadata.xml", ".gitignore", "README.md",
709
- `src/${cfg.widgetClass}.ide.js`,
710
- `src/${cfg.widgetClass}.runtime.js`,
711
- `src/${cfg.widgetClass}.ide.css`,
712
- `src/${cfg.widgetClass}.runtime.css`,
713
- ...(cfg.library === "echarts" ? ["src/echarts.min.js (placeholder)"] : []),
1012
+ "package.json",
1013
+ "gulpfile.js",
1014
+ "metadata.xml",
1015
+ ".eslintrc",
1016
+ ".prettierrc",
1017
+ ".prettierignore",
1018
+ ".gitignore",
1019
+ "README.md",
1020
+ `src/ide/${wName}.ide.js`,
1021
+ `src/runtime/${wName}.runtime.js`,
1022
+ `src/assets/styles/${wName}.ide.css`,
1023
+ `src/assets/styles/${wName}.runtime.css`,
1024
+ "src/lib/A.js",
714
1025
  ];
715
1026
 
716
1027
  log("");
717
- log(` ${c.bold}${cfg.packageName}/${c.reset}`);
1028
+ log(` ${c.bold}${wName}/${c.reset}`);
718
1029
  files.forEach((f, i) => {
719
1030
  const last = i === files.length - 1;
720
1031
  const prefix = last ? "└──" : "├──";
@@ -723,37 +1034,46 @@ function printTree(root, cfg) {
723
1034
  log(` ${c.dim}└── node_modules/ (after npm install)${c.reset}`);
724
1035
  }
725
1036
 
726
- // ─── CLI entry ────────────────────────────────────────────────────────────────
1037
+ // ─────────────────────────────────────────────────────────────────────────────
1038
+ // CLI ENTRY
1039
+ // ─────────────────────────────────────────────────────────────────────────────
727
1040
  async function main() {
728
1041
  banner();
729
1042
 
730
- // Support --name MyWidget for non-interactive / CI use
731
- const argName = (process.argv.find(a => a.startsWith("--name=")) || "").replace("--name=","");
732
-
733
1043
  const rl = readline.createInterface({
734
- input: process.stdin,
735
- output: process.stdout,
736
- terminal: process.stdin.isTTY, // disable echo in pipe mode
1044
+ input: process.stdin,
1045
+ output: process.stdout,
1046
+ terminal: process.stdin.isTTY,
737
1047
  });
738
1048
 
739
1049
  log(`${c.bold} Let's set up your ThingWorx widget!${c.reset}`);
740
- log(` ${c.dim}Press Enter to accept defaults.${c.reset}`);
1050
+ log(` ${c.dim}Press Enter to accept defaults shown in (parentheses).${c.reset}`);
741
1051
  log("");
742
1052
 
743
- const packageName = await prompt(rl, "Widget package name (e.g. MyChartWidget)", "MyWidget");
744
- const moduleName = await prompt(rl, "Display name in Composer", packageName.replace(/Widget$/, " Widget"));
745
- const description = await prompt(rl, "Short description", `${moduleName} for ThingWorx`);
746
- const version = await prompt(rl, "Version", "1.0.0");
747
- const author = await prompt(rl, "Author / vendor", "YourCompany");
748
- const minTwxVer = await prompt(rl, "Minimum ThingWorx version", "9.0.0");
749
- const twxServer = await prompt(rl, "ThingWorx server URL", "http://localhost:8085");
750
- const twxUser = await prompt(rl, "ThingWorx username", "Administrator");
751
- const twxPassword = await prompt(rl, "ThingWorx password", "yourpassword");
1053
+ const packageName = await prompt(rl, "Widget package name (PascalCase, e.g. MyChartWidget)", "MyWidget");
1054
+ const moduleName = await prompt(rl, "Display name in Composer", packageName);
1055
+ const description = await prompt(rl, "Short description", `${moduleName} for ThingWorx`);
1056
+ const version = await prompt(rl, "Version", "1.0.0");
1057
+ const author = await prompt(rl, "Author / vendor", "YourCompany");
1058
+ const minTwxVer = await prompt(rl, "Minimum ThingWorx version", "9.0.0");
1059
+ const twxServer = await prompt(rl, "ThingWorx server URL", "http://localhost:8085");
1060
+ const twxUser = await prompt(rl, "ThingWorx username", "Administrator");
1061
+ const twxPassword = await prompt(rl, "ThingWorx password", "yourpassword");
1062
+
752
1063
  log("");
753
- const library = await promptSelect(rl, "Chart library to include?", ["echarts", "none (I'll add my own)"]);
1064
+ log(` ${c.cyan}?${c.reset} External npm dependencies to install (space-separated).`);
1065
+ log(` ${c.dim} These will be auto-copied to build/ and added to package.json.${c.reset}`);
1066
+ log(` ${c.dim} You must still add each one to metadata.xml manually (see README).${c.reset}`);
1067
+ log(` ${c.dim} Example: echarts datatable xyz${c.reset}`);
1068
+ const depsRaw = await prompt(rl, "Dependencies", "echarts");
754
1069
 
755
1070
  rl.close();
756
1071
 
1072
+ const extraDeps = depsRaw
1073
+ .split(/\s+/)
1074
+ .map(d => d.trim())
1075
+ .filter(Boolean);
1076
+
757
1077
  const cfg = {
758
1078
  packageName,
759
1079
  moduleName,
@@ -764,24 +1084,28 @@ async function main() {
764
1084
  twxServer,
765
1085
  twxUser,
766
1086
  twxPassword,
767
- library: library.startsWith("echarts") ? "echarts" : "none",
768
- widgetClass: packageName, // used for file names: MyWidget.ide.js etc.
1087
+ extraDeps,
769
1088
  };
770
1089
 
771
1090
  log("");
772
- info(`Scaffolding ${c.bold}${packageName}${c.reset}...`);
1091
+ info(`Scaffolding ${c.bold}${packageName}${c.reset} with deps: ${c.yellow}${extraDeps.join(", ") || "(none)"}${c.reset}`);
773
1092
  log("");
774
1093
 
775
1094
  const root = await scaffold(cfg);
776
1095
 
777
- // Print what was created
778
- const files = fs.readdirSync(path.join(root, "src"));
779
1096
  ok("package.json");
780
- ok("gulpfile.js");
781
- ok("metadata.xml");
1097
+ ok("gulpfile.js (Gulp 5 + undici — no deprecated 'request')");
1098
+ ok("metadata.xml (with dep FileResource stubs + inline instructions)");
1099
+ ok(".eslintrc");
1100
+ ok(".prettierrc");
1101
+ ok(".prettierignore");
782
1102
  ok(".gitignore");
783
- ok("README.md");
784
- files.forEach(f => ok(`src/${f}`));
1103
+ ok("README.md (dep resolution guide included)");
1104
+ ok(`src/ide/${packageName}.ide.js`);
1105
+ ok(`src/runtime/${packageName}.runtime.js`);
1106
+ ok(`src/assets/styles/${packageName}.ide.css`);
1107
+ ok(`src/assets/styles/${packageName}.runtime.css`);
1108
+ ok("src/lib/A.js");
785
1109
 
786
1110
  printTree(root, cfg);
787
1111
 
@@ -791,14 +1115,19 @@ async function main() {
791
1115
  log(` ${c.dim}Next steps:${c.reset}`);
792
1116
  log(` ${c.cyan} cd ${packageName}${c.reset}`);
793
1117
  log(` ${c.cyan} npm install${c.reset}`);
794
- if (cfg.library === "echarts") {
795
- log(` ${c.yellow} # Download echarts.min.js → https://echarts.apache.org/en/download.html${c.reset}`);
796
- log(` ${c.yellow} # Copy it into src/ (replacing the placeholder)${c.reset}`);
1118
+ if (extraDeps.length) {
1119
+ log("");
1120
+ log(` ${c.yellow} ⚠️ External dependencies manual metadata.xml step required:${c.reset}`);
1121
+ extraDeps.forEach(dep => {
1122
+ log(` ${c.yellow} • Add <FileResource type="JS" file="${dep}.js" isDevelopment="false" isRuntime="true"/> to metadata.xml${c.reset}`);
1123
+ });
1124
+ log(` ${c.dim} See README.md → "External Dependencies" for the full guide.${c.reset}`);
797
1125
  }
1126
+ log("");
798
1127
  log(` ${c.cyan} npm run build${c.reset} ${c.dim}# dev build${c.reset}`);
799
1128
  log(` ${c.cyan} npm run buildProduction${c.reset} ${c.dim}# minified build + zip${c.reset}`);
800
1129
  log(` ${c.cyan} npm run upload${c.reset} ${c.dim}# build + push to ThingWorx${c.reset}`);
801
1130
  log("");
802
1131
  }
803
1132
 
804
- main().catch(e => { err(String(e)); process.exit(1); });
1133
+ main().catch(e => { fail(String(e)); process.exit(1); });