@nordicsemiconductor/pc-nrfconnect-shared 197.0.0 → 199.0.0

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,10 +6,12 @@
6
6
  * SPDX-License-Identifier: LicenseRef-Nordic-4-Clause
7
7
  */
8
8
 
9
+ /* eslint-disable max-classes-per-file, class-methods-use-this */
10
+
9
11
  import { execSync } from 'child_process';
10
- import { program } from 'commander';
12
+ import { Option, program } from 'commander';
11
13
  import fs from 'fs';
12
- import FtpClient from 'ftp';
14
+ import LowlevelFtpClient from 'ftp';
13
15
  import semver from 'semver';
14
16
  import calculateShasum from 'shasum';
15
17
 
@@ -30,7 +32,201 @@ interface App {
30
32
  packageJson: PackageJsonApp;
31
33
  }
32
34
 
33
- const client = new FtpClient();
35
+ let client: Client;
36
+
37
+ abstract class Client {
38
+ abstract sourceUrl: string;
39
+
40
+ initialise(options: Options): Promise<void> | void {} // eslint-disable-line @typescript-eslint/no-unused-vars
41
+ end(): void {}
42
+
43
+ abstract download(filename: string): Promise<string>;
44
+ abstract uploadContent(
45
+ content: Buffer,
46
+ remoteFilename: string
47
+ ): Promise<void>;
48
+ abstract uploadLocalFile(
49
+ localFilename: string,
50
+ remoteFilename: string
51
+ ): Promise<void>;
52
+ }
53
+
54
+ class FtpClient extends Client {
55
+ /*
56
+ * To use this script with an FTP server REPO_HOST, REPO_USER and REPO_PASS need to be set
57
+ */
58
+
59
+ ftpClient = new LowlevelFtpClient();
60
+
61
+ host = process.env.REPO_HOST || 'localhost';
62
+ port = Number(process.env.REPO_PORT) || 21;
63
+ user = process.env.REPO_USER || 'anonymous';
64
+ password = process.env.REPO_PASS || 'anonymous@';
65
+
66
+ sourceDir: string;
67
+ sourceUrl: string;
68
+
69
+ constructor(private readonly options: Options) {
70
+ super();
71
+ this.sourceDir = this.getSourceDir();
72
+ this.sourceUrl = this.getSourceUrl();
73
+ }
74
+
75
+ getSourceDir = () => {
76
+ const repoDirOfficial = '/.pc-tools/nrfconnect-apps';
77
+
78
+ if (process.env.REPO_DIR) return process.env.REPO_DIR;
79
+ if (this.options.deployOfficial) return repoDirOfficial;
80
+
81
+ return `${repoDirOfficial}/${this.options.source}`;
82
+ };
83
+
84
+ getSourceUrl = () => {
85
+ if (process.env.REPO_URL) return process.env.REPO_URL;
86
+ return `https://developer.nordicsemi.com${this.sourceDir}`;
87
+ };
88
+
89
+ connect = () =>
90
+ new Promise<void>((resolve, reject) => {
91
+ console.log(
92
+ `Connecting to ftp://${this.user}@${this.host}:${this.port}`
93
+ );
94
+ this.ftpClient.once('error', err => {
95
+ this.ftpClient.removeAllListeners('ready');
96
+ reject(err);
97
+ });
98
+ this.ftpClient.once('ready', () => {
99
+ this.ftpClient.removeAllListeners('error');
100
+ resolve();
101
+ });
102
+ this.ftpClient.connect({
103
+ host: this.host,
104
+ port: this.port,
105
+ user: this.user,
106
+ password: this.password,
107
+ });
108
+ });
109
+
110
+ changeWorkingDirectory = () =>
111
+ new Promise<void>((resolve, reject) => {
112
+ console.log(`Changing to directory ${this.sourceDir}`);
113
+ this.ftpClient.cwd(this.sourceDir, err => {
114
+ if (err) {
115
+ reject(
116
+ new Error(
117
+ '\nError: Failed to change to directory. ' +
118
+ 'Check whether it exists on the FTP server.'
119
+ )
120
+ );
121
+ } else {
122
+ resolve();
123
+ }
124
+ });
125
+ });
126
+
127
+ initialise = async () => {
128
+ await this.connect();
129
+ await this.changeWorkingDirectory();
130
+ };
131
+
132
+ download = (filename: string) =>
133
+ new Promise<string>((resolve, reject) => {
134
+ console.log(`Downloading file ${filename}`);
135
+ let data = '';
136
+ this.ftpClient.get(filename, (err, stream) => {
137
+ if (err) return reject(err);
138
+ stream.once('close', () => resolve(data));
139
+ stream.on('data', chunk => {
140
+ data += chunk;
141
+ });
142
+ return undefined;
143
+ });
144
+ });
145
+
146
+ upload = (
147
+ contentOrLocalFilename: string | Buffer,
148
+ remoteFilename: string
149
+ ) =>
150
+ new Promise<void>((resolve, reject) => {
151
+ console.log(`Uploading file ${remoteFilename}`);
152
+ this.ftpClient.put(contentOrLocalFilename, remoteFilename, err =>
153
+ err ? reject(err) : resolve()
154
+ );
155
+ });
156
+
157
+ uploadContent = this.upload;
158
+ uploadLocalFile = this.upload;
159
+
160
+ end = () => this.ftpClient.end();
161
+ }
162
+
163
+ class ArtifactoryClient extends Client {
164
+ token = process.env.ARTIFACTORY_TOKEN;
165
+
166
+ sourceUrl: string;
167
+
168
+ constructor(private readonly options: Options) {
169
+ super();
170
+
171
+ if (this.token == null) {
172
+ throw new Error(
173
+ 'The environment variable ARTIFACTORY_TOKEN must be set.'
174
+ );
175
+ }
176
+
177
+ this.sourceUrl = `https://files.nordicsemi.com/artifactory/swtools/${this.getAccessLevel()}/ncd/apps/${
178
+ options.source
179
+ }`;
180
+ }
181
+
182
+ getAccessLevel = () => {
183
+ if (this.options.accessLevel != null) return this.options.accessLevel;
184
+
185
+ return this.options.deployOfficial ? 'external' : 'internal';
186
+ };
187
+
188
+ download = async (filename: string) => {
189
+ console.log(`Downloading ${filename}`);
190
+
191
+ const url = `${this.sourceUrl}/${filename}`;
192
+ const res = await fetch(url, {
193
+ headers: { Authorization: `Bearer ${this.token}` },
194
+ });
195
+
196
+ if (!res.ok) {
197
+ throw new Error(`Failed to download ${url}: ${res.statusText}`);
198
+ }
199
+
200
+ return res.text();
201
+ };
202
+
203
+ upload = async (content: Buffer, remoteFilename: string) => {
204
+ const url = `${this.sourceUrl}/${remoteFilename}`;
205
+ const res = await fetch(url, {
206
+ method: 'PUT',
207
+ body: content,
208
+ headers: { Authorization: `Bearer ${this.token}` },
209
+ });
210
+
211
+ if (!res.ok) {
212
+ throw new Error(`Failed to upload ${url}: ${res.statusText}`);
213
+ }
214
+ };
215
+
216
+ uploadContent = (content: Buffer, remoteFilename: string) => {
217
+ console.log(`Uploading content for ${remoteFilename}`);
218
+
219
+ return this.upload(content, remoteFilename);
220
+ };
221
+
222
+ uploadLocalFile = (localFilename: string, remoteFilename: string) => {
223
+ console.log(
224
+ `Uploading local file ${localFilename} as ${remoteFilename}`
225
+ );
226
+
227
+ return this.upload(fs.readFileSync(localFilename), remoteFilename);
228
+ };
229
+ }
34
230
 
