-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy pathtemplate.js
executable file
·524 lines (454 loc) · 17.2 KB
/
template.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
/* eslint-env node */
'use strict';
/* eslint-env node */
const path = require('path');
const parseVersion = require('./parseVersion');
const { existsSync, readFileSync, writeFileSync } = require('fs');
var superlatives = [
'whimsical',
'distinguished',
'meritorious',
'noteworthy',
'magnificent',
'superb',
'splendid',
'resplendent',
'sublime',
'renowned',
'remarkable',
'transcendent'
];
// Basic template description.
exports.description = "Create a new Niagara module";
// Template-specific notes to be displayed before question prompts.
exports.notes = "";
// warn if any files might be overwritten. build/ and node_modules/ are ok.
exports.warnOn = [
'**/*',
'!build/**',
'!node_modules/**'
];
////////////////////////////////////////////////////////////////
// Utility functions
////////////////////////////////////////////////////////////////
//find the index of the prompt using the property name it provides
function findPromptIndexByName(arr, name) {
for (var i = 0; i < arr.length; i++) {
if (arr[i].name === name) {
return i;
}
}
return -1;
}
function capitalizeFirstLetter(s) {
return s.substring(0, 1).toUpperCase() + s.substring(1);
}
function isValidNiagaraModuleName(name) {
return /^[A-Za-z][A-Za-z0-9_]*$/.test(name);
}
function isValidTypeSpec(name) {
return /^[A-Za-z][A-Za-z0-9_]*:[A-Z][A-Za-z0-9]*$/.test(name);
}
//remove properties from an object where the property name satisfies the filter
function filterOutProps(obj, filter) {
for (var i in obj) {
if (obj.hasOwnProperty(i) && filter(i)) {
delete obj[i];
}
}
}
//return a random superlative
function superlative() {
return superlatives.splice(Math.floor(Math.random() * superlatives.length), 1);
}
// Returns true if niagara-module.xml file exists (4.13+)
function hasNiagaraModuleFile() {
return existsSync('./niagara-module.xml');
}
function updateNiagaraModuleFile(name, preferredSymbol) {
const path = process.cwd() + '/niagara-module.xml';
if (existsSync(path)) {
let content = readFileSync(path, 'utf8');
const runtimeProfilesRegex = /runtimeProfiles="([^"]+)"/;
const match = content.match(runtimeProfilesRegex);
if (!match) {
throw new Error('Invalid niagara-module.xml does not contain runtimeProfiles');
}
let runtimeProfiles = match[1];
if (runtimeProfiles.match('ux')) {
return; //already there
}
content = content.replace(runtimeProfilesRegex, `runtimeProfiles="${ runtimeProfiles },ux"`);
writeFileSync(path, content);
} else {
const content = `<?xml version="1.0" encoding="UTF-8"?>
<niagara-module moduleName="${ name }" preferredSymbol="${ preferredSymbol }" runtimeProfiles="ux"/>`;
writeFileSync(path, content);
}
}
exports.template = function (grunt, init, done) {
let niagaraModuleName;
let preferredSymbol;
let allPrompts;
let currentNiagaraVersion;
const atOrLaterThan = (version) => {
if (!currentNiagaraVersion) {
throw new Error('cannot call before version prompt');
}
return currentNiagaraVersion.compareTo(version) >= 0;
}
const v46OrLater = () => atOrLaterThan('4.6.0');
const v49OrLater = () => atOrLaterThan('4.9.0');
const v410OrLater = () => atOrLaterThan('4.10.0');
const v410u10OrLater = () => atOrLaterThan('4.10.10');
const v411OrLater = () => atOrLaterThan('4.11.0');
const v413OrLater = () => atOrLaterThan('4.13.0');
/**
* Insert the given prompts after the prompt specified by name. They will
* not be inserted if they already exist.
*
* @private
* @inner
* @param {String} name
* @example
* insertPromptAfter('prompt1', prompt2, prompt3, prompt4);
*/
function insertPromptsAfter(name) {
var prompts = Array.prototype.slice.call(arguments, 1),
promptToInsert,
idx = findPromptIndexByName(allPrompts, name);
if (idx === -1) {
throw new Error('name ' + name + ' not found');
}
while (prompts.length) {
promptToInsert = prompts.splice(0, 1)[0]; //get first prompt
if (promptToInsert && allPrompts.indexOf(promptToInsert) === -1) {
allPrompts.splice(++idx, 0, promptToInsert);
}
}
}
////////////////////////////////////////////////////////////////
// Definitions of prompts
////////////////////////////////////////////////////////////////
const widgetNamePrompt = {
message: 'bajaux Widget name',
name: 'widgetName',
default: function (value, data, done) {
done(null, capitalizeFirstLetter(niagaraModuleName) + 'Widget');
},
validator: function (value, done) {
done(isValidNiagaraModuleName(value));
},
warning: 'Must be only letters, numbers, or underscores'
};
const formFactorPrompt = {
message: 'bajaux form factor (mini/compact/max)',
name: 'formFactor',
default: 'mini',
warning: 'This defines the desired form factor of your bajaux Widget. ' +
'For a field editor, choose mini. For a fullscreen view, choose max.'
};
const classNamePrompt = {
message: 'Fully qualified class name for your Widget',
name: 'fullClassName',
default: function (value, data, done) {
var author = data['author_name'].replace(/[^A-Za-z0-9]/g, '')
.toLowerCase();
done(null, `com.${ author }.${ niagaraModuleName.toLowerCase() }.ux.B${ data.widgetName }`);
},
validator: function (value, done) {
done(value.split('.').pop().charAt(0) === 'B');
},
warning: 'Class must be fully qualified and start with B'
};
/**
* If you choose to register as an agent on a type, we will generate you a
* Java class. This will add another prompt for the class name to use.
*/
const registerAgentPrompt = {
message: 'Register your Widget as an agent on a Type? (Leave blank for none)',
name: 'agentType',
validator: function (value, done) {
if (value === '') {
return done(true);
}
if (isValidTypeSpec(value)) {
insertPromptsAfter('agentType', classNamePrompt);
return done(true);
}
return done(false);
},
warning: 'Registering your Widget as an agent on a Type will mark it as an' +
'editor for that Type. Must be a valid Niagara type spec such as ' +
'baja:String.'
};
const niagaraModuleNamePrompt = {
message: 'Niagara module name',
name: 'name',
default: function (value, data, done) {
var name = path.basename(process.cwd())
.replace(/-/g, '_')
.replace(/[^A-Za-z0-9_]/g, '');
done(null, name);
},
validator: function (value, done) {
var valid = isValidNiagaraModuleName(value);
if (valid) {
niagaraModuleName = value;
}
done(valid);
},
warning: 'Must be only letters, numbers or underscores.'
};
const targetNiagaraVersionPrompt = {
message: 'What Niagara version will you build your module against?',
name: 'targetVersion',
default: '4.10.10',
validator: (value, done) => {
currentNiagaraVersion = parseVersion(value);
done(!!currentNiagaraVersion);
},
warning: 'Must be in major.minor.update format, e.g. "4.10.10" or "4.14.0".'
};
const preferredSymbolPrompt = {
message: 'Shortened preferred symbol for your Niagara module',
name: 'preferredSymbol',
default: function (value, data, done) {
var name = data.name;
done(null, name.replace(/[aeiou]|[^A-Za-z0-9]/g, ''));
},
validator: (value, done) => {
preferredSymbol = value;
done(!!preferredSymbol);
},
warning: 'Must be unique'
};
/**
* Decide if you want template files fleshed out with demo logic, or barebones
*/
const skeletonPrompt = {
message: 'Only generate skeleton files?',
name: 'skeleton',
default: 'y/N',
warning: 'y: Your widget files will be the bare ' +
'minimum structure of a bajaux widget. N: Your widget files will contain ' +
'demo logic to examine and modify. If this is your first time using ' +
'grunt-init-niagara or bajaux, choose N.'
};
/**
* If you create a bajaux Widget, adds prompts for the name of the Widget and
* whether you want to register it as an agent.
*/
const bajauxPrompt = {
message: 'Would you like to create a bajaux Widget?',
name: 'bajaux',
default: 'y/N',
validator: function (value, done) {
//this logic should be in before(), but grunt-init uses an old version of
//the 'prompt' library
if (value.toLowerCase() === 'y') {
//we're creating a bajaux widget. prompt for a couple more bajaux things
insertPromptsAfter('bajaux', widgetNamePrompt, formFactorPrompt,
registerAgentPrompt, skeletonPrompt, v410OrLater() && jsxPrompt, lessPrompt);
}
done();
}
};
const jsxPrompt = {
message: 'Would you like to use JSX to create your Widget?',
name: 'jsx',
default: 'y/N',
warning: 'New bajaux APIs in Niagara 4.10 allow the usage of JSX when ' +
'creating your Widgets. These APIs are in _Development_ status. They are ' +
'Niagara-specific and they are not the same thing as React. See the ' +
'bajaux documentation for full details.'
};
const lessPrompt = {
message: 'Would you like to use LESS to generate CSS?',
name: 'less',
default: 'y/N',
warning: 'LESS is a style sheet language that can be compiled to CSS. Variables and ' +
'mixins are some of the powerful features that make LESS more dynamic. More information ' +
'about LESS and its features can be found here \'https://lesscss.org/\'.'
};
const authorPrompt = {
message: 'Name of author or organization',
name: 'author_name',
default: process.env.USER || process.env.USERNAME || 'Me',
warning: 'The author name will be used to generate copyright notices and ' +
'the module\'s vendor name.'
};
allPrompts = [
niagaraModuleNamePrompt,
targetNiagaraVersionPrompt,
preferredSymbolPrompt,
{ name: 'description', message: 'Description of your Niagara module' },
authorPrompt,
bajauxPrompt, //can add more prompts depending on answers
init.prompt('version'),
init.prompt('homepage'),
init.prompt('bugs'),
init.prompt('author_email'),
init.prompt('node_version', '>= 10.22.0'),
init.prompt('npm_test', 'grunt ci')
];
////////////////////////////////////////////////////////////////
// Process user responses to prompts and copy in our files
////////////////////////////////////////////////////////////////
init.process({}, allPrompts, function (err, props) {
//is the file used for a bajaux widget?
function isWidgetFile(file) {
return file.indexOf('Widget') > 0;
}
//is the file used for agent registration of a bajaux widget?
function isAgentFile(file) {
return file.match(/\.java$/) || file.match(/module-include\.xml$/);
}
function isLessFile(file) {
return file.match(props.name + '.less');
}
function isCssFile(file) {
return file.match(props.name + '.css');
}
function isCssResourceFile(file) {
return file.match('CssResource');
}
function isRunFile(file) {
return file.match('.run.js');
}
function isNonSkeleton(file) {
return file.match('.htm') ||
file.match('.css') ||
file.match('.less') ||
file.match(props.name + '\\.js$') ||
file.match(props.name + 'Spec\\.js$') ||
file.match('.hbs');
}
if (err) { throw err; }
//fix/tweak our properties (to be used by templates)
props.keywords = [];
props.year = new Date().getFullYear();
props.devDependencies = {
"grunt": "~1.0.1",
"grunt-niagara": "^2.1.0",
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"babel-plugin-istanbul": "^4.1.3"
};
if (props.jsx) {
props.devDependencies['@babel/plugin-transform-react-jsx'] = '^7.10.0';
props.devDependencies['eslint-plugin-react'] = '^7.20.0';
} else {
props.jsx = false;
}
if (props.less) {
props.devDependencies['grunt-contrib-less'] = '^2.0.0';
}
props.isFirstParty = props.author_name.toLowerCase() === 'tridium';
props.isThirdParty = !props.isFirstParty;
if ((v410OrLater() && props.isFirstParty) || (v413OrLater() && props.isThirdParty)) {
props.gradleVersion = '7';
} else {
props.gradleVersion = '4';
}
props.gradleFile = props.name + '-ux.gradle';
props.fe = String(props.formFactor).toLowerCase() === 'mini';
props.widgetClass = props.fe ? 'BaseEditor' : 'Widget';
props.widgetModule = props.fe ? 'nmodule/webEditors/rc/fe/baja/BaseEditor' : 'bajaux/Widget';
switch (String(props.formFactor).toLowerCase()) {
case 'mini': props.widgetInterface = 'BIFormFactorMini'; break;
case 'compact': props.widgetInterface = 'BIFormFactorCompact'; break;
case 'max': props.widgetInterface = 'BIFormFactorMax'; break;
}
props.fullClassName = props.fullClassName || '';
var split = props.fullClassName.split('.');
props.className = split.pop();
props.package = split.join('.');
props.superlative = superlative;
props.bajaux = String(props.bajaux).toLowerCase() === 'y';
props.less = String(props.less).toLowerCase() === 'y';
props.skeleton = !props.bajaux || String(props.skeleton).toLowerCase() === 'y';
props.moduleName = niagaraModuleName;
props.preferredSymbol = preferredSymbol;
props.jsBuildName = capitalizeFirstLetter(props.moduleName) + 'JsBuild';
props.useCssResource = props.less && v413OrLater();
props.cssResourceName = capitalizeFirstLetter(props.moduleName) + 'CssResource';
props.widgetName = props.widgetName === undefined ? 'NotAWidget' : props.widgetName;
props.jqueryVersion = v411OrLater() || v410u10OrLater() ? '' : (v49OrLater() ? '-3.4.1' : '-3.2.0');
props.handlebarsFilename = v49OrLater() ? 'handlebars' : 'handlebars-v4.0.6';
props.hasLogJs = v46OrLater();
props.hasGruntPlugin = v46OrLater();
props.supportsPluginsBlock = props.isFirstParty;
props.supportsVendor = v46OrLater();
props.newWidgetConstructor = v410OrLater();
props.coreModulePlugin = props.isFirstParty && v49OrLater() ? 'com.tridium.convention.core-module' : 'com.tridium.niagara-module';
props.addJqueryShim = v411OrLater() || v410u10OrLater();
props.hasNiagaraModule = v413OrLater() && hasNiagaraModuleFile();
props.addMoment = v410OrLater();
var files = init.filesToCopy(props);
if (props.less) {
filterOutProps(files, isCssFile);
} else {
filterOutProps(files, isLessFile);
}
if (!props.bajaux) {
//we're not making a bajaux widget, so don't copy any bajaux-specific files.
filterOutProps(files, function (file) {
return (isWidgetFile(file) || isAgentFile(file));
});
} else if (!props.agentType) {
//we're making a widget but not registering it as an agent, so don't copy
//Java class files etc.
filterOutProps(files, isAgentFile);
}
if (props.bajaux && props.skeleton) {
filterOutProps(files, isNonSkeleton);
}
if (!props.useCssResource) {
filterOutProps(files, isCssResourceFile);
}
if (props.isFirstParty) {
filterOutProps(files, isRunFile);
}
// filter out .gradle/.kts file based on gradle version
if (props.gradleVersion === '7') {
filterOutProps(files, (file) => file.match(/.*-ux\.gradle$/));
} else {
filterOutProps(files, (file) => file.match(/.*-ux\.gradle\.kts$/));
}
init.copyAndProcess(files, props, {
//processing a binary BOG file will destroy it
noProcess: 'name-ux/srcTest/rc/stations/**'
});
init.writePackageJSON(props.name + '-ux/package.json', props);
// Template-specific notes to be displayed after question prompts.
exports.after =
'You should now cd into your new ' + props.name + '-ux directory and ' +
'install project dependencies with _npm install_. ' +
'After that, you may execute project tasks with _grunt_. ' +
'For more information about installing and configuring Grunt, please ' +
'see the Getting Started guide:' +
'\n\n' +
'http://gruntjs.com/getting-started' +
'\n\n' +
'Build the Niagara module with Gradle by changing to your Niagara User ' +
'Home and typing: ' +
('_gradlew :' + props.name + '-ux:build_');
if (props.bajaux && !props.skeleton) {
exports.after += '\n\n' +
'If you like, you can start up a station and visit the following URL ' +
'for a simple demo:' +
'\n\n' +
'http://localhost/module/' + props.name + '/rc/' + props.name + '.htm';
}
if (v413OrLater()) {
try {
updateNiagaraModuleFile(props.name, props.preferredSymbol);
} catch (e) {
console.error('Could not update niagara-module.xml! Please verify it manually.');
throw e;
}
}
done();
});
};