@ixon-cdk/core 1.0.0 → 1.1.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,9 +6,9 @@ const { getRootDir, logFileCrudMessage, logErrorMessage } = require('../utils');
6
6
  module.exports = class ConfigService {
7
7
  _config = { components: {} };
8
8
 
9
- _path = path.join(getRootDir(), 'config.json');
9
+ constructor(configFile = 'config.json') {
10
+ this._path = path.join(getRootDir(), configFile);
10
11
 
11
- constructor() {
12
12
  let config;
13
13
 
14
14
  if (!fs.existsSync(this._path)) {
@@ -19,7 +19,7 @@ module.exports = class ConfigService {
19
19
  try {
20
20
  config = require(this._path);
21
21
  } catch {
22
- logErrorMessage('Couldn\'t parse config file.');
22
+ logErrorMessage("Couldn't parse config file.");
23
23
  process.exit();
24
24
  }
25
25
 
@@ -60,11 +60,7 @@ module.exports = class ConfigService {
60
60
  }
61
61
 
62
62
  extendComponent(name, config) {
63
- this._config.components[name] = merge(
64
- {},
65
- this._config.components[name],
66
- config,
67
- );
63
+ this._config.components[name] = merge({}, this._config.components[name], config);
68
64
  this._sync();
69
65
  }
70
66
 
package/package.json CHANGED
@@ -1,24 +1,24 @@
1
1
  {
2
2
  "name": "@ixon-cdk/core",
3
- "version": "1.0.0",
3
+ "version": "1.1.0-next.1",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "author": "",
7
7
  "license": "ISC",
8
8
  "dependencies": {
9
9
  "app-root-dir": "^1.0.2",
10
- "archiver": "^5.3.0",
11
- "axios": "^0.21.4",
10
+ "archiver": "^5.3.1",
11
+ "axios": "^0.26.1",
12
12
  "chalk": "^4.1.2",
13
- "chokidar": "^3.5.2",
13
+ "chokidar": "^3.5.3",
14
14
  "crypto-js": "^4.1.1",
15
- "express": "^4.17.1",
16
- "glob": "^7.2.0",
15
+ "express": "^4.17.3",
16
+ "glob": "^8.0.1",
17
17
  "livereload": "^0.9.3",
18
18
  "lodash": "^4.17.21",
19
19
  "opener": "^1.5.2",
20
- "prompts": "^2.4.1",
20
+ "prompts": "^2.4.2",
21
21
  "rimraf": "^3.0.2",
22
- "yargs": "^17.1.1"
22
+ "yargs": "^17.4.1"
23
23
  }
24
24
  }
package/server/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  const path = require('path');
2
2
  const express = require('express');
3
+ const liveReload = require('livereload');
3
4
  const ConfigService = require('../config/config.service');
4
5
 
6
+ const LR_PORT_DEFAULT = 35729;
7
+
5
8
  module.exports = class Server {
6
9
  constructor(opts) {
7
10
  this._configSrv = new ConfigService();
@@ -25,10 +28,7 @@ module.exports = class Server {
25
28
  // CORS
26
29
  app.use((_, res, next) => {
27
30
  res.header('Access-Control-Allow-Origin', '*');
28
- res.header(
29
- 'Access-Control-Allow-Headers',
30
- 'Origin, X-Requested-With, Content-Type, Accept',
31
- );
31
+ res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
32
32
  next();
33
33
  });
34
34
 
@@ -38,16 +38,15 @@ module.exports = class Server {
38
38
  const outputDir = this._getOutputDir(name);
39
39
  if (outputDir) {
40
40
  const dir = path.resolve(this._rootDir, outputDir);
41
- app.use(
42
- `/${this._opts.componentBasePath}/${name}`,
43
- express.static(dir),
44
- );
41
+ app.use(`/${this._opts.componentBasePath}/${name}`, express.static(dir));
45
42
  }
46
43
  });
47
44
  }
48
45
 
49
46
  // Adds live reload watchers
50
- const lrServer = require('livereload').createServer();
47
+ const lrServer = liveReload.createServer({
48
+ port: this._opts.liveReloadPort,
49
+ });
51
50
  const _refresh = () => lrServer.refresh('/');
52
51
  const _debouncedRefresh = require('lodash/debounce')(_refresh, 250);
53
52
  names
@@ -55,7 +54,9 @@ module.exports = class Server {
55
54
  [path.join(this._rootDir, this._getOutput(name)), 100],
56
55
  [path.join(this._rootDir, `dist/${name}/manifest.json`), 500],
57
56
  ])
58
- .forEach(([filename, interval]) => require('fs').watchFile(filename, { interval }, _debouncedRefresh));
57
+ .forEach(([filename, interval]) =>
58
+ require('fs').watchFile(filename, { interval }, _debouncedRefresh),
59
+ );
59
60
 
60
61
  // Simulator app
61
62
  const appDir = path.dirname(require.resolve('@ixon-cdk/simulator'));
@@ -76,17 +77,27 @@ module.exports = class Server {
76
77
  res.status(200).sendFile('/index.html', { root: appDir });
77
78
  });
78
79
 
79
- app.listen(this._opts.port);
80
+ this.server = app.listen(this._opts.port);
80
81
  }
81
82
 
82
83
  openSimulator(name = null) {
83
84
  const url = this._getComponentUrl(name);
84
- const queryString = url ? `?pct-url=${encodeURIComponent(url)}` : '';
85
- require('opener')(`${this._getBaseUrl()}/${queryString}`);
85
+ const lrPort = this._opts.liveReloadPort;
86
+ const params = new URLSearchParams();
87
+ if (url) {
88
+ params.set('pct-url', url);
89
+ }
90
+ if (lrPort !== LR_PORT_DEFAULT) {
91
+ params.set('lr-port', lrPort);
92
+ }
93
+ const queryString = params.toString();
94
+ const openUrl = `${this._getBaseUrl()}/?${queryString}`;
95
+ require('opener')(openUrl);
86
96
  }
87
97
 
88
98
  _getBaseUrl() {
89
- return `http://localhost:${this._opts.port}`;
99
+ const address = this.server.address();
100
+ return `http://localhost:${address.port}`;
90
101
  }
91
102
 
92
103
  _getComponentUrl(name) {
@@ -3,8 +3,10 @@ const path = require('path');
3
3
  const prompts = require('prompts');
4
4
 
5
5
  const {
6
+ getFiles,
6
7
  getRootDir,
7
8
  ensureModule,
9
+ logErrorMessage,
8
10
  logFileCrudMessage,
9
11
  pascalCase,
10
12
  } = require('../utils');
@@ -13,12 +15,131 @@ const ConfigService = require('../config/config.service');
13
15
  module.exports = class TemplateService {
14
16
  _configSrv = new ConfigService();
15
17
 
18
+ _rootDir = getRootDir();
19
+
16
20
  _schemas = {};
17
21
 
18
22
  constructor() {
19
23
  this._discover();
20
24
  }
21
25
 
26
+ async addFromExample(componentName, componentPrefix) {
27
+ const examplesRoot = path.join(this._rootDir, 'node_modules/component-examples');
28
+
29
+ if (!fs.existsSync(examplesRoot)) {
30
+ logErrorMessage("Package 'ixoncloud/component-examples' is not installed.");
31
+ process.exit();
32
+ }
33
+
34
+ const examplesConfigSrv = new ConfigService('node_modules/component-examples/config.json');
35
+ const { components } = examplesConfigSrv._config;
36
+
37
+ if (!Object.keys(components).length) {
38
+ logErrorMessage('No examples found.');
39
+ process.exit();
40
+ }
41
+
42
+ const exampleComponentRoot = examplesConfigSrv.getNewComponentRoot();
43
+ const examplePrefix = examplesConfigSrv.getPrefix();
44
+
45
+ const result = await prompts({
46
+ type: 'select',
47
+ name: 'name',
48
+ message: 'Pick a template',
49
+ choices: Object.keys(components).map((name) => ({ title: name, value: name })),
50
+ initial: 0,
51
+ });
52
+ const exampleName = result.name;
53
+
54
+ if (!exampleName) {
55
+ process.exit();
56
+ } else if (!components[exampleName]) {
57
+ logErrorMessage(`Example '${exampleName}' is not configured.`);
58
+ process.exit();
59
+ }
60
+
61
+ const { build } = components[exampleName].runner;
62
+ const { builder } = build;
63
+ const isVue = builder.startsWith('@ixon-cdk/vue-builder');
64
+ const isStatic = builder.startsWith('@ixon-cdk/static-builder');
65
+ const isSvelte = builder.startsWith('@ixon-cdk/svelte-builder');
66
+
67
+ // find source files, read and replace.
68
+ const exampleDir = path.join(examplesRoot, exampleComponentRoot, exampleName);
69
+ const inputFilePath = path.join(exampleDir, build.input);
70
+ let inputFileContents = fs.readFileSync(inputFilePath, { encoding: 'utf-8' });
71
+
72
+ if (isVue) {
73
+ inputFileContents = this._findAndReplaceVueInputFile(inputFileContents, componentName);
74
+ } else if (isStatic) {
75
+ inputFileContents = this._findAndReplaceStaticInputFile(
76
+ inputFileContents,
77
+ examplePrefix,
78
+ exampleName,
79
+ componentPrefix,
80
+ componentName,
81
+ );
82
+ }
83
+
84
+ // find manifest file, read and replace.
85
+ const manifestFilePath = path.join(path.dirname(inputFilePath), 'manifest.json');
86
+ const manifestFileContents = JSON.parse(fs.readFileSync(manifestFilePath));
87
+ manifestFileContents.main = `${this._getTag(componentPrefix, componentName)}.min.js`;
88
+ manifestFileContents.version = '1';
89
+
90
+ let input;
91
+ if (isVue) {
92
+ input = `${componentName}.vue`;
93
+ } else if (isStatic) {
94
+ input = `${componentName}.js`;
95
+ } else if (isSvelte) {
96
+ input = `${componentName}.svelte`;
97
+ }
98
+
99
+ const files = await getFiles(exampleDir).catch(logErrorMessage);
100
+
101
+ // find and install any missing dependencies.
102
+ files.forEach((file) => {
103
+ if (/\.(jsx?|tsx?|svelte|vue)$/i.test(file)) {
104
+ const fileContents = fs.readFileSync(file);
105
+ const { dependencies } = require(path.join(examplesRoot, 'package.json'));
106
+ this._checkDependencies(fileContents, dependencies || {});
107
+ }
108
+ });
109
+
110
+ // else start copying input, and menifest files
111
+ const componentRoot = this._configSrv.getNewComponentRoot();
112
+ const dir = path.join(this._rootDir, componentRoot, componentName);
113
+ const _logCreatedInDir = (fileName) => {
114
+ logFileCrudMessage('CREATE', path.join(componentRoot, componentName, fileName));
115
+ };
116
+ fs.mkdirSync(dir, { recursive: true });
117
+
118
+ files
119
+ .map((file) => file.slice(exampleDir.length + 1))
120
+ .forEach((file) => {
121
+ // create directory if it doesn't exist.
122
+ const _dir = path.dirname(path.join(dir, file));
123
+ if (!fs.existsSync(_dir)) {
124
+ fs.mkdirSync(_dir, { recursive: true });
125
+ }
126
+ // for the input-file and manifest use the replaced file contents.
127
+ if (file === build.input) {
128
+ fs.writeFileSync(path.join(dir, input), inputFileContents, { encoding: 'utf-8' });
129
+ _logCreatedInDir(input);
130
+ } else if (file === 'manifest.json') {
131
+ const json = `${JSON.stringify(manifestFileContents, null, 2)}\n`;
132
+ fs.writeFileSync(path.join(dir, file), json, { encoding: 'utf-8' });
133
+ _logCreatedInDir(file);
134
+ } else {
135
+ fs.copyFileSync(path.join(exampleDir, file), path.join(dir, file));
136
+ _logCreatedInDir(file);
137
+ }
138
+ });
139
+
140
+ return Promise.resolve({ config: { runner: { build: { builder, input } } } });
141
+ }
142
+
22
143
  async add(componentName, componentPrefix) {
23
144
  let schema;
24
145
 
@@ -33,16 +154,12 @@ module.exports = class TemplateService {
33
154
  initial: 0,
34
155
  });
35
156
 
36
- const tag = componentPrefix
37
- ? `${componentPrefix}-${componentName}`
38
- : componentName;
39
-
40
157
  if (result) {
41
158
  schema = this._schemas[result.templateName];
42
159
  }
43
160
 
44
161
  if (schema) {
45
- const context = { name: componentName, tag };
162
+ const context = { name: componentName, tag: this._getTag(componentPrefix, componentName) };
46
163
  let variantIdx = null;
47
164
 
48
165
  if (schema.variants) {
@@ -65,31 +182,28 @@ module.exports = class TemplateService {
65
182
  schema = this._interpolateSchema(schema, context);
66
183
 
67
184
  if (schema.config.runner) {
68
- const modulesNames = Object.keys(schema.config.runner).reduce(
69
- (names, cmd) => {
70
- if (schema.config.runner[cmd].builder) {
71
- const name = schema.config.runner[cmd].builder.split(':')[0];
72
- if (!names.includes(name)) {
73
- return [...names, name];
74
- }
185
+ const modulesNames = Object.keys(schema.config.runner).reduce((names, cmd) => {
186
+ if (schema.config.runner[cmd].builder) {
187
+ const name = schema.config.runner[cmd].builder.split(':')[0];
188
+ if (!names.includes(name)) {
189
+ return [...names, name];
75
190
  }
76
- return names;
77
- },
78
- [],
79
- );
191
+ }
192
+ return names;
193
+ }, []);
80
194
  modulesNames.forEach((name) => ensureModule(name));
81
195
  }
82
196
 
83
- const root = this._configSrv.getNewComponentRoot();
197
+ const componentRoot = this._configSrv.getNewComponentRoot();
84
198
 
85
199
  schema.files.forEach((file) => {
86
- file.dest = `${root}/${componentName}/${file.dest}`;
200
+ file.dest = `${componentRoot}/${componentName}/${file.dest}`;
87
201
  this.createFile(file, context);
88
202
  });
89
203
 
90
204
  if (variantIdx !== null) {
91
205
  schema.variants[variantIdx].files.forEach((file) => {
92
- file.dest = `${root}/${componentName}/${file.dest}`;
206
+ file.dest = `${componentRoot}/${componentName}/${file.dest}`;
93
207
  this.createFile(file, context);
94
208
  });
95
209
  }
@@ -99,34 +213,100 @@ module.exports = class TemplateService {
99
213
  }
100
214
 
101
215
  createFile(file, ctx) {
102
- const rootDir = getRootDir();
103
- fs.mkdirSync(path.dirname(path.join(rootDir, file.dest)), {
216
+ fs.mkdirSync(path.dirname(path.join(this._rootDir, file.dest)), {
104
217
  recursive: true,
105
218
  });
106
219
  if (file.interpolateContent) {
107
220
  const text = fs.readFileSync(
108
- path.join(
109
- path.dirname(require.resolve('@ixon-cdk/templates')),
110
- file.source,
111
- ),
221
+ path.join(path.dirname(require.resolve('@ixon-cdk/templates')), file.source),
112
222
  { encoding: 'utf-8' },
113
223
  );
114
224
  const data = this._interpolateText(text, ctx);
115
- fs.writeFileSync(path.join(rootDir, file.dest), data, {
225
+ fs.writeFileSync(path.join(this._rootDir, file.dest), data, {
116
226
  encoding: 'utf-8',
117
227
  });
118
228
  } else {
119
229
  fs.copyFileSync(
120
- path.join(
121
- path.dirname(require.resolve('@ixon-cdk/templates')),
122
- file.source,
123
- ),
124
- path.join(rootDir, file.dest),
230
+ path.join(path.dirname(require.resolve('@ixon-cdk/templates')), file.source),
231
+ path.join(this._rootDir, file.dest),
125
232
  );
126
233
  }
127
234
  logFileCrudMessage('CREATE', file.dest);
128
235
  }
129
236
 
237
+ /**
238
+ * This script loops over a dependencies object and will check if the provided file contents is
239
+ * importing any. When that is the case, it will first check for that package whether it could
240
+ * already be a depencency in your workspace. If not, the package will get installed and saved.
241
+ */
242
+ _checkDependencies(fileContents, dependencies) {
243
+ const rootDeps = require(path.join(this._rootDir, 'package.json')).dependencies || {};
244
+ Object.keys(dependencies).forEach((pkg) => {
245
+ const matcher = new RegExp(`^\\s*import.*\\s['"]${pkg}\\S*['"]`, 'gm');
246
+ if (matcher.test(fileContents) && !(pkg in rootDeps)) {
247
+ const version = dependencies[pkg];
248
+ console.log(`Installing package '${pkg}'...`);
249
+ require('child_process').execSync(`npm i ${pkg}@${version} --save`);
250
+ }
251
+ });
252
+ }
253
+
254
+ /**
255
+ * This script will find and replace the component name definition in a vue (SFC) input file.
256
+ */
257
+ _findAndReplaceVueInputFile(fileContents, componentName) {
258
+ const matcher = /export\s+default\s+{\s+name:\s+['"](\S+)['"]/gm;
259
+ return fileContents.replaceAll(matcher, (match, p1) => match.replace(p1, componentName));
260
+ }
261
+
262
+ /**
263
+ * This script will find and replace the component class definition and the arguments for the
264
+ * custom element define method in a static input file.
265
+ *
266
+ * Given the example input file has the follwing contents:
267
+ *
268
+ * ```js
269
+ * class PctExample extends HTMLElement {
270
+ * ...
271
+ * }
272
+ *
273
+ * customElements.define('pct-example', PctExample);
274
+ * ```
275
+ *
276
+ * ...if your workspace prefix is "abc" and your component name is "from-example", the content
277
+ * will be transformed into:
278
+ *
279
+ * ```js
280
+ * class AbcFromExample extends HTMLElement {
281
+ * ...
282
+ * }
283
+ *
284
+ * customElements.define('abc-from-example', AbcFromExample);
285
+ * ```
286
+ */
287
+ _findAndReplaceStaticInputFile(
288
+ fileContents,
289
+ searchPrefix,
290
+ searchName,
291
+ replacementPrefix,
292
+ replacementName,
293
+ ) {
294
+ const searchTag = this._getTag(searchPrefix, searchName);
295
+ const replacementTag = this._getTag(replacementPrefix, replacementName);
296
+ return fileContents
297
+ .replaceAll(new RegExp(`class\\s+(${pascalCase(searchTag)})`, 'gm'), (match, p1) =>
298
+ match.replace(p1, pascalCase(replacementTag)),
299
+ )
300
+ .replaceAll(
301
+ new RegExp(
302
+ `define\\s*\\(\\s*['"](${searchTag})['"]\\s*,\\s+(${pascalCase(searchTag)})\\)`,
303
+ 'gm',
304
+ ),
305
+ (match, p1, p2) =>
306
+ match.replace(p1, replacementTag).replace(p2, pascalCase(replacementTag)),
307
+ );
308
+ }
309
+
130
310
  _discover() {
131
311
  const dir = path.dirname(require.resolve('@ixon-cdk/templates'));
132
312
  const files = fs.readdirSync(dir);
@@ -141,14 +321,18 @@ module.exports = class TemplateService {
141
321
  });
142
322
  }
143
323
 
324
+ _getTag(prefix, name) {
325
+ return prefix ? `${prefix}-${name}` : name;
326
+ }
327
+
144
328
  _interpolateSchema(schema, params) {
145
329
  return JSON.parse(this._interpolateText(JSON.stringify(schema), params));
146
330
  }
147
331
 
148
332
  _interpolateText(text, params) {
149
333
  return text
150
- .replace(/\{name\}/g, params.name)
151
- .replace(/\{tag\}/g, params.tag)
152
- .replace(/\{pascalCase\(tag\)\}/g, pascalCase(params.tag));
334
+ .replace(/<%=\s*name\s*%>/g, params.name)
335
+ .replace(/<%=\s*tag\s*%>/g, params.tag)
336
+ .replace(/<%=\s*classify\(\s*tag\s*\)\s*%>/g, pascalCase(params.tag));
153
337
  }
154
338
  };
package/utils.js CHANGED
@@ -61,7 +61,12 @@ function ensureModule(moduleName) {
61
61
  }
62
62
  if (!moduleExists(moduleName)) {
63
63
  console.log(`Installing package '${moduleName}'...`);
64
- require('child_process').execSync(`npm install --save-dev ${moduleName}`);
64
+ if (moduleName.startsWith('@ixon-cdk/')) {
65
+ const cdkVersion = require('./package.json').version;
66
+ require('child_process').execSync(`npm install --save-dev ${moduleName}@${cdkVersion}`);
67
+ } else {
68
+ require('child_process').execSync(`npm install --save-dev ${moduleName}`);
69
+ }
65
70
  }
66
71
  }
67
72
 
@@ -114,10 +119,22 @@ function generatePreviewHash(templateId, templateName, versionId, versionNumber,
114
119
  return require('crypto-js').AES.encrypt(JSON.stringify(ref), salt).toString();
115
120
  }
116
121
 
122
+ async function getFiles(dir) {
123
+ const dirents = await fs.promises.readdir(dir, { withFileTypes: true });
124
+ const files = await Promise.all(
125
+ dirents.map((dirent) => {
126
+ const res = path.resolve(dir, dirent.name);
127
+ return dirent.isDirectory() ? getFiles(res) : res;
128
+ }),
129
+ );
130
+ return Array.prototype.concat(...files);
131
+ }
132
+
117
133
  module.exports = {
118
134
  getArgv,
119
135
  getRootDir,
120
136
  logErrorMessage,
137
+ getFiles,
121
138
  logFileCrudMessage,
122
139
  logSuccessMessage,
123
140
  moduleExists,