35
231
  const hasMessage = (error: unknown): error is { message: unknown } =>
36
232
  error != null && typeof error === 'object' && 'message' in error;
@@ -38,64 +234,84 @@ const hasMessage = (error: unknown): error is { message: unknown } =>
38
234
  const errorAsString = (error: unknown) =>
39
235
  hasMessage(error) ? error.message : String(error);
40
236
 
41
- const getSourceDir = (deployOfficial: boolean, sourceName: string) => {
42
- const repoDirOfficial = '/.pc-tools/nrfconnect-apps';
237
+ const validAccessLevels = [
238
+ 'external',
239
+ 'external-confidential',
240
+ 'internal',
241
+ 'internal-confidential',
242
+ ] as const;
43
243
 
44
- if (process.env.REPO_DIR) return process.env.REPO_DIR;
45
- if (deployOfficial) return repoDirOfficial;
244
+ type AccessLevel = (typeof validAccessLevels)[number];
46
245
 
47
- return `${repoDirOfficial}/${sourceName}`;
48
- };
246
+ const isAccessLevel = (value: string): value is AccessLevel =>
247
+ (validAccessLevels as readonly string[]).includes(value);
248
+
249
+ const splitSourceAndAccessLevel = (sourceAndMaybeAccessLevel: string) => {
250
+ const match = sourceAndMaybeAccessLevel.match(
251
+ /(?<source>.*?)\s*\((?<accessLevel>.*)\)/
252
+ );
49
253
 
50
- const getSourceUrl = (deployOfficial: boolean, sourceName: string) => {
51
- const repoUrlOfficial =
52
- 'https://developer.nordicsemi.com/.pc-tools/nrfconnect-apps';
254
+ if (match == null) {
255
+ return { source: sourceAndMaybeAccessLevel };
256
+ }
53
257
 
54
- if (process.env.REPO_URL) return process.env.REPO_URL;
55
- if (deployOfficial) return repoUrlOfficial;
258
+ const { source, accessLevel } = match.groups!; // eslint-disable-line @typescript-eslint/no-non-null-assertion -- Can never be null because of the regex
56
259
 
57
- return `${repoUrlOfficial}/${sourceName}`;
260
+ if (!isAccessLevel(accessLevel)) {
261
+ throw new Error(
262
+ `The specified access level "${accessLevel}" must be one of ${validAccessLevels.join(
263
+ ', '
264
+ )}.`
265
+ );
266
+ }
267
+
268
+ return { source, accessLevel };
58
269
  };
59
270
 
60
271
  interface Options {
61
272
  doPack: boolean;
62
- doCreateSource: boolean;
63
273
  deployOfficial: boolean;
64
- sourceDir: string;
65
- sourceUrl: string;
274
+ source: string;
66
275
  sourceName?: string;
276
+ destination: 'ftp' | 'artifactory';
277
+ accessLevel?: AccessLevel;
67
278
  }
68
279
 
69
280
  const parseOptions = (): Options => {
70
281
  program
71
- .description('Publish to nordic repository')
282
+ .description('Publish an nRF Connect for Desktop app')
72
283
  .requiredOption(
73
284
  '-s, --source <source>',
74
- 'Specify the source to publish (e.g. official).'
285
+ 'Specify the source to publish (e.g. "official" or "releast-test"). ' +
286
+ 'When publishing to Artifactory, an access level can be ' +
287
+ 'specified at the end in parantheses (e.g. "official (external)").'
288
+ )
289
+ .addOption(
290
+ new Option(
291
+ '-d, --destination <ftp|artifactory>',
292
+ 'Specify where to publish.'
293
+ )
294
+ .choices(['ftp', 'artifactory'])
295
+ .makeOptionMandatory()
75
296
  )
76
297
  .option(
77
298
  '-n, --no-pack',
78
299
  'Publish existing .tgz file at the root directory without npm pack.'
79
300
  )
80
- .option(
81
- '--create-source <source name>',
82
- 'Do not fail if the source specifiec with --source does not yet ' +
83
- 'exist but instead create a new source with this name ' +
84
- '(e.g. "Release Test").'
85
- )
86
301
  .parse();
87
302
 
88
303
  const options = program.opts();
89
304
 
90
- const deployOfficial = options.source === 'official';
305
+ const { source, accessLevel } = splitSourceAndAccessLevel(options.source);
306
+
307
+ const deployOfficial = source === 'official';
91
308
 
92
309
  return {
93
310
  doPack: options.pack,
94
- doCreateSource: options.createSource != null,
95
- sourceName: options.createSource,
311
+ source,
96
312
  deployOfficial,
97
- sourceDir: getSourceDir(deployOfficial, options.source),
98
- sourceUrl: getSourceUrl(deployOfficial, options.source),
313
+ destination: options.destination,
314
+ accessLevel,
99
315
  };
100
316
  };
101
317
 
@@ -144,7 +360,7 @@ const packOrReadPackage = (options: Options): App => {
144
360
  version,
145
361
  filename,
146
362
  shasum,
147
- sourceUrl: options.sourceUrl,
363
+ sourceUrl: client.sourceUrl,
148
364
  isOfficial: options.deployOfficial,
149
365
  appInfoName: `${name}.json`,
150
366
  releaseNotesFilename: `${name}-Changelog.md`,
@@ -153,72 +369,6 @@ const packOrReadPackage = (options: Options): App => {
153
369
  };
154
370
  };
155
371
 
156
- const connect = (config: {
157
- host: string;
158
- port: number;
159
- user: string;
160
- password: string;
161
- }) =>
162
- new Promise<void>((resolve, reject) => {
163
- console.log(
164
- `Connecting to ftp://${config.user}@${config.host}:${config.port}`
165
- );
166
- client.once('error', err => {
167
- client.removeAllListeners('ready');
168
- reject(err);
169
- });
170
- client.once('ready', () => {
171
- client.removeAllListeners('error');
172
- resolve();
173
- });
174
- client.connect(config);
175
- });
176
-
177
- const createSourceDirectory = (dir: string) =>
178
- new Promise<void>((resolve, reject) => {
179
- console.log(`Creating source directory ${dir}`);
180
- client.mkdir(dir, true, err => {
181
- if (err) {
182
- reject(new Error(`Failed to create source directory.`));
183
- } else {
184
- resolve();
185
- }
186
- });
187
- });
188
-
189
- const changeWorkingDirectory = (dir: string) =>
190
- new Promise<void>((resolve, reject) => {
191
- console.log(`Changing to directory ${dir}`);
192
- client.cwd(dir, err => {
193
- if (err) {
194
- reject(
195
- new Error(
196
- '\nError: Failed to change to directory. ' +
197
- 'Check whether it exists on the FTP server.\n' +
198
- 'If you want to create a new source, use the ' +
199
- '--create-source option.'
200
- )
201
- );
202
- } else {
203
- resolve();
204
- }
205
- });
206
- });
207
-
208
- const downloadFileContent = (filename: string) =>
209
- new Promise<string>((resolve, reject) => {
210
- console.log(`Downloading file ${filename}`);
211
- let data = '';
212
- client.get(filename, (err, stream) => {
213
- if (err) return reject(err);
214
- stream.once('close', () => resolve(data));
215
- stream.on('data', chunk => {
216
- data += chunk;
217
- });
218
- return undefined;
219
- });
220
- });
221
-
222
372
  const assertAppVersionIsValid = (
223
373
  latestAppVersion: string | undefined,
224
374
  app: App
@@ -234,39 +384,10 @@ const assertAppVersionIsValid = (
234
384
  }
235
385
  };
236
386
 
237
- type UploadLocalFile = (localFileName: string, remote: string) => Promise<void>;
238
- type UploadBufferContent = (content: Buffer, remote: string) => Promise<void>;
239
-
240
- const uploadFile: UploadLocalFile & UploadBufferContent = (
241
- local: string | Buffer,
242
- remote: string
243
- ) =>
244
- new Promise<void>((resolve, reject) => {
245
- console.log(`Uploading file ${remote}`);
246
- client.put(local, remote, err => (err ? reject(err) : resolve()));
247
- });
248
-
249
- const createBlankSourceJson = async (name: string) => {
250
- try {
251
- await downloadFileContent('source.json');
252
- } catch {
253
- // Expected that the download throws an exception,
254
- // because the file is supposed to not exist yet
255
- return {
256
- name,
257
- apps: [],
258
- };
259
- }
260
-
261
- throw new Error(
262
- '`--create-source` given, but a `source.json` already exists on the server.'
263
- );
264
- };
265
-
266
387
  const downloadSourceJson = async () => {
267
388
  let sourceJsonContent;
268
389
  try {
269
- sourceJsonContent = await downloadFileContent('source.json');
390
+ sourceJsonContent = await client.download('source.json');
270
391
  const sourceJson = <SourceJson>JSON.parse(sourceJsonContent);
271
392
  if (
272
393
  sourceJson == null ||
@@ -292,15 +413,10 @@ const downloadSourceJson = async () => {
292
413
  }
293
414
  };
294
415
 
295
- const getUpdatedSourceJson = async (
296
- app: App,
297
- options: Options
298
- ): Promise<SourceJson> => {
299
- const sourceJson = await (options.doCreateSource
300
- ? createBlankSourceJson(options.sourceName!) // eslint-disable-line @typescript-eslint/no-non-null-assertion -- Can never be null because of the control flow
301
- : downloadSourceJson());
416
+ const getUpdatedSourceJson = async (app: App): Promise<SourceJson> => {
417
+ const sourceJson = await downloadSourceJson();
302
418
  return {
303
- name: sourceJson.name,
419
+ ...sourceJson,
304
420
  apps: [
305
421
  ...new Set(sourceJson.apps).add(
306
422
  `${app.sourceUrl}/${app.appInfoName}`
@@ -313,7 +429,7 @@ const downloadExistingAppInfo = async (
313
429
  app: App
314
430
  ): Promise<Partial<Pick<AppInfo, 'latestVersion' | 'versions'>>> => {
315
431
  try {
316
- const appInfoContent = await downloadFileContent(app.appInfoName);
432
+ const appInfoContent = await client.download(app.appInfoName);
317
433
  return JSON.parse(appInfoContent) as AppInfo;
318
434
  } catch (error) {
319
435
  console.log(
@@ -367,18 +483,19 @@ const getUpdatedAppInfo = async (app: App): Promise<AppInfo> => {
367
483
  };
368
484
 
369
485
  const uploadSourceJson = (sourceJson: SourceJson) =>
370
- uploadFile(
486
+ client.uploadContent(
371
487
  Buffer.from(JSON.stringify(sourceJson, undefined, 2)),
372
488
  'source.json'
373
489
  );
374
490
 
375
491
  const uploadAppInfo = (app: App, appInfo: AppInfo) =>
376
- uploadFile(
492
+ client.uploadContent(
377
493
  Buffer.from(JSON.stringify(appInfo, undefined, 2)),
378
494
  app.appInfoName
379
495
  );
380
496
 
381
- const uploadPackage = (app: App) => uploadFile(app.filename, app.filename);
497
+ const uploadPackage = (app: App) =>
498
+ client.uploadLocalFile(app.filename, app.filename);
382
499
 
383
500
  const uploadChangelog = (app: App) => {
384
501
  const changelogFilename = 'Changelog.md';
@@ -387,7 +504,7 @@ const uploadChangelog = (app: App) => {
387
504
  return Promise.reject(new Error(errorMsg));
388
505
  }
389
506
 
390
- return uploadFile(changelogFilename, app.releaseNotesFilename);
507
+ return client.uploadLocalFile(changelogFilename, app.releaseNotesFilename);
391
508
  };
392
509
 
393
510
  const uploadIcon = (app: App) => {
@@ -397,36 +514,27 @@ const uploadIcon = (app: App) => {
397
514
  return Promise.reject(new Error(errorMsg));
398
515
  }
399
516
 
400
- return uploadFile(localIconFilename, app.iconFilename);
517
+ return client.uploadLocalFile(localIconFilename, app.iconFilename);
401
518
  };
402
519
 
403
520
  const main = async () => {
404
521
  try {
405
- /*
406
- * To use this script REPO_HOST, REPO_USER and REPO_PASS will need to be set
407
- */
408
- const config = {
409
- host: process.env.REPO_HOST || 'localhost',
410
- port: Number(process.env.REPO_PORT) || 21,
411
- user: process.env.REPO_USER || 'anonymous',
412
- password: process.env.REPO_PASS || 'anonymous@',
413
- };
414
-
415
522
  const options = parseOptions();
416
523
 
524
+ client =
525
+ options.destination === 'ftp'
526
+ ? new FtpClient(options)
527
+ : new ArtifactoryClient(options);
528
+
417
529
  checkAppProperties({
418
530
  checkChangelogHasCurrentEntry: options.deployOfficial,
419
531
  });
420
532
 
421
533
  const app = packOrReadPackage(options);
422
534
 
423
- await connect(config);
424
- if (options.doCreateSource) {
425
- await createSourceDirectory(options.sourceDir);
426
- }
427
- await changeWorkingDirectory(options.sourceDir);
535
+ await client.initialise(options);
428
536
 
429
- const sourceJson = await getUpdatedSourceJson(app, options);
537
+ const sourceJson = await getUpdatedSourceJson(app);
430
538
  const appInfo = await getUpdatedAppInfo(app);
431
539
 
432
540
  await uploadChangelog(app);
@@ -441,7 +549,7 @@ const main = async () => {
441
549
  process.exitCode = 1;
442
550
  }
443
551
 
444
- client.end();
552
+ client?.end();
445
553
  };
446
554
 
447
555
  main();
@@ -78,8 +78,8 @@ const updateChangelog = (nextReleaseNumber: string) => {
78
78
  }
79
79
 
80
80
  if (
81
- header === 'Unreleased' ||
82
- header === `${nextReleaseNumber} - Unreleased`
81
+ header.toLowerCase() === 'unreleased' ||
82
+ header.toLowerCase() === `${nextReleaseNumber} - unreleased`
83
83
  ) {
84
84
  writeFileSync(
85
85
  'Changelog.md',
@@ -8,11 +8,7 @@ import React from 'react';
8
8
  import { useSelector } from 'react-redux';
9
9
 
10
10
  import Card from '../Card/Card';
11
- import {
12
- buyOnlineUrl,
13
- deviceInfo,
14
- productPageUrl,
15
- } from '../Device/deviceInfo/deviceInfo';
11
+ import { deviceInfo } from '../Device/deviceInfo/deviceInfo';
16
12
  import { selectedDevice, selectedDeviceInfo } from '../Device/deviceSlice';
17
13
  import AboutButton from './AboutButton';
18
14
  import Section from './Section';
@@ -38,7 +34,7 @@ export default () => {
38
34
  }
39
35
 
40
36
  const pca = device.devkit?.boardVersion;
41
- const { name, cores } = deviceInfo(device);
37
+ const { name, cores, website } = deviceInfo(device);
42
38
 
43
39
  return (
44
40
  <Card title="Device">
@@ -61,13 +57,13 @@ export default () => {
61
57
 
62
58
  <Section>
63
59
  <AboutButton
64
- url={buyOnlineUrl(device)}
60
+ url={website.buyOnline}
65
61
  label="Find distributor"
66
62
  />
67
63
  </Section>
68
64
  <Section>
69
65
  <AboutButton
70
- url={productPageUrl(device)}
66
+ url={website.productPage}
71
67
  label="Go to product page"
72
68
  />
73
69
  </Section>