create-thingworx-widget2 1.0.1 → 1.0.2

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