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 +76 -0
- package/bin/create-thingworx-widget.js +804 -0
- package/package.json +28 -0
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
|
+
}
|