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