create-thingworx-widget2 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # create-thingworx-widget
2
+
3
+ Scaffold a production-ready **ThingWorx custom widget** project in seconds — just like `npm create vite@latest`.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npm create thingworx-widget2@latest
9
+ # or
10
+ npx create-thingworx-widget2
11
+ ```
12
+
13
+ The CLI will ask you a few questions (widget name, description, ThingWorx server, chart library, etc.) and generate a complete project folder.
14
+
15
+ ## What gets generated
16
+
17
+ ```
18
+ MyWidget/
19
+ ├── src/
20
+ │ ├── MyWidget.ide.js ← Composer (IDE) behaviour & property definitions
21
+ │ ├── MyWidget.runtime.js ← Mashup runtime rendering & data handling
22
+ │ ├── MyWidget.ide.css ← Composer styles
23
+ │ ├── MyWidget.runtime.css ← Runtime styles
24
+ │ └── echarts.min.js ← (placeholder if ECharts selected)
25
+ ├── build/ ← Gulp output (gitignored)
26
+ ├── zip/ ← Extension .zip ready for TWX import
27
+ ├── metadata.xml ← ThingWorx extension manifest
28
+ ├── gulpfile.js ← Build pipeline (clean → copy → babel → minify → zip)
29
+ ├── package.json
30
+ ├── .gitignore
31
+ └── README.md
32
+ ```
33
+
34
+ ## Generated npm scripts
35
+
36
+ | Command | Description |
37
+ |---|---|
38
+ | `npm run build` | Dev build (unminified) → `zip/` |
39
+ | `npm run buildProduction` | Production build (concat + minify) → `zip/` |
40
+ | `npm run upload` | Dev build + upload to ThingWorx |
41
+ | `npm run uploadProduction` | Production build + upload to ThingWorx |
42
+
43
+ ## Publishing to npm
44
+
45
+ ```bash
46
+ npm login
47
+ npm publish --access public
48
+ ```
49
+
50
+ Once published, anyone can run:
51
+ ```bash
52
+ npm create thingworx-widget@latest
53
+ ```
54
+
55
+ ## Local development / testing
56
+
57
+ ```bash
58
+ # Inside this package folder
59
+ node bin/create-thingworx-widget.js
60
+
61
+ # Or link globally
62
+ npm link
63
+ create-thingworx-widget
64
+ ```
65
+
66
+ ## How the build pipeline works
67
+
68
+ The Gulp pipeline (`gulpfile.js`) mirrors the proven pattern from the LinearGradientWidget:
69
+
70
+ 1. **`cleanBuildDir`** — wipes `build/`, `zip/`, `lib/`
71
+ 2. **`copy`** — copies `src/**` → `build/ui/<PackageName>/` and `metadata.xml` → `build/`
72
+ 3. **`prepareBuild`**
73
+ - Babel transpiles all JS (removes ES module `import`/`export` for TWX compatibility)
74
+ - In `--p` (production) mode: reads `metadata.xml`, groups JS files by IDE/Runtime, concatenates + minifies with Terser, updates `metadata.xml` references
75
+ - Zips `build/` → `zip/<PackageName>-dev|min-<version>.zip`
76
+ 4. **`upload`** (optional) — POSTs the zip to ThingWorx via the Extension Package Uploader servlet
@@ -0,0 +1,804 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+ const readline = require("readline");
8
+
9
+ // ─── ANSI colours ────────────────────────────────────────────────────────────
10
+ 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",
19
+ };
20
+
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}`);
26
+
27
+ // ─── Banner ───────────────────────────────────────────────────────────────────
28
+ 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
+ });
49
+ }
50
+
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());
65
+ }
66
+
67
+ // ─── File writers (boilerplate matching LinearGradient project) ───────────────
68
+
69
+ function writePackageJson(root, cfg) {
70
+ 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,
76
+ thingworxServer: cfg.twxServer,
77
+ thingworxUser: cfg.twxUser,
78
+ thingworxPassword: cfg.twxPassword,
79
+ author: cfg.author,
80
+ minimumThingWorxVersion: cfg.minTwxVersion,
81
+ homepage: "",
82
+ autoUpdate: { gitHubURL: "" },
83
+ repository: { type: "git", url: "" },
84
+ 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",
108
+ },
109
+ };
110
+ fs.writeFileSync(path.join(root, "package.json"), JSON.stringify(content, null, 4));
111
+ }
112
+
113
+ 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
+ : "";
123
+
124
+ const content = `<?xml version="1.0" encoding="UTF-8"?>
125
+ <Entities>
126
+ <ExtensionPackages>
127
+ <ExtensionPackage
128
+ name="${wName}"
129
+ description="${cfg.description}"
130
+ vendor="${cfg.author}"
131
+ packageVersion="${cfg.version}"
132
+ minimumThingWorxVersion="${cfg.minTwxVersion}"
133
+ buildNumber=""/>
134
+ </ExtensionPackages>
135
+
136
+ <Widgets>
137
+ <Widget name="${wName}">${libLine}
138
+ <UIResources>
139
+
140
+ <FileResource type="CSS" file="${cssIde}"
141
+ description="" isDevelopment="true" isRuntime="false"/>
142
+ <FileResource type="JS" file="${jsIde}"
143
+ description="" isDevelopment="true" isRuntime="false"/>
144
+
145
+ <FileResource type="CSS" file="${cssRuntime}"
146
+ description="" isDevelopment="false" isRuntime="true"/>
147
+ <FileResource type="JS" file="${jsRuntime}"
148
+ description="" isDevelopment="false" isRuntime="true"/>
149
+
150
+ </UIResources>
151
+ </Widget>
152
+ </Widgets>
153
+ </Entities>
154
+ `;
155
+ fs.writeFileSync(path.join(root, "metadata.xml"), content);
156
+ }
157
+
158
+ 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');
163
+ const deleteEmpty = require('delete-empty');
164
+
165
+ const { series, src, dest } = require('gulp');
166
+ const zip = require('gulp-zip');
167
+ const concat = require('gulp-concat');
168
+ const terser = require('gulp-terser');
169
+ const babel = require('gulp-babel');
170
+ const request = require('request');
171
+
172
+ const packageJson = require('./package.json');
173
+
174
+ /** Files to remove from the build. Add paths relative to src/ if needed. */
175
+ const removeFiles = [];
176
+
177
+ /**
178
+ * CLI args:
179
+ * --p Production build (concat + minify)
180
+ * --l Library mode (unsupported)
181
+ */
182
+ const args = (argList => {
183
+ let arg = {}, a, opt, thisOpt, curOpt;
184
+ for (a = 0; a < argList.length; a++) {
185
+ thisOpt = argList[a].trim();
186
+ opt = thisOpt.replace(/^\\-+/, '');
187
+ if (opt === thisOpt) { if (curOpt) arg[curOpt] = opt; curOpt = null; }
188
+ else { curOpt = opt; arg[curOpt] = true; }
189
+ }
190
+ return arg;
191
+ })(process.argv);
192
+
193
+ if (args.l) throw new Error('Argument --l is unsupported for this project.');
194
+
195
+ const outPath = \`build/ui/\${packageJson.packageName}\`;
196
+ const libPath = 'lib';
197
+ const packageKind = args.p ? 'min' : 'dev';
198
+ const zipName = \`\${packageJson.packageName}-\${packageKind}-\${packageJson.version}.zip\`;
199
+
200
+ /** Clean build, zip, and lib directories. */
201
+ async function cleanBuildDir(cb) {
202
+ await del('build');
203
+ await del('zip');
204
+ await del('lib');
205
+
206
+ const paths = outPath.split('/');
207
+ for (let i = 1; i < paths.length; i++) {
208
+ paths[i] = paths[i - 1] + '/' + paths[i];
209
+ }
210
+ for (const p of paths) fs.mkdirSync(p);
211
+
212
+ await del('zip/**');
213
+ fs.mkdirSync('zip');
214
+ cb();
215
+ }
216
+
217
+ /** Copy src files + metadata.xml into build. */
218
+ function copy(cb) {
219
+ src('src/**')
220
+ .pipe(dest(\`\${outPath}/\`))
221
+ .on('end', () => {
222
+ fs.copyFileSync('metadata.xml', 'build/metadata.xml');
223
+ cb();
224
+ });
225
+ }
226
+
227
+ /** Transpile, optionally minify, then zip. */
228
+ async function prepareBuild(cb) {
229
+ if (removeFiles.length) await del(removeFiles.map(f => \`\${outPath}/\${f}\`));
230
+
231
+ // Copy npm dependencies declared in package.json "dependencies"
232
+ for (const dep in (packageJson.dependencies || {})) {
233
+ const depPkg = require(\`./node_modules/\${dep}/package.json\`);
234
+ await new Promise(resolve =>
235
+ src(\`node_modules/\${dep}/\${depPkg.main}\`).pipe(dest(outPath)).on('end', resolve));
236
+ }
237
+
238
+ // Babel transpile (removes ES module import/export for TWX)
239
+ await new Promise(resolve => {
240
+ src(\`\${outPath}/**/*.js\`)
241
+ .pipe(babel({ plugins: ['remove-import-export'] }))
242
+ .pipe(dest(outPath))
243
+ .on('end', resolve);
244
+ });
245
+
246
+ if (args.p) {
247
+ const metadataFile = await new Promise(resolve =>
248
+ fs.readFile('build/metadata.xml', 'utf8', (e, d) => resolve(d)));
249
+ const metadataXML = await new Promise(resolve =>
250
+ xml2js.parseString(metadataFile, (e, r) => resolve(r)));
251
+
252
+ const fileResources = metadataXML.Entities.Widgets[0].Widget[0].UIResources[0].FileResource;
253
+
254
+ const fileGroups = [
255
+ { isDevelopment: true, isRuntime: true, extension: 'min' },
256
+ { isDevelopment: false, isRuntime: true, extension: 'runtime' },
257
+ { isDevelopment: true, isRuntime: false, extension: 'ide' },
258
+ ];
259
+
260
+ for (const group of fileGroups) {
261
+ group.files = fileResources.filter(r => {
262
+ const include =
263
+ (group.isDevelopment ? r.$.isDevelopment === 'true' : r.$.isDevelopment !== 'true') &&
264
+ (group.isRuntime ? r.$.isRuntime === 'true' : r.$.isRuntime !== 'true') &&
265
+ r.$.type === 'JS' && !!r.$.file;
266
+ if (include && !fs.existsSync(\`\${outPath}/\${r.$.file}\`)) {
267
+ console.warn(\`Skipping \${outPath}/\${r.$.file} — file not found.\`);
268
+ return false;
269
+ }
270
+ return include;
271
+ }).map(r => r.$.file);
272
+ }
273
+
274
+ metadataXML.Entities.Widgets[0].Widget[0].UIResources[0].FileResource =
275
+ fileResources.filter(r => r.$.type !== 'JS' || !r.$.file);
276
+
277
+ for (const group of fileGroups) {
278
+ if (!group.files.length) continue;
279
+ const name = \`\${packageJson.packageName}.\${group.extension}\`;
280
+ await new Promise(resolve => {
281
+ src(group.files.map(f => \`\${outPath}/\${f}\`))
282
+ .pipe(concat(\`\${name}.js\`))
283
+ .pipe(terser({ compress: true, mangle: true }))
284
+ .pipe(dest(outPath))
285
+ .on('end', resolve);
286
+ });
287
+ await del(group.files.map(f => \`\${outPath}/\${f}\`));
288
+ 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() }
292
+ });
293
+ }
294
+
295
+ metadataXML.Entities.ExtensionPackages[0].ExtensionPackage[0].$.packageVersion = packageJson.version;
296
+ metadataXML.Entities.ExtensionPackages[0].ExtensionPackage[0].$.buildNumber = JSON.stringify(packageJson.autoUpdate);
297
+
298
+ const builder = new xml2js.Builder();
299
+ await new Promise(resolve => fs.writeFile('build/metadata.xml', builder.buildObject(metadataXML), resolve));
300
+ await deleteEmpty(\`\${outPath}/\`);
301
+ }
302
+
303
+ const zipStream = src('build/**').pipe(zip(zipName)).pipe(dest('zip'));
304
+ await new Promise(resolve => zipStream.on('end', resolve));
305
+ cb();
306
+ }
307
+
308
+ /** Upload extension zip to a ThingWorx server (requires valid credentials in package.json). */
309
+ async function upload(cb) {
310
+ const host = packageJson.thingworxServer;
311
+ const user = packageJson.thingworxUser;
312
+ const password = packageJson.thingworxPassword;
313
+
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);
336
+
337
+ if (err) console.error('Failed to delete previous widget version:', err);
338
+ }).auth(user, password);
339
+ });
340
+ }
341
+
342
+ exports.default = series(cleanBuildDir, copy, prepareBuild);
343
+ exports.upload = series(cleanBuildDir, copy, prepareBuild, upload);
344
+ `;
345
+ fs.writeFileSync(path.join(root, "gulpfile.js"), content);
346
+ }
347
+
348
+ 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
+ : "";
356
+
357
+ const content = `/* ── ${wName} — IDE (Composer) ── */
358
+
359
+ TW.IDE.Widgets.${wName} = function () {
360
+
361
+ this.widgetIconUrl = function () {
362
+ return "../Common/extensions/${wName}/ui/${wName}/icon.png";
363
+ };
364
+
365
+ this.widgetProperties = function () {
366
+ return {
367
+ name: "${cfg.moduleName}",
368
+ description: "${cfg.description}",
369
+ category: ["Common"],
370
+ properties: {
371
+
372
+ // ── Data binding ──────────────────────────────────────────
373
+ Data: {
374
+ description: "Infotable data source.",
375
+ baseType: "INFOTABLE",
376
+ isBindingTarget: true,
377
+ isEditable: false,
378
+ },
379
+
380
+ // ── Dimensions ────────────────────────────────────────────
381
+ Width: {
382
+ description: "Widget width in pixels.",
383
+ baseType: "NUMBER",
384
+ defaultValue: 600,
385
+ },
386
+ Height: {
387
+ description: "Widget height in pixels.",
388
+ baseType: "NUMBER",
389
+ defaultValue: 400,
390
+ },
391
+
392
+ // ── Display options ───────────────────────────────────────
393
+ Title: {
394
+ description: "Chart title displayed above the widget.",
395
+ baseType: "STRING",
396
+ defaultValue: "${cfg.moduleName}",
397
+ isBindingTarget: true,
398
+ isEditable: true,
399
+ },
400
+ ShowLegend: {
401
+ description: "Show the chart legend.",
402
+ baseType: "BOOLEAN",
403
+ defaultValue: true,
404
+ },
405
+ Theme: {
406
+ description: "Widget colour theme.",
407
+ baseType: "STRING",
408
+ defaultValue: "light",
409
+ },
410
+ }
411
+ };
412
+ };
413
+
414
+ this.widgetServices = function () {
415
+ return {
416
+ Refresh: { description: "Re-renders the widget with current data." },
417
+ };
418
+ };
419
+
420
+ this.widgetEvents = function () {
421
+ return {
422
+ SelectionChanged: { description: "Fired when a data point is selected." },
423
+ };
424
+ };
425
+ ${libComment}
426
+ this.renderHtml = function () {
427
+ return [
428
+ '<div class="widget-content ${wName.toLowerCase()}-ide-wrapper">',
429
+ ' <span class="${wName.toLowerCase()}-ide-label">📊 ${cfg.moduleName}</span>',
430
+ '</div>',
431
+ ].join('');
432
+ };
433
+
434
+ this.afterRender = function () { /* IDE after-render hook */ };
435
+ this.beforeSave = function () { /* validate before Composer save */ };
436
+ this.beforeDestroy= function () { /* clean up IDE resources */ };
437
+ };
438
+ `;
439
+ fs.writeFileSync(path.join(root, "src", `${wClass}.ide.js`), content);
440
+ }
441
+
442
+ 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];`;
449
+
450
+ const content = `/* ── ${wName} — Runtime (Mashup) ── */
451
+
452
+ TW.Runtime.Widgets.${wName} = function () {
453
+
454
+ var widget = this;
455
+ var chart = null;
456
+ var cachedData = null;
457
+
458
+ // ── Render shell HTML ─────────────────────────────────────────────────
459
+ this.renderHtml = function () {
460
+ return [
461
+ '<div class="widget-content ${wName.toLowerCase()}-wrapper">',
462
+ ' <div class="${wName.toLowerCase()}-toolbar">',
463
+ ' <span class="${wName.toLowerCase()}-title"></span>',
464
+ ' </div>',
465
+ ' <div class="${wName.toLowerCase()}-chart"></div>',
466
+ ' <div class="${wName.toLowerCase()}-error" style="display:none;"></div>',
467
+ '</div>',
468
+ ].join('');
469
+ };
470
+
471
+ // ── After render — attach events and initialise the chart ─────────────
472
+ this.afterRender = function () {
473
+ // Update title from property
474
+ widget.jqElement.find(".${wName.toLowerCase()}-title")
475
+ .text(widget.getProperty("Title") || "${cfg.moduleName}");
476
+
477
+ ${libInit}
478
+
479
+ _renderChart();
480
+ };
481
+
482
+ // ── Called by ThingWorx when a bound data service returns ─────────────
483
+ this.updateProperty = function (info) {
484
+ if (info.TargetProperty === "Data") {
485
+ cachedData = info.ActualDataRows || [];
486
+ _renderChart();
487
+ }
488
+ if (info.TargetProperty === "Title") {
489
+ widget.jqElement.find(".${wName.toLowerCase()}-title").text(info.SinglePropertyValue);
490
+ }
491
+ };
492
+
493
+ // ── Service invocations (e.g., Refresh) ───────────────────────────────
494
+ this.serviceInvoked = function (name) {
495
+ if (name === "Refresh") _renderChart();
496
+ };
497
+
498
+ // ── Clean up on destroy ───────────────────────────────────────────────
499
+ this.beforeDestroy = function () {
500
+ if (chart && typeof chart.dispose === "function") chart.dispose();
501
+ chart = null;
502
+ };
503
+
504
+ // ── Private: build / update the chart ────────────────────────────────
505
+ function _renderChart() {
506
+ if (!cachedData || !cachedData.length) {
507
+ _showError("No data bound. Connect a data service to the Data property.");
508
+ return;
509
+ }
510
+ _hideError();
511
+
512
+ // TODO: build your chart options from cachedData
513
+ // Example with ECharts:
514
+ // var option = { ... };
515
+ // if (chart) chart.setOption(option);
516
+ }
517
+
518
+ function _showError(msg) {
519
+ widget.jqElement.find(".${wName.toLowerCase()}-error")
520
+ .text(msg).show();
521
+ }
522
+
523
+ function _hideError() {
524
+ widget.jqElement.find(".${wName.toLowerCase()}-error").hide();
525
+ }
526
+ };
527
+ `;
528
+ fs.writeFileSync(path.join(root, "src", `${wClass}.runtime.js`), content);
529
+ }
530
+
531
+ function writeIdeCss(root, cfg) {
532
+ const wName = cfg.packageName.toLowerCase();
533
+ const content = `/* ── ${cfg.packageName} — IDE (Design Time) styles ── */
534
+
535
+ .${wName}-ide-wrapper {
536
+ width: 100%;
537
+ height: 100%;
538
+ display: flex;
539
+ align-items: center;
540
+ justify-content: center;
541
+ background-color: #f0f3f4;
542
+ border: 2px dashed #aab7b8;
543
+ box-sizing: border-box;
544
+ font-family: Arial, sans-serif;
545
+ color: #7f8c8d;
546
+ font-size: 14px;
547
+ border-radius: 4px;
548
+ }
549
+
550
+ .${wName}-ide-label {
551
+ font-weight: 600;
552
+ letter-spacing: 0.5px;
553
+ }
554
+ `;
555
+ fs.writeFileSync(path.join(root, "src", `${cfg.widgetClass}.ide.css`), content);
556
+ }
557
+
558
+ function writeRuntimeCss(root, cfg) {
559
+ const wName = cfg.packageName.toLowerCase();
560
+ const content = `/* ── ${cfg.packageName} — Runtime styles ── */
561
+
562
+ .${wName}-wrapper {
563
+ width: 100%;
564
+ height: 100%;
565
+ display: flex;
566
+ flex-direction: column;
567
+ position: relative;
568
+ box-sizing: border-box;
569
+ font-family: Arial, sans-serif;
570
+ background: #ffffff;
571
+ }
572
+
573
+ .${wName}-toolbar {
574
+ display: flex;
575
+ align-items: center;
576
+ gap: 8px;
577
+ padding: 6px 10px;
578
+ background: #f8f9fa;
579
+ border-bottom: 1px solid #e5e8e7;
580
+ flex-shrink: 0;
581
+ }
582
+
583
+ .${wName}-title {
584
+ font-size: 14px;
585
+ font-weight: 600;
586
+ color: #2c3e50;
587
+ }
588
+
589
+ .${wName}-chart {
590
+ flex: 1;
591
+ min-height: 0;
592
+ position: relative;
593
+ }
594
+
595
+ .${wName}-chart canvas {
596
+ width: 100% !important;
597
+ height: 100% !important;
598
+ }
599
+
600
+ .${wName}-error {
601
+ background: #fadbd8;
602
+ color: #c0392b;
603
+ padding: 8px 12px;
604
+ margin: 6px;
605
+ border-radius: 4px;
606
+ font-size: 12px;
607
+ text-align: center;
608
+ }
609
+ `;
610
+ fs.writeFileSync(path.join(root, "src", `${cfg.widgetClass}.runtime.css`), content);
611
+ }
612
+
613
+ function writeGitignore(root) {
614
+ fs.writeFileSync(path.join(root, ".gitignore"),
615
+ `node_modules/\nbuild/\nzip/\nlib/\n*.log\n.DS_Store\n`);
616
+ }
617
+
618
+ function writeReadme(root, cfg) {
619
+ const content = `# ${cfg.moduleName} — ThingWorx Custom Widget
620
+
621
+ > ${cfg.description}
622
+
623
+ ## Quick Start
624
+
625
+ \`\`\`bash
626
+ npm install
627
+ npm run build # development build → zip/
628
+ npm run buildProduction # minified build → zip/
629
+ \`\`\`
630
+
631
+ ## Project Structure
632
+
633
+ \`\`\`
634
+ ${cfg.packageName}/
635
+ ├── 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
645
+ \`\`\`
646
+
647
+ ## npm Scripts
648
+
649
+ | Command | Description |
650
+ |---|---|
651
+ | \`npm run build\` | Dev build (unminified) |
652
+ | \`npm run buildProduction\` | Production build (concat + minify) |
653
+ | \`npm run upload\` | Dev build + upload to TWX |
654
+ | \`npm run uploadProduction\` | Production build + upload to TWX |
655
+
656
+ ## Deploying to ThingWorx
657
+
658
+ 1. Run \`npm run buildProduction\`
659
+ 2. Go to ThingWorx Composer → **Import / Export → Import Extension**
660
+ 3. Upload the \`.zip\` file from the \`zip/\` folder
661
+
662
+ ## ThingWorx Server Config
663
+
664
+ Edit \`package.json\` and set:
665
+ - \`thingworxServer\` — e.g. \`http://localhost:8085\`
666
+ - \`thingworxUser\` / \`thingworxPassword\`
667
+ `;
668
+ fs.writeFileSync(path.join(root, "README.md"), content);
669
+ }
670
+
671
+ // ─── Main scaffold ────────────────────────────────────────────────────────────
672
+ async function scaffold(cfg) {
673
+ const root = path.resolve(process.cwd(), cfg.packageName);
674
+
675
+ if (fs.existsSync(root)) {
676
+ err(`Directory "${cfg.packageName}" already exists. Aborting.`);
677
+ process.exit(1);
678
+ }
679
+
680
+ fs.mkdirSync(path.join(root, "src"), { recursive: true });
681
+
682
+ writePackageJson(root, cfg);
683
+ writeMetadataXml(root, cfg);
684
+ writeGulpfile(root, cfg);
685
+ writeIdeJs(root, cfg);
686
+ writeRuntimeJs(root, cfg);
687
+ writeIdeCss(root, cfg);
688
+ writeRuntimeCss(root, cfg);
689
+ writeGitignore(root);
690
+ writeReadme(root, cfg);
691
+
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
+ }
700
+
701
+ return root;
702
+ }
703
+
704
+ // ─── Print file tree ──────────────────────────────────────────────────────────
705
+ function printTree(root, cfg) {
706
+ const rel = f => path.relative(root, f);
707
+ 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)"] : []),
714
+ ];
715
+
716
+ log("");
717
+ log(` ${c.bold}${cfg.packageName}/${c.reset}`);
718
+ files.forEach((f, i) => {
719
+ const last = i === files.length - 1;
720
+ const prefix = last ? "└──" : "├──";
721
+ log(` ${c.dim}${prefix}${c.reset} ${f}`);
722
+ });
723
+ log(` ${c.dim}└── node_modules/ (after npm install)${c.reset}`);
724
+ }
725
+
726
+ // ─── CLI entry ────────────────────────────────────────────────────────────────
727
+ async function main() {
728
+ banner();
729
+
730
+ // Support --name MyWidget for non-interactive / CI use
731
+ const argName = (process.argv.find(a => a.startsWith("--name=")) || "").replace("--name=","");
732
+
733
+ const rl = readline.createInterface({
734
+ input: process.stdin,
735
+ output: process.stdout,
736
+ terminal: process.stdin.isTTY, // disable echo in pipe mode
737
+ });
738
+
739
+ 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)"]);
754
+
755
+ rl.close();
756
+
757
+ const cfg = {
758
+ packageName,
759
+ moduleName,
760
+ description,
761
+ version,
762
+ author,
763
+ minTwxVersion: minTwxVer,
764
+ twxServer,
765
+ twxUser,
766
+ twxPassword,
767
+ library: library.startsWith("echarts") ? "echarts" : "none",
768
+ widgetClass: packageName, // used for file names: MyWidget.ide.js etc.
769
+ };
770
+
771
+ log("");
772
+ info(`Scaffolding ${c.bold}${packageName}${c.reset}...`);
773
+ log("");
774
+
775
+ const root = await scaffold(cfg);
776
+
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}`));
785
+
786
+ printTree(root, cfg);
787
+
788
+ log("");
789
+ log(` ${c.bold}${c.green}✨ Done!${c.reset} Your widget is ready.`);
790
+ log("");
791
+ log(` ${c.dim}Next steps:${c.reset}`);
792
+ log(` ${c.cyan} cd ${packageName}${c.reset}`);
793
+ 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}`);
797
+ }
798
+ log(` ${c.cyan} npm run build${c.reset} ${c.dim}# dev build${c.reset}`);
799
+ 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("");
802
+ }
803
+
804
+ main().catch(e => { err(String(e)); process.exit(1); });
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "create-thingworx-widget2",
3
+ "version": "1.0.1",
4
+ "description": "Scaffold a ThingWorx custom widget project — like npm create vite@latest but for ThingWorx.",
5
+ "keywords": [
6
+ "thingworx",
7
+ "widget",
8
+ "scaffold",
9
+ "gulp",
10
+ "echarts",
11
+ "create"
12
+ ],
13
+ "author": "mr-dark-codex",
14
+ "license": "MIT",
15
+ "bin": {
16
+ "create-thingworx-widget2": "bin/create-thingworx-widget.js"
17
+ },
18
+ "engines": {
19
+ "node": ">=14.0.0"
20
+ },
21
+ "files": [
22
+ "bin/"
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": ""
27
+ }
28
+ }