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