@learnpack/learnpack 5.0.21 → 5.0.22

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 CHANGED
@@ -21,7 +21,7 @@ $ npm install -g @learnpack/learnpack
21
21
  $ learnpack COMMAND
22
22
  running command...
23
23
  $ learnpack (-v|--version|version)
24
- @learnpack/learnpack/5.0.21 win32-x64 node-v20.16.0
24
+ @learnpack/learnpack/5.0.22 win32-x64 node-v20.16.0
25
25
  $ learnpack --help [COMMAND]
26
26
  USAGE
27
27
  $ learnpack COMMAND
@@ -75,7 +75,7 @@ DESCRIPTION
75
75
  12. If there is a file within the exercises folder but not inside of any particular exercise's folder. (Warning)
76
76
  ```
77
77
 
78
- _See code: [src\commands\audit.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.21/src\commands\audit.ts)_
78
+ _See code: [src\commands\audit.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.22/src\commands\audit.ts)_
79
79
 
80
80
  ## `learnpack clean`
81
81
 
@@ -90,7 +90,7 @@ DESCRIPTION
90
90
  Extra documentation goes here
91
91
  ```
92
92
 
93
- _See code: [src\commands\clean.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.21/src\commands\clean.ts)_
93
+ _See code: [src\commands\clean.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.22/src\commands\clean.ts)_
94
94
 
95
95
  ## `learnpack download [PACKAGE]`
96
96
 
@@ -108,7 +108,7 @@ DESCRIPTION
108
108
  Extra documentation goes here
109
109
  ```
110
110
 
111
- _See code: [src\commands\download.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.21/src\commands\download.ts)_
111
+ _See code: [src\commands\download.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.22/src\commands\download.ts)_
112
112
 
113
113
  ## `learnpack help [COMMAND]`
114
114
 
@@ -140,7 +140,7 @@ OPTIONS
140
140
  -y, --yes Skip all prompts and initialize an empty project
141
141
  ```
142
142
 
143
- _See code: [src\commands\init.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.21/src\commands\init.ts)_
143
+ _See code: [src\commands\init.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.22/src\commands\init.ts)_
144
144
 
145
145
  ## `learnpack login [PACKAGE]`
146
146
 
@@ -158,7 +158,7 @@ DESCRIPTION
158
158
  Extra documentation goes here
159
159
  ```
160
160
 
161
- _See code: [src\commands\login.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.21/src\commands\login.ts)_
161
+ _See code: [src\commands\login.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.22/src\commands\login.ts)_
162
162
 
163
163
  ## `learnpack logout [PACKAGE]`
164
164
 
@@ -176,7 +176,7 @@ DESCRIPTION
176
176
  Extra documentation goes here
177
177
  ```
178
178
 
179
- _See code: [src\commands\logout.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.21/src\commands\logout.ts)_
179
+ _See code: [src\commands\logout.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.22/src\commands\logout.ts)_
180
180
 
181
181
  ## `learnpack plugins`
182
182
 
@@ -307,7 +307,7 @@ OPTIONS
307
307
  -h, --help show CLI help
308
308
  ```
309
309
 
310
- _See code: [src\commands\publish.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.21/src\commands\publish.ts)_
310
+ _See code: [src\commands\publish.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.22/src\commands\publish.ts)_
311
311
 
312
312
  ## `learnpack start`
313
313
 
@@ -329,7 +329,7 @@ OPTIONS
329
329
  -y, --yes Skip all prompts and initialize an empty project
330
330
  ```
331
331
 
332
- _See code: [src\commands\start.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.21/src\commands\start.ts)_
332
+ _See code: [src\commands\start.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.22/src\commands\start.ts)_
333
333
 
334
334
  ## `learnpack test [EXERCISESLUG]`
335
335
 
@@ -346,7 +346,7 @@ OPTIONS
346
346
  -y, --yes Skip all prompts and initialize an empty project
347
347
  ```
348
348
 
349
- _See code: [src\commands\test.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.21/src\commands\test.ts)_
349
+ _See code: [src\commands\test.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.22/src\commands\test.ts)_
350
350
 
351
351
  ## `learnpack translate`
352
352
 
@@ -360,7 +360,7 @@ OPTIONS
360
360
  -y, --yes Skip all prompts and initialize an empty project
361
361
  ```
362
362
 
363
- _See code: [src\commands\translate.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.21/src\commands\translate.ts)_
363
+ _See code: [src\commands\translate.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.22/src\commands\translate.ts)_
364
364
  <!-- commandsstop -->
365
365
 
366
366
  > > > > > > > 0cb3e56d84c197f9d008836bb573eade212b7e57
@@ -9,7 +9,7 @@ declare const _default: {
9
9
  checkLearnpackClean: (configObj: IConfigObj, errors: IAuditErrors[]) => void;
10
10
  findInFile: (types: string[], content: string) => IFindings;
11
11
  checkUrl: (config: IConfigObj, filePath: string, fileName: string, exercise: IExercise | undefined, errors: IAuditErrors[], warnings: IAuditErrors[], counter: ICounter | undefined) => Promise<boolean>;
12
- writeFile: (content: string, filePath: string) => Promise<void>;
12
+ writeFile: (filePath: string, content: string) => void;
13
13
  showErrors: (errors: IAuditErrors[], counter: ICounter | undefined) => Promise<unknown>;
14
14
  showWarnings: (warnings: IAuditErrors[]) => Promise<unknown>;
15
15
  };
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
+ /* eslint-disable no-await-in-loop, @typescript-eslint/no-non-null-asserted-optional-chain, no-promise-executor-return */
2
3
  Object.defineProperty(exports, "__esModule", { value: true });
3
4
  const console_1 = require("./console");
4
5
  const fs = require("fs");
5
- const path = require("path");
6
6
  // eslint-disable-next-line
7
7
  const fetch = require("node-fetch");
8
8
  // eslint-disable-next-line
@@ -92,9 +92,34 @@ const findInFile = (types, content) => {
92
92
  }
93
93
  return findings;
94
94
  };
95
- // This function checks that each of the url's are working.
95
+ const checkLinkWithRetry = async (url, retries = 3, delay = 1000) => {
96
+ for (let attempt = 1; attempt <= retries; attempt++) {
97
+ try {
98
+ let res = await fetch(url, { method: "HEAD" });
99
+ if (res.status === 429) {
100
+ await new Promise(resolve => setTimeout(resolve, delay));
101
+ delay *= 2; // Exponential backoff
102
+ continue;
103
+ }
104
+ if (res.status === 403) {
105
+ return { isValid: false, status: 403 };
106
+ }
107
+ if (!res.ok) {
108
+ res = await fetch(url, { method: "GET" });
109
+ if (!res.ok) {
110
+ return { isValid: false, status: res.status };
111
+ }
112
+ }
113
+ return { isValid: true };
114
+ }
115
+ catch (error) {
116
+ console.debug(`Error checking link ${url}:`, error);
117
+ return { isValid: false, status: 429 };
118
+ }
119
+ }
120
+ return { isValid: false, status: 429 };
121
+ };
96
122
  const checkUrl = async (config, filePath, fileName, exercise, errors, warnings, counter) => {
97
- var _a, _b, _c, _d;
98
123
  if (!fs.existsSync(filePath))
99
124
  return false;
100
125
  const content = fs.readFileSync(filePath).toString();
@@ -109,95 +134,51 @@ const checkUrl = async (config, filePath, fileName, exercise, errors, warnings,
109
134
  if (Object.prototype.hasOwnProperty.call(frontmatter.attributes, attribute) &&
110
135
  (attribute === "intro" || attribute === "tutorial")) {
111
136
  counter && counter.links.total++;
112
- try {
113
- // eslint-disable-next-line
114
- let res = await fetch(frontmatter.attributes[attribute], {
115
- method: "HEAD",
116
- });
117
- if (!res.ok) {
137
+ const url = frontmatter.attributes[attribute];
138
+ const { isValid, status } = await checkLinkWithRetry(url);
139
+ if (!isValid) {
140
+ if (status === 429 || status === 403) {
141
+ warnings.push({
142
+ exercise: exercise === null || exercise === void 0 ? void 0 : exercise.title,
143
+ msg: `Warning: This link might be temporarily inaccessible (${status}): ${url}`,
144
+ });
145
+ }
146
+ else {
118
147
  counter && counter.links.error++;
119
148
  errors.push({
120
149
  exercise: exercise === null || exercise === void 0 ? void 0 : exercise.title,
121
- msg: `This link is broken (${res.ok}): ${frontmatter.attributes[attribute]}`,
150
+ msg: `This link is broken: ${url}`,
122
151
  });
123
152
  }
124
153
  }
125
- catch (_e) {
126
- counter && counter.links.error++;
127
- errors.push({
128
- exercise: exercise === null || exercise === void 0 ? void 0 : exercise.title,
129
- msg: `This link is broken: ${frontmatter.attributes[attribute]}`,
130
- });
131
- }
132
154
  }
133
155
  }
134
- // Check url's of each README file.
156
+ // Check URLs in README files
135
157
  const findings = findInFile(["relativeImages", "externalImages", "markdownLinks"], content);
136
158
  for (const finding in findings) {
137
159
  if (Object.prototype.hasOwnProperty.call(findings, finding)) {
138
160
  const obj = findings[finding];
139
- // Valdites all the relative path images.
140
- if (finding === "relativeImages" && Object.keys(obj).length > 0) {
141
- for (const img in obj) {
142
- if (Object.prototype.hasOwnProperty.call(obj, img)) {
143
- // Validates if the image is in the assets folder.
144
- counter && counter.images.total++;
145
- const relativePath = path
146
- .relative(exercise ? exercise.path.replace(/\\/gm, "/") : "./", `${(_a = config.config) === null || _a === void 0 ? void 0 : _a.dirPath}/assets/${obj[img].relUrl}`)
147
- .replace(/\\/gm, "/");
148
- if (relativePath !== obj[img].absUrl.split("?").shift()) {
149
- counter && counter.images.error++;
150
- errors.push({
151
- exercise: exercise === null || exercise === void 0 ? void 0 : exercise.title,
152
- msg: `This relative path (${obj[img].relUrl}) is not pointing to the assets folder.`,
153
- });
154
- }
155
- if (!fs.existsSync(`${(_b = config.config) === null || _b === void 0 ? void 0 : _b.dirPath}/assets/${obj[img].relUrl}`)) {
156
- counter && counter.images.error++;
157
- errors.push({
158
- exercise: exercise === null || exercise === void 0 ? void 0 : exercise.title,
159
- msg: `The file ${obj[img].relUrl} doesn't exist in the assets folder.`,
160
- });
161
- }
162
- }
163
- }
164
- }
165
- else if (finding === "externalImages" && Object.keys(obj).length > 0) {
166
- // Valdites all the aboslute path images.
161
+ if (finding === "externalImages" && Object.keys(obj).length > 0) {
167
162
  for (const img in obj) {
168
163
  if (Object.prototype.hasOwnProperty.call(obj, img)) {
169
164
  counter && counter.images.total++;
170
- if (fs.existsSync(`${(_c = config.config) === null || _c === void 0 ? void 0 : _c.dirPath}/assets${obj[img].mdUrl
171
- .split("?")
172
- .shift()}`)) {
173
- const relativePath = path
174
- .relative(exercise ? exercise.path.replace(/\\/gm, "/") : "./", `${(_d = config.config) === null || _d === void 0 ? void 0 : _d.dirPath}/assets/${obj[img].mdUrl}`)
175
- .replace(/\\/gm, "/");
176
- warnings.push({
177
- exercise: exercise === null || exercise === void 0 ? void 0 : exercise.title,
178
- msg: `On this exercise you have an image with an absolute path "${obj[img].absUrl}". We recommend you to replace it by the relative path: "${relativePath}".`,
179
- });
180
- }
181
- try {
182
- // eslint-disable-next-line
183
- let res = await fetch(obj[img].absUrl, {
184
- method: "HEAD",
185
- });
186
- if (!res.ok) {
165
+ const url = obj[img].absUrl;
166
+ const { isValid, status } = await checkLinkWithRetry(url);
167
+ if (!isValid) {
168
+ if (status === 429 || status === 403) {
169
+ warnings.push({
170
+ exercise: exercise === null || exercise === void 0 ? void 0 : exercise.title,
171
+ msg: `Warning: This image link might be temporarily inaccessible (${status}): ${url}`,
172
+ });
173
+ }
174
+ else {
187
175
  counter && counter.images.error++;
188
176
  errors.push({
189
177
  exercise: exercise === null || exercise === void 0 ? void 0 : exercise.title,
190
- msg: `This link is broken: ${obj[img].absUrl}`,
178
+ msg: `This image link is broken: ${url}`,
191
179
  });
192
180
  }
193
181
  }
194
- catch (_f) {
195
- counter && counter.images.error++;
196
- errors.push({
197
- exercise: exercise === null || exercise === void 0 ? void 0 : exercise.title,
198
- msg: `This link is broken: ${obj[img].absUrl}`,
199
- });
200
- }
201
182
  }
202
183
  }
203
184
  }
@@ -205,27 +186,24 @@ const checkUrl = async (config, filePath, fileName, exercise, errors, warnings,
205
186
  for (const link in obj) {
206
187
  if (Object.prototype.hasOwnProperty.call(obj, link)) {
207
188
  counter && counter.links.total++;
208
- if (!obj[link].mdUrl.includes("twitter")) {
209
- try {
210
- // eslint-disable-next-line
211
- let res = await fetch(obj[link].mdUrl, {
212
- method: "HEAD",
213
- });
214
- if (res.status > 399 && res.status < 500) {
189
+ const url = obj[link].mdUrl;
190
+ if (!url.includes("twitter")) {
191
+ const { isValid, status } = await checkLinkWithRetry(url);
192
+ if (!isValid) {
193
+ if (status === 429 || status === 403) {
194
+ warnings.push({
195
+ exercise: exercise === null || exercise === void 0 ? void 0 : exercise.title,
196
+ msg: `Warning: This markdown link might be temporarily inaccessible (${status}): ${url}`,
197
+ });
198
+ }
199
+ else {
215
200
  counter && counter.links.error++;
216
201
  errors.push({
217
202
  exercise: exercise === null || exercise === void 0 ? void 0 : exercise.title,
218
- msg: `This link is broken: ${obj[link].mdUrl}`,
203
+ msg: `This markdown link is broken: ${url}`,
219
204
  });
220
205
  }
221
206
  }
222
- catch (_g) {
223
- counter && counter.links.error++;
224
- errors.push({
225
- exercise: exercise === null || exercise === void 0 ? void 0 : exercise.title,
226
- msg: `This link is broken: ${obj[link].mdUrl}`,
227
- });
228
- }
229
207
  }
230
208
  }
231
209
  }
@@ -234,10 +212,10 @@ const checkUrl = async (config, filePath, fileName, exercise, errors, warnings,
234
212
  }
235
213
  return true;
236
214
  };
237
- // This function writes a given file with the given content.
238
- const writeFile = async (content, filePath) => {
215
+ // This function writes a file in the given path.
216
+ const writeFile = (filePath, content) => {
239
217
  try {
240
- await fs.promises.writeFile(filePath, content);
218
+ fs.writeFileSync(filePath, content);
241
219
  }
242
220
  catch (error) {
243
221
  if (error)
@@ -1 +1 @@
1
- {"version":"5.0.21","commands":{"audit":{"id":"audit","description":"learnpack audit is the command in charge of creating an auditory of the repository\n...\nlearnpack audit checks for the following information in a repository:\n 1. The configuration object has slug, repository and description. (Error)\n 2. The command learnpack clean has been run. (Error)\n 3. If a markdown or test file doesn't have any content. (Error)\n 4. The links are accessing to valid servers. (Error)\n 5. The relative images are working (If they have the shortest path to the image or if the images exists in the assets). (Error)\n 6. The external images are working (If they are pointing to a valid server). (Error)\n 7. The exercises directory names are valid. (Error)\n 8. If an exercise doesn't have a README file. (Error)\n 9. The exercises array (Of the config file) has content. (Error)\n 10. The exercses have the same translations. (Warning)\n 11. The .gitignore file exists. (Warning)\n 12. If there is a file within the exercises folder but not inside of any particular exercise's folder. (Warning)\n","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[]},"clean":{"id":"clean","description":"Clean the configuration object\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[]},"download":{"id":"download","description":"Describe the command here\n...\nExtra documentation goes here\n","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"init":{"id":"init","description":"Create a new learning package: Book, Tutorial or Exercise","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"grading":{"name":"grading","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"login":{"id":"login","description":"Describe the command here\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"logout":{"id":"logout","description":"Describe the command here\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"publish":{"id":"publish","description":"Builds the project by copying necessary files and directories into a zip file","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"help":{"name":"help","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"start":{"id":"start","description":"Runs a small server with all the exercise instructions","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"port":{"name":"port","type":"option","char":"p","description":"server port"},"host":{"name":"host","type":"option","char":"h","description":"server host"},"disableGrading":{"name":"disableGrading","type":"boolean","char":"D","description":"disble grading functionality","allowNo":false},"watch":{"name":"watch","type":"boolean","char":"w","description":"Watch for file changes","allowNo":false},"editor":{"name":"editor","type":"option","char":"e","description":"[preview, extension]","options":["extension","preview"]},"version":{"name":"version","type":"option","char":"v","description":"E.g: 1.0.1"},"grading":{"name":"grading","type":"option","char":"g","description":"[isolated, incremental]","options":["isolated","incremental"]},"debug":{"name":"debug","type":"boolean","char":"d","description":"debugger mode for more verbage","allowNo":false}},"args":[]},"test":{"id":"test","description":"Test exercises","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false}},"args":[{"name":"exerciseSlug","description":"The name of the exercise to test","required":false,"hidden":false}]},"translate":{"id":"translate","description":"List all the lessons, the user is able of select many of them to translate to the given languages","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false}},"args":[]}}}
1
+ {"version":"5.0.22","commands":{"audit":{"id":"audit","description":"learnpack audit is the command in charge of creating an auditory of the repository\n...\nlearnpack audit checks for the following information in a repository:\n 1. The configuration object has slug, repository and description. (Error)\n 2. The command learnpack clean has been run. (Error)\n 3. If a markdown or test file doesn't have any content. (Error)\n 4. The links are accessing to valid servers. (Error)\n 5. The relative images are working (If they have the shortest path to the image or if the images exists in the assets). (Error)\n 6. The external images are working (If they are pointing to a valid server). (Error)\n 7. The exercises directory names are valid. (Error)\n 8. If an exercise doesn't have a README file. (Error)\n 9. The exercises array (Of the config file) has content. (Error)\n 10. The exercses have the same translations. (Warning)\n 11. The .gitignore file exists. (Warning)\n 12. If there is a file within the exercises folder but not inside of any particular exercise's folder. (Warning)\n","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[]},"clean":{"id":"clean","description":"Clean the configuration object\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[]},"download":{"id":"download","description":"Describe the command here\n...\nExtra documentation goes here\n","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"init":{"id":"init","description":"Create a new learning package: Book, Tutorial or Exercise","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"grading":{"name":"grading","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"login":{"id":"login","description":"Describe the command here\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"logout":{"id":"logout","description":"Describe the command here\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"publish":{"id":"publish","description":"Builds the project by copying necessary files and directories into a zip file","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"help":{"name":"help","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"start":{"id":"start","description":"Runs a small server with all the exercise instructions","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false},"port":{"name":"port","type":"option","char":"p","description":"server port"},"host":{"name":"host","type":"option","char":"h","description":"server host"},"disableGrading":{"name":"disableGrading","type":"boolean","char":"D","description":"disble grading functionality","allowNo":false},"watch":{"name":"watch","type":"boolean","char":"w","description":"Watch for file changes","allowNo":false},"editor":{"name":"editor","type":"option","char":"e","description":"[preview, extension]","options":["extension","preview"]},"version":{"name":"version","type":"option","char":"v","description":"E.g: 1.0.1"},"grading":{"name":"grading","type":"option","char":"g","description":"[isolated, incremental]","options":["isolated","incremental"]},"debug":{"name":"debug","type":"boolean","char":"d","description":"debugger mode for more verbage","allowNo":false}},"args":[]},"test":{"id":"test","description":"Test exercises","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false}},"args":[{"name":"exerciseSlug","description":"The name of the exercise to test","required":false,"hidden":false}]},"translate":{"id":"translate","description":"List all the lessons, the user is able of select many of them to translate to the given languages","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"yes":{"name":"yes","type":"boolean","char":"y","description":"Skip all prompts and initialize an empty project","allowNo":false}},"args":[]}}}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@learnpack/learnpack",
3
3
  "description": "Seamlessly build, sell and/or take interactive & auto-graded tutorials, start learning now or build a new tutorial to your audience.",
4
- "version": "5.0.21",
4
+ "version": "5.0.22",
5
5
  "author": "Alejandro Sanchez @alesanchezr",
6
6
  "contributors": [
7
7
  {
@@ -1,393 +1,372 @@
1
- import { IAuditErrors } from "../models/audit"
2
- import { IConfigObj } from "../models/config"
3
- import { ICounter } from "../models/counter"
4
- import { IFindings } from "../models/findings"
5
- import { IExercise } from "../models/exercise-obj"
6
- import { IFrontmatter } from "../models/front-matter"
7
- import Console from "./console"
8
- import * as fs from "fs"
9
- import * as path from "path"
10
-
11
- // eslint-disable-next-line
12
- const fetch = require("node-fetch");
13
- // eslint-disable-next-line
14
- const fm = require("front-matter");
15
-
16
- // This function checks if a url is valid.
17
- const isUrl = async (
18
- url: string,
19
- errors: IAuditErrors[],
20
- counter: ICounter
21
- ) => {
22
- const regexUrl = /(https?:\/\/[\w./-]+)/gm
23
- counter.links.total++
24
- if (!regexUrl.test(url)) {
25
- counter.links.error++
26
- errors.push({
27
- exercise: undefined,
28
- msg: `The repository value of the configuration file is not a link: ${url}`,
29
- })
30
- return false
31
- }
32
-
33
- const res = await fetch(url, { method: "HEAD" })
34
- if (!res.ok) {
35
- counter.links.error++
36
- errors.push({
37
- exercise: undefined,
38
- msg: `The link of the repository is broken: ${url}`,
39
- })
40
- }
41
-
42
- return true
43
- }
44
-
45
- const checkForEmptySpaces = (str: string) => {
46
- const isEmpty = true
47
- for (const letter of str) {
48
- if (letter !== " ") {
49
- return false
50
- }
51
- }
52
-
53
- return isEmpty
54
- }
55
-
56
- const checkLearnpackClean = (configObj: IConfigObj, errors: IAuditErrors[]) => {
57
- if (
58
- (configObj.config?.outputPath &&
59
- fs.existsSync(configObj.config?.outputPath)) ||
60
- fs.existsSync(`${configObj.config?.dirPath}/_app`) ||
61
- fs.existsSync(`${configObj.config?.dirPath}/reports`) ||
62
- fs.existsSync(`${configObj.config?.dirPath}/resets`) ||
63
- fs.existsSync(`${configObj.config?.dirPath}/app.tar.gz`) ||
64
- fs.existsSync(`${configObj.config?.dirPath}/config.json`) ||
65
- fs.existsSync(`${configObj.config?.dirPath}/vscode_queue.json`) ||
66
- fs.existsSync(`${configObj.config?.dirPath}/telemetry.json`)
67
- ) {
68
- errors.push({
69
- exercise: undefined,
70
- msg: "You have to run learnpack clean command",
71
- })
72
- }
73
- }
74
-
75
- const findInFile = (types: string[], content: string) => {
76
- const regex: any = {
77
- relativeImages:
78
- /!\[.*]\s*\((((\.\/)?(\.{2}\/){1,5})(.*\/)*(.[^\s/]*\.[A-Za-z]{2,4})\S*)\)/gm,
79
- externalImages: /!\[.*]\((https?:\/(\/[^)/]+)+\/?)\)/gm,
80
- markdownLinks: /(\s)+\[.*]\((https?:\/(\/[^)/]+)+\/?)\)/gm,
81
- url: /(https?:\/\/[\w./-]+)/gm,
82
- uploadcare: /https:\/\/ucarecdn.com\/(?:.*\/)*([\w./-]+)/gm,
83
- }
84
-
85
- const validTypes = Object.keys(regex)
86
- if (!Array.isArray(types))
87
- types = [types]
88
-
89
- const findings: IFindings = {}
90
- type findingsType =
91
- | "relativeImages"
92
- | "externalImages"
93
- | "markdownLinks"
94
- | "url"
95
- | "uploadcare";
96
-
97
- for (const type of types) {
98
- if (!validTypes.includes(type))
99
- throw new Error("Invalid type: " + type)
100
- else
101
- findings[type as findingsType] = {}
102
- }
103
-
104
- for (const type of types) {
105
- let m: RegExpExecArray
106
- while ((m = regex[type].exec(content)) !== null) {
107
- // This is necessary to avoid infinite loops with zero-width matches
108
- if (m.index === regex.lastIndex) {
109
- regex.lastIndex++
110
- }
111
-
112
- // The result can be accessed through the `m`-variable.
113
- // m.forEach((match, groupIndex) => values.push(match));
114
-
115
- findings[type as findingsType]![m[0]] = {
116
- content: m[0],
117
- absUrl: m[1],
118
- mdUrl: m[2],
119
- relUrl: m[6],
120
- }
121
- }
122
- }
123
-
124
- return findings
125
- }
126
-
127
- // This function checks that each of the url's are working.
128
- const checkUrl = async (
129
- config: IConfigObj,
130
- filePath: string,
131
- fileName: string,
132
- exercise: IExercise | undefined,
133
- errors: IAuditErrors[],
134
- warnings: IAuditErrors[],
135
- counter: ICounter | undefined
136
- ) => {
137
- if (!fs.existsSync(filePath))
138
- return false
139
- const content: string = fs.readFileSync(filePath).toString()
140
- const isEmpty = checkForEmptySpaces(content)
141
- if (isEmpty || !content)
142
- errors.push({
143
- exercise: exercise?.title!,
144
- msg: `This file (${fileName}) doesn't have any content inside.`,
145
- })
146
-
147
- const frontmatter: IFrontmatter = fm(content)
148
- for (const attribute in frontmatter.attributes) {
149
- if (
150
- Object.prototype.hasOwnProperty.call(frontmatter.attributes, attribute) &&
151
- (attribute === "intro" || attribute === "tutorial")
152
- ) {
153
- counter && counter.links.total++
154
- try {
155
- // eslint-disable-next-line
156
- let res = await fetch(frontmatter.attributes[attribute], {
157
- method: "HEAD",
158
- })
159
- if (!res.ok) {
160
- counter && counter.links.error++
161
- errors.push({
162
- exercise: exercise?.title!,
163
- msg: `This link is broken (${res.ok}): ${frontmatter.attributes[attribute]}`,
164
- })
165
- }
166
- } catch {
167
- counter && counter.links.error++
168
- errors.push({
169
- exercise: exercise?.title,
170
- msg: `This link is broken: ${frontmatter.attributes[attribute]}`,
171
- })
172
- }
173
- }
174
- }
175
-
176
- // Check url's of each README file.
177
- const findings: IFindings = findInFile(
178
- ["relativeImages", "externalImages", "markdownLinks"],
179
- content
180
- )
181
- type findingsType =
182
- | "relativeImages"
183
- | "externalImages"
184
- | "markdownLinks"
185
- | "url"
186
- | "uploadcare";
187
- for (const finding in findings) {
188
- if (Object.prototype.hasOwnProperty.call(findings, finding)) {
189
- const obj = findings[finding as findingsType]
190
- // Valdites all the relative path images.
191
- if (finding === "relativeImages" && Object.keys(obj!).length > 0) {
192
- for (const img in obj) {
193
- if (Object.prototype.hasOwnProperty.call(obj, img)) {
194
- // Validates if the image is in the assets folder.
195
- counter && counter.images.total++
196
- const relativePath = path
197
- .relative(
198
- exercise ? exercise.path.replace(/\\/gm, "/") : "./",
199
- `${config!.config?.dirPath}/assets/${obj[img].relUrl}`
200
- )
201
- .replace(/\\/gm, "/")
202
- if (relativePath !== obj[img].absUrl.split("?").shift()) {
203
- counter && counter.images.error++
204
- errors.push({
205
- exercise: exercise?.title,
206
- msg: `This relative path (${obj[img].relUrl}) is not pointing to the assets folder.`,
207
- })
208
- }
209
-
210
- if (
211
- !fs.existsSync(
212
- `${config!.config?.dirPath}/assets/${obj[img].relUrl}`
213
- )
214
- ) {
215
- counter && counter.images.error++
216
- errors.push({
217
- exercise: exercise?.title,
218
- msg: `The file ${obj[img].relUrl} doesn't exist in the assets folder.`,
219
- })
220
- }
221
- }
222
- }
223
- } else if (finding === "externalImages" && Object.keys(obj!).length > 0) {
224
- // Valdites all the aboslute path images.
225
- for (const img in obj) {
226
- if (Object.prototype.hasOwnProperty.call(obj, img)) {
227
- counter && counter.images.total++
228
- if (
229
- fs.existsSync(
230
- `${config!.config?.dirPath}/assets${obj[img].mdUrl
231
- .split("?")
232
- .shift()}`
233
- )
234
- ) {
235
- const relativePath = path
236
- .relative(
237
- exercise ? exercise.path.replace(/\\/gm, "/") : "./",
238
- `${config!.config?.dirPath}/assets/${obj[img].mdUrl}`
239
- )
240
- .replace(/\\/gm, "/")
241
- warnings.push({
242
- exercise: exercise?.title,
243
- msg: `On this exercise you have an image with an absolute path "${obj[img].absUrl}". We recommend you to replace it by the relative path: "${relativePath}".`,
244
- })
245
- }
246
-
247
- try {
248
- // eslint-disable-next-line
249
- let res = await fetch(obj[img].absUrl, {
250
- method: "HEAD",
251
- })
252
- if (!res.ok) {
253
- counter && counter.images.error++
254
- errors.push({
255
- exercise: exercise?.title,
256
- msg: `This link is broken: ${obj[img].absUrl}`,
257
- })
258
- }
259
- } catch {
260
- counter && counter.images.error++
261
- errors.push({
262
- exercise: exercise?.title,
263
- msg: `This link is broken: ${obj[img].absUrl}`,
264
- })
265
- }
266
- }
267
- }
268
- } else if (finding === "markdownLinks" && Object.keys(obj!).length > 0) {
269
- for (const link in obj) {
270
- if (Object.prototype.hasOwnProperty.call(obj, link)) {
271
- counter && counter.links.total++
272
- if (!obj[link].mdUrl.includes("twitter")) {
273
- try {
274
- // eslint-disable-next-line
275
- let res = await fetch(obj[link].mdUrl, {
276
- method: "HEAD",
277
- })
278
- if (res.status > 399 && res.status < 500) {
279
- counter && counter.links.error++
280
- errors.push({
281
- exercise: exercise?.title,
282
- msg: `This link is broken: ${obj[link].mdUrl}`,
283
- })
284
- }
285
- } catch {
286
- counter && counter.links.error++
287
- errors.push({
288
- exercise: exercise?.title,
289
- msg: `This link is broken: ${obj[link].mdUrl}`,
290
- })
291
- }
292
- }
293
- }
294
- }
295
- }
296
- }
297
- }
298
-
299
- return true
300
- }
301
-
302
- // This function writes a given file with the given content.
303
- const writeFile = async (content: string, filePath: string) => {
304
- try {
305
- await fs.promises.writeFile(filePath, content)
306
- } catch (error) {
307
- if (error)
308
- Console.error(
309
- `We weren't able to write the file in this path "${filePath}".`,
310
- error
311
- )
312
- }
313
- }
314
-
315
- // This function checks if there are errors, and show them in the console at the end.
316
- const showErrors = (errors: IAuditErrors[], counter: ICounter | undefined) => {
317
- return new Promise((resolve, reject) => {
318
- if (errors) {
319
- if (errors.length > 0) {
320
- Console.log("Checking for errors...")
321
- for (const [i, error] of errors.entries())
322
- Console.error(
323
- `${i + 1}) ${error.msg} ${
324
- error.exercise ? `(Exercise: ${error.exercise})` : ""
325
- }`
326
- )
327
- if (counter) {
328
- Console.error(
329
- ` We found ${errors.length} error${
330
- errors.length > 1 ? "s" : ""
331
- } among ${counter.images.total} images, ${
332
- counter.links.total
333
- } link, ${counter.readmeFiles} README files and ${
334
- counter.exercises
335
- } exercises.`
336
- )
337
- } else {
338
- Console.error(
339
- ` We found ${errors.length} error${
340
- errors.length > 1 ? "s" : ""
341
- } related with the project integrity.`
342
- )
343
- }
344
-
345
- process.exit(1)
346
- } else {
347
- if (counter) {
348
- Console.success(
349
- `We didn't find any errors in this repository among ${counter.images.total} images, ${counter.links.total} link, ${counter.readmeFiles} README files and ${counter.exercises} exercises.`
350
- )
351
- } else {
352
- Console.success(`We didn't find any errors in this repository.`)
353
- }
354
-
355
- process.exit(0)
356
- }
357
- } else {
358
- reject("Failed")
359
- }
360
- })
361
- }
362
-
363
- // This function checks if there are warnings, and show them in the console at the end.
364
- const showWarnings = (warnings: IAuditErrors[]) => {
365
- return new Promise((resolve, reject) => {
366
- if (warnings) {
367
- if (warnings.length > 0) {
368
- Console.log("Checking for warnings...")
369
- for (const [i, warning] of warnings.entries())
370
- Console.warning(
371
- `${i + 1}) ${warning.msg} ${
372
- warning.exercise ? `File: ${warning.exercise}` : ""
373
- }`
374
- )
375
- }
376
-
377
- resolve("SUCCESS")
378
- } else {
379
- reject("Failed")
380
- }
381
- })
382
- }
383
-
384
- export default {
385
- isUrl,
386
- checkForEmptySpaces,
387
- checkLearnpackClean,
388
- findInFile,
389
- checkUrl,
390
- writeFile,
391
- showErrors,
392
- showWarnings,
393
- }
1
+ /* eslint-disable no-await-in-loop, @typescript-eslint/no-non-null-asserted-optional-chain, no-promise-executor-return */
2
+
3
+ import { IAuditErrors } from "../models/audit"
4
+ import { IConfigObj } from "../models/config"
5
+ import { ICounter } from "../models/counter"
6
+ import { IFindings } from "../models/findings"
7
+ import { IExercise } from "../models/exercise-obj"
8
+ import { IFrontmatter } from "../models/front-matter"
9
+ import Console from "./console"
10
+ import * as fs from "fs"
11
+ import * as path from "path"
12
+
13
+ // eslint-disable-next-line
14
+ const fetch = require("node-fetch")
15
+ // eslint-disable-next-line
16
+ const fm = require("front-matter")
17
+
18
+ // This function checks if a url is valid.
19
+ const isUrl = async (
20
+ url: string,
21
+ errors: IAuditErrors[],
22
+ counter: ICounter
23
+ ) => {
24
+ const regexUrl = /(https?:\/\/[\w./-]+)/gm
25
+ counter.links.total++
26
+ if (!regexUrl.test(url)) {
27
+ counter.links.error++
28
+ errors.push({
29
+ exercise: undefined,
30
+ msg: `The repository value of the configuration file is not a link: ${url}`,
31
+ })
32
+ return false
33
+ }
34
+
35
+ const res = await fetch(url, { method: "HEAD" })
36
+ if (!res.ok) {
37
+ counter.links.error++
38
+ errors.push({
39
+ exercise: undefined,
40
+ msg: `The link of the repository is broken: ${url}`,
41
+ })
42
+ }
43
+
44
+ return true
45
+ }
46
+
47
+ const checkForEmptySpaces = (str: string) => {
48
+ const isEmpty = true
49
+ for (const letter of str) {
50
+ if (letter !== " ") {
51
+ return false
52
+ }
53
+ }
54
+
55
+ return isEmpty
56
+ }
57
+
58
+ const checkLearnpackClean = (configObj: IConfigObj, errors: IAuditErrors[]) => {
59
+ if (
60
+ (configObj.config?.outputPath &&
61
+ fs.existsSync(configObj.config?.outputPath)) ||
62
+ fs.existsSync(`${configObj.config?.dirPath}/_app`) ||
63
+ fs.existsSync(`${configObj.config?.dirPath}/reports`) ||
64
+ fs.existsSync(`${configObj.config?.dirPath}/resets`) ||
65
+ fs.existsSync(`${configObj.config?.dirPath}/app.tar.gz`) ||
66
+ fs.existsSync(`${configObj.config?.dirPath}/config.json`) ||
67
+ fs.existsSync(`${configObj.config?.dirPath}/vscode_queue.json`) ||
68
+ fs.existsSync(`${configObj.config?.dirPath}/telemetry.json`)
69
+ ) {
70
+ errors.push({
71
+ exercise: undefined,
72
+ msg: "You have to run learnpack clean command",
73
+ })
74
+ }
75
+ }
76
+
77
+ const findInFile = (types: string[], content: string) => {
78
+ const regex: any = {
79
+ relativeImages:
80
+ /!\[.*]\s*\((((\.\/)?(\.{2}\/){1,5})(.*\/)*(.[^\s/]*\.[A-Za-z]{2,4})\S*)\)/gm,
81
+ externalImages: /!\[.*]\((https?:\/(\/[^)/]+)+\/?)\)/gm,
82
+ markdownLinks: /(\s)+\[.*]\((https?:\/(\/[^)/]+)+\/?)\)/gm,
83
+ url: /(https?:\/\/[\w./-]+)/gm,
84
+ uploadcare: /https:\/\/ucarecdn.com\/(?:.*\/)*([\w./-]+)/gm,
85
+ }
86
+
87
+ const validTypes = Object.keys(regex)
88
+ if (!Array.isArray(types))
89
+ types = [types]
90
+
91
+ const findings: IFindings = {}
92
+ type findingsType =
93
+ | "relativeImages"
94
+ | "externalImages"
95
+ | "markdownLinks"
96
+ | "url"
97
+ | "uploadcare"
98
+
99
+ for (const type of types) {
100
+ if (!validTypes.includes(type))
101
+ throw new Error("Invalid type: " + type)
102
+ else
103
+ findings[type as findingsType] = {}
104
+ }
105
+
106
+ for (const type of types) {
107
+ let m: RegExpExecArray
108
+ while ((m = regex[type].exec(content)) !== null) {
109
+ // This is necessary to avoid infinite loops with zero-width matches
110
+ if (m.index === regex.lastIndex) {
111
+ regex.lastIndex++
112
+ }
113
+
114
+ // The result can be accessed through the `m`-variable.
115
+ // m.forEach((match, groupIndex) => values.push(match));
116
+
117
+ findings[type as findingsType]![m[0]] = {
118
+ content: m[0],
119
+ absUrl: m[1],
120
+ mdUrl: m[2],
121
+ relUrl: m[6],
122
+ }
123
+ }
124
+ }
125
+
126
+ return findings
127
+ }
128
+
129
+ const checkLinkWithRetry = async (
130
+ url: string,
131
+ retries = 3,
132
+ delay = 1000
133
+ ): Promise<{ isValid: boolean; status?: number }> => {
134
+ for (let attempt = 1; attempt <= retries; attempt++) {
135
+ try {
136
+ let res = await fetch(url, { method: "HEAD" })
137
+
138
+ if (res.status === 429) {
139
+ await new Promise(resolve => setTimeout(resolve, delay))
140
+ delay *= 2 // Exponential backoff
141
+ continue
142
+ }
143
+
144
+ if (res.status === 403) {
145
+ return { isValid: false, status: 403 }
146
+ }
147
+
148
+ if (!res.ok) {
149
+ res = await fetch(url, { method: "GET" })
150
+
151
+ if (!res.ok) {
152
+ return { isValid: false, status: res.status }
153
+ }
154
+ }
155
+
156
+ return { isValid: true }
157
+ } catch (error) {
158
+ console.debug(`Error checking link ${url}:`, error)
159
+ return { isValid: false, status: 429 }
160
+ }
161
+ }
162
+
163
+ return { isValid: false, status: 429 }
164
+ }
165
+
166
+ const checkUrl = async (
167
+ config: IConfigObj,
168
+ filePath: string,
169
+ fileName: string,
170
+ exercise: IExercise | undefined,
171
+ errors: IAuditErrors[],
172
+ warnings: IAuditErrors[],
173
+ counter: ICounter | undefined
174
+ ) => {
175
+ if (!fs.existsSync(filePath))
176
+ return false
177
+ const content: string = fs.readFileSync(filePath).toString()
178
+ const isEmpty = checkForEmptySpaces(content)
179
+ if (isEmpty || !content)
180
+ errors.push({
181
+ exercise: exercise?.title!,
182
+ msg: `This file (${fileName}) doesn't have any content inside.`,
183
+ })
184
+
185
+ const frontmatter: IFrontmatter = fm(content)
186
+ for (const attribute in frontmatter.attributes) {
187
+ if (
188
+ Object.prototype.hasOwnProperty.call(frontmatter.attributes, attribute) &&
189
+ (attribute === "intro" || attribute === "tutorial")
190
+ ) {
191
+ counter && counter.links.total++
192
+ const url = frontmatter.attributes[attribute]
193
+
194
+ const { isValid, status } = await checkLinkWithRetry(url)
195
+
196
+ if (!isValid) {
197
+ if (status === 429 || status === 403) {
198
+ warnings.push({
199
+ exercise: exercise?.title!,
200
+ msg: `Warning: This link might be temporarily inaccessible (${status}): ${url}`,
201
+ })
202
+ } else {
203
+ counter && counter.links.error++
204
+ errors.push({
205
+ exercise: exercise?.title!,
206
+ msg: `This link is broken: ${url}`,
207
+ })
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ // Check URLs in README files
214
+ const findings: IFindings = findInFile(
215
+ ["relativeImages", "externalImages", "markdownLinks"],
216
+ content
217
+ )
218
+ type findingsType = "relativeImages" | "externalImages" | "markdownLinks"
219
+
220
+ for (const finding in findings) {
221
+ if (Object.prototype.hasOwnProperty.call(findings, finding)) {
222
+ const obj = findings[finding as findingsType]
223
+
224
+ if (finding === "externalImages" && Object.keys(obj!).length > 0) {
225
+ for (const img in obj) {
226
+ if (Object.prototype.hasOwnProperty.call(obj, img)) {
227
+ counter && counter.images.total++
228
+ const url = obj[img].absUrl
229
+
230
+ const { isValid, status } = await checkLinkWithRetry(url)
231
+
232
+ if (!isValid) {
233
+ if (status === 429 || status === 403) {
234
+ warnings.push({
235
+ exercise: exercise?.title,
236
+ msg: `Warning: This image link might be temporarily inaccessible (${status}): ${url}`,
237
+ })
238
+ } else {
239
+ counter && counter.images.error++
240
+ errors.push({
241
+ exercise: exercise?.title,
242
+ msg: `This image link is broken: ${url}`,
243
+ })
244
+ }
245
+ }
246
+ }
247
+ }
248
+ } else if (finding === "markdownLinks" && Object.keys(obj!).length > 0) {
249
+ for (const link in obj) {
250
+ if (Object.prototype.hasOwnProperty.call(obj, link)) {
251
+ counter && counter.links.total++
252
+ const url = obj[link].mdUrl
253
+
254
+ if (!url.includes("twitter")) {
255
+ const { isValid, status } = await checkLinkWithRetry(url)
256
+
257
+ if (!isValid) {
258
+ if (status === 429 || status === 403) {
259
+ warnings.push({
260
+ exercise: exercise?.title,
261
+ msg: `Warning: This markdown link might be temporarily inaccessible (${status}): ${url}`,
262
+ })
263
+ } else {
264
+ counter && counter.links.error++
265
+ errors.push({
266
+ exercise: exercise?.title,
267
+ msg: `This markdown link is broken: ${url}`,
268
+ })
269
+ }
270
+ }
271
+ }
272
+ }
273
+ }
274
+ }
275
+ }
276
+ }
277
+
278
+ return true
279
+ }
280
+
281
+ // This function writes a file in the given path.
282
+ const writeFile = (filePath: string, content: string) => {
283
+ try {
284
+ fs.writeFileSync(filePath, content)
285
+ } catch (error) {
286
+ if (error)
287
+ Console.error(
288
+ `We weren't able to write the file in this path "${filePath}".`,
289
+ error
290
+ )
291
+ }
292
+ }
293
+
294
+ // This function checks if there are errors, and show them in the console at the end.
295
+ const showErrors = (errors: IAuditErrors[], counter: ICounter | undefined) => {
296
+ return new Promise((resolve, reject) => {
297
+ if (errors) {
298
+ if (errors.length > 0) {
299
+ Console.log("Checking for errors...")
300
+ for (const [i, error] of errors.entries())
301
+ Console.error(
302
+ `${i + 1}) ${error.msg} ${
303
+ error.exercise ? `(Exercise: ${error.exercise})` : ""
304
+ }`
305
+ )
306
+ if (counter) {
307
+ Console.error(
308
+ ` We found ${errors.length} error${
309
+ errors.length > 1 ? "s" : ""
310
+ } among ${counter.images.total} images, ${
311
+ counter.links.total
312
+ } link, ${counter.readmeFiles} README files and ${
313
+ counter.exercises
314
+ } exercises.`
315
+ )
316
+ } else {
317
+ Console.error(
318
+ ` We found ${errors.length} error${
319
+ errors.length > 1 ? "s" : ""
320
+ } related with the project integrity.`
321
+ )
322
+ }
323
+
324
+ process.exit(1)
325
+ } else {
326
+ if (counter) {
327
+ Console.success(
328
+ `We didn't find any errors in this repository among ${counter.images.total} images, ${counter.links.total} link, ${counter.readmeFiles} README files and ${counter.exercises} exercises.`
329
+ )
330
+ } else {
331
+ Console.success(`We didn't find any errors in this repository.`)
332
+ }
333
+
334
+ process.exit(0)
335
+ }
336
+ } else {
337
+ reject("Failed")
338
+ }
339
+ })
340
+ }
341
+
342
+ // This function checks if there are warnings, and show them in the console at the end.
343
+ const showWarnings = (warnings: IAuditErrors[]) => {
344
+ return new Promise((resolve, reject) => {
345
+ if (warnings) {
346
+ if (warnings.length > 0) {
347
+ Console.log("Checking for warnings...")
348
+ for (const [i, warning] of warnings.entries())
349
+ Console.warning(
350
+ `${i + 1}) ${warning.msg} ${
351
+ warning.exercise ? `File: ${warning.exercise}` : ""
352
+ }`
353
+ )
354
+ }
355
+
356
+ resolve("SUCCESS")
357
+ } else {
358
+ reject("Failed")
359
+ }
360
+ })
361
+ }
362
+
363
+ export default {
364
+ isUrl,
365
+ checkForEmptySpaces,
366
+ checkLearnpackClean,
367
+ findInFile,
368
+ checkUrl,
369
+ writeFile,
370
+ showErrors,
371
+ showWarnings,
372
+ }