@ijuantm/simpl-addon 2.0.0 → 2.1.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/install.js +62 -55
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -56,5 +56,5 @@ The installer:
56
56
 
57
57
  ## Requirements
58
58
 
59
- - **Node.js**: >= 20.x.x
59
+ - **Node.js**: >= 22.x.x
60
60
  - **Simpl Framework**: A (preferably clean) installation of Simpl, if not clean, some manual merging may be required, or the installer may skip files or break things (you have been warned).
package/install.js CHANGED
@@ -13,6 +13,7 @@ const COLORS = {
13
13
  };
14
14
 
15
15
  const CDN_BASE = 'https://cdn.simpl.iwanvanderwal.nl/framework';
16
+ const LOCAL_RELEASES_DIR = process.env.SIMPL_LOCAL_RELEASES || path.join(process.cwd(), 'local-releases');
16
17
 
17
18
  const log = (message, color = 'reset') => console.log(`${COLORS[color]}${message}${COLORS.reset}`);
18
19
 
@@ -61,8 +62,7 @@ const getSimplVersion = () => {
61
62
 
62
63
  if (!fs.existsSync(simplFile)) throw new Error('Not a Simpl project. Missing .simpl file in current directory.');
63
64
 
64
- const content = fs.readFileSync(simplFile, 'utf8');
65
- const config = JSON.parse(content);
65
+ const config = JSON.parse(fs.readFileSync(simplFile, 'utf8'));
66
66
 
67
67
  if (!config.version) throw new Error('Invalid .simpl file: missing version field');
68
68
 
@@ -97,6 +97,18 @@ const showHelp = () => {
97
97
  console.log();
98
98
  };
99
99
 
100
+ const checkServerAvailability = () => new Promise(resolve => {
101
+ const req = https.get(`${CDN_BASE}/versions.json`, {timeout: 5000}, res => {
102
+ res.resume();
103
+ resolve(res.statusCode === 200);
104
+ });
105
+ req.on('error', () => resolve(false));
106
+ req.on('timeout', () => {
107
+ req.destroy();
108
+ resolve(false);
109
+ });
110
+ });
111
+
100
112
  const listAddons = async (version) => {
101
113
  console.log();
102
114
  log(` ╭${'─'.repeat(62)}╮`);
@@ -106,8 +118,17 @@ const listAddons = async (version) => {
106
118
  log(' 📦 Fetching available add-ons...', 'bold');
107
119
 
108
120
  try {
109
- const response = await fetchUrl(`${CDN_BASE}/${version}/add-ons/list.json`);
110
- const addons = JSON.parse(String(response))['add-ons'];
121
+ const localListPath = path.join(LOCAL_RELEASES_DIR, version, 'add-ons', 'list.json');
122
+ let addons;
123
+
124
+ if (fs.existsSync(localListPath)) {
125
+ console.log();
126
+ log(` 💻 Using local add-ons list`, 'bold');
127
+ addons = JSON.parse(fs.readFileSync(localListPath, 'utf8'))['add-ons'];
128
+ } else {
129
+ if (!await checkServerAvailability()) throw new Error('CDN server is currently unreachable');
130
+ addons = JSON.parse(await fetchUrl(`${CDN_BASE}/${version}/add-ons/list.json`))['add-ons'];
131
+ }
111
132
 
112
133
  console.log();
113
134
 
@@ -115,9 +136,8 @@ const listAddons = async (version) => {
115
136
  else addons.forEach(name => log(` ${COLORS.cyan}•${COLORS.reset} ${name}`));
116
137
  } catch (error) {
117
138
  console.log();
118
- log(` ${COLORS.red}✗${COLORS.reset} Failed to fetch add-ons: ${error.message}`, 'red');
139
+ log(` ${COLORS.red}✗${COLORS.reset} Failed to fetch add-ons`, 'red');
119
140
  console.log();
120
-
121
141
  process.exit(1);
122
142
  }
123
143
 
@@ -142,13 +162,10 @@ const extractMarkers = (content) => {
142
162
 
143
163
  const collectContentBetweenMarkers = (lines, startIndex) => {
144
164
  const content = [];
145
-
146
165
  for (let i = startIndex + 1; i < lines.length; i++) {
147
166
  if (lines[i].trim().includes('@addon-end')) break;
148
-
149
167
  content.push(lines[i]);
150
168
  }
151
-
152
169
  return content;
153
170
  };
154
171
 
@@ -166,7 +183,6 @@ const processEnvContent = (content, targetContent) => {
166
183
  }
167
184
 
168
185
  const match = line.match(/^([A-Z_][A-Z0-9_]*)=/);
169
-
170
186
  if (match && !new RegExp(`^${match[1]}=`, 'm').test(targetContent)) envVarsToAdd.push(line);
171
187
  });
172
188
 
@@ -182,19 +198,16 @@ const mergeFile = (targetPath, addonContent, markers, isEnv = false) => {
182
198
  const targetContent = fs.readFileSync(targetPath, 'utf8');
183
199
  const addonLines = addonContent.split('\n');
184
200
  const operations = [];
185
-
186
201
  let newContent = targetContent;
187
202
 
188
203
  markers.forEach(marker => {
189
204
  let content = collectContentBetweenMarkers(addonLines, marker.lineIndex);
190
-
191
205
  if (content.length === 0) return;
192
206
 
193
207
  let lineCount = content.length;
194
208
 
195
209
  if (isEnv) {
196
210
  const processed = processEnvContent(content, newContent);
197
-
198
211
  content = processed.content;
199
212
  lineCount = processed.count;
200
213
 
@@ -214,13 +227,10 @@ const mergeFile = (targetPath, addonContent, markers, isEnv = false) => {
214
227
 
215
228
  if (marker.type === 'prepend') {
216
229
  newContent = content.join('\n') + '\n' + newContent;
217
-
218
230
  operations.push({success: true, type: 'prepend', lines: lineCount});
219
231
  } else if (marker.type === 'append') {
220
232
  if (!newContent.endsWith('\n')) newContent += '\n';
221
-
222
233
  newContent += '\n' + content.join('\n') + '\n';
223
-
224
234
  operations.push({success: true, type: 'append', lines: lineCount});
225
235
  } else if ((marker.type === 'after' || marker.type === 'before') && marker.searchText) {
226
236
  const targetLines = newContent.split('\n');
@@ -232,9 +242,7 @@ const mergeFile = (targetPath, addonContent, markers, isEnv = false) => {
232
242
  }
233
243
 
234
244
  targetLines.splice(insertIndex, 0, ...content);
235
-
236
245
  newContent = targetLines.join('\n');
237
-
238
246
  operations.push({success: true, type: marker.type, lines: lineCount, searchText: marker.searchText});
239
247
  }
240
248
  });
@@ -247,13 +255,11 @@ const mergeFile = (targetPath, addonContent, markers, isEnv = false) => {
247
255
  const printMergeResults = (relativePath, isEnv, result) => {
248
256
  const indent = ' ';
249
257
  const varText = isEnv ? 'environment variable' : 'line';
250
-
251
258
  let hasChanges = false;
252
259
 
253
260
  result.operations.forEach(op => {
254
261
  if (op.success) {
255
262
  hasChanges = true;
256
-
257
263
  if (op.type === 'prepend') log(`${indent}${COLORS.green}✓${COLORS.reset} Prepended ${COLORS.bold}${op.lines}${COLORS.reset} ${varText}${op.lines !== 1 ? 's' : ''} to file start`);
258
264
  else if (op.type === 'append') log(`${indent}${COLORS.green}✓${COLORS.reset} Appended ${COLORS.bold}${op.lines}${COLORS.reset} ${varText}${op.lines !== 1 ? 's' : ''} to file end`);
259
265
  else if (op.type === 'after') log(`${indent}${COLORS.green}✓${COLORS.reset} Inserted ${COLORS.bold}${op.lines}${COLORS.reset} ${varText}${op.lines !== 1 ? 's' : ''} ${COLORS.cyan}after${COLORS.reset} "${COLORS.dim}${op.searchText}${COLORS.reset}"`);
@@ -265,19 +271,21 @@ const printMergeResults = (relativePath, isEnv, result) => {
265
271
  return hasChanges;
266
272
  };
267
273
 
268
- const extractZip = async zipPath => {
269
- const tempExtract = path.join(process.cwd(), '__temp_extract_addon__');
274
+ const extractZip = async (zipPath, destDir) => {
275
+ fs.mkdirSync(destDir, {recursive: true});
270
276
 
271
- if (process.platform === 'win32') {
272
- await execAsync(`powershell -command "Expand-Archive -Path '${zipPath}' -DestinationPath '${tempExtract}' -Force"`);
273
- } else {
274
- await execAsync(`unzip -q "${zipPath}" -d "${tempExtract}"`);
275
- }
277
+ if (process.platform === 'win32') await execAsync(`powershell -command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"`);
278
+ else await execAsync(`unzip -q "${zipPath}" -d "${destDir}"`);
279
+
280
+ const entries = fs.readdirSync(destDir, {withFileTypes: true});
276
281
 
277
- const entries = fs.readdirSync(tempExtract, {withFileTypes: true});
278
- const sourceDir = entries.length === 1 && entries[0].isDirectory() ? path.join(tempExtract, entries[0].name) : tempExtract;
282
+ if (entries.length === 1 && entries[0].isDirectory()) {
283
+ const nestedDir = path.join(destDir, entries[0].name);
284
+ fs.readdirSync(nestedDir).forEach(item => fs.renameSync(path.join(nestedDir, item), path.join(destDir, item)));
285
+ fs.rmdirSync(nestedDir);
286
+ }
279
287
 
280
- return {sourceDir, tempExtract};
288
+ return destDir;
281
289
  };
282
290
 
283
291
  const processAddonFiles = (addonDir, targetDir) => {
@@ -290,19 +298,18 @@ const processAddonFiles = (addonDir, targetDir) => {
290
298
  const relativePath = path.join(basePath, entry.name).replace(/\\/g, '/');
291
299
  const destPath = path.join(targetDir, relativePath);
292
300
 
293
- if (entry.isDirectory()) processDirectory(srcPath, relativePath);
294
- else {
301
+ if (entry.isDirectory()) {
302
+ processDirectory(srcPath, relativePath);
303
+ } else {
295
304
  const content = fs.readFileSync(srcPath, 'utf8');
296
305
 
297
306
  if (fs.existsSync(destPath)) {
298
307
  const markers = extractMarkers(content);
299
-
300
308
  if (markers.length > 0 || entry.name === '.env') toMerge.push({content, destPath, relativePath, markers});
301
309
  else skipped.push(relativePath);
302
310
  } else {
303
311
  fs.mkdirSync(path.dirname(destPath), {recursive: true});
304
312
  fs.copyFileSync(srcPath, destPath);
305
-
306
313
  copied.push(relativePath);
307
314
  }
308
315
  }
@@ -314,21 +321,30 @@ const processAddonFiles = (addonDir, targetDir) => {
314
321
  };
315
322
 
316
323
  const downloadAddon = async (addonName, version, targetDir) => {
317
- const zipUrl = `${CDN_BASE}/${version}/add-ons/${addonName}.zip`;
318
- const tempZip = path.join(process.cwd(), `temp-addon-${addonName}.zip`);
324
+ const localZipPath = path.join(LOCAL_RELEASES_DIR, version, 'add-ons', `${addonName}.zip`);
325
+ const tempExtract = path.join(process.cwd(), '__temp_extract_addon__');
319
326
 
320
327
  try {
321
- await downloadFile(zipUrl, tempZip);
328
+ if (fs.existsSync(localZipPath)) {
329
+ console.log();
330
+ log(` 💻 Using local add-on files`, 'bold');
331
+ const sourceDir = await extractZip(localZipPath, tempExtract);
332
+ const result = processAddonFiles(sourceDir, targetDir);
333
+ fs.rmSync(tempExtract, {recursive: true, force: true});
334
+ return result;
335
+ }
322
336
 
323
- const {sourceDir, tempExtract} = await extractZip(tempZip);
324
- const result = processAddonFiles(sourceDir, targetDir);
337
+ if (!await checkServerAvailability()) throw new Error('CDN server is currently unreachable');
325
338
 
339
+ const tempZip = path.join(process.cwd(), `temp-addon-${addonName}.zip`);
340
+ await downloadFile(`${CDN_BASE}/${version}/add-ons/${addonName}.zip`, tempZip);
341
+ const sourceDir = await extractZip(tempZip, tempExtract);
342
+ const result = processAddonFiles(sourceDir, targetDir);
326
343
  fs.unlinkSync(tempZip);
327
344
  fs.rmSync(tempExtract, {recursive: true, force: true});
328
-
329
345
  return result;
330
346
  } catch (error) {
331
- if (fs.existsSync(tempZip)) fs.unlinkSync(tempZip);
347
+ if (fs.existsSync(tempExtract)) fs.rmSync(tempExtract, {recursive: true, force: true});
332
348
  throw error;
333
349
  }
334
350
  };
@@ -340,17 +356,14 @@ const mergeFiles = (toMerge) => {
340
356
 
341
357
  toMerge.forEach(({content, destPath, relativePath, markers}) => {
342
358
  const isEnv = path.basename(destPath) === '.env';
343
-
344
359
  log(`\n ${COLORS.cyan}•${COLORS.reset} ${COLORS.dim}${relativePath}${COLORS.reset}`);
345
360
 
346
361
  try {
347
362
  const result = mergeFile(destPath, content, markers, isEnv);
348
-
349
363
  if (printMergeResults(relativePath, isEnv, result)) merged.push(relativePath);
350
364
  else unchanged.push(relativePath);
351
365
  } catch (error) {
352
366
  log(` ${COLORS.red}✗ Error:${COLORS.reset} ${error.message}`, 'red');
353
-
354
367
  failed.push(relativePath);
355
368
  }
356
369
  });
@@ -364,7 +377,6 @@ const main = async () => {
364
377
 
365
378
  if (!firstArg || firstArg === '--help' || firstArg === '-h') {
366
379
  showHelp();
367
-
368
380
  process.exit(0);
369
381
  }
370
382
 
@@ -376,13 +388,11 @@ const main = async () => {
376
388
  console.log();
377
389
  log(` ${COLORS.red}✗${COLORS.reset} ${error.message}`, 'red');
378
390
  console.log();
379
-
380
391
  process.exit(1);
381
392
  }
382
393
 
383
394
  if (firstArg === '--list' || firstArg === '-l') {
384
395
  await listAddons(version);
385
-
386
396
  process.exit(0);
387
397
  }
388
398
 
@@ -401,10 +411,10 @@ const main = async () => {
401
411
  ({copied, skipped, toMerge} = await downloadAddon(addonName, version, process.cwd()));
402
412
  } catch (error) {
403
413
  console.log();
404
- log(` ${COLORS.red}✗${COLORS.reset} ${error.message}`, 'red');
405
- log(` ${COLORS.dim}Run ${COLORS.dim}npx @ijuantm/simpl-addon --list${COLORS.reset} to see available add-ons`);
414
+ log(` ${COLORS.red}✗${COLORS.reset} Installation failed`, 'red');
415
+ if (error.message === 'CDN server is currently unreachable') log(` ${COLORS.dim}The CDN server is currently unavailable. Please try again later.${COLORS.reset}`);
416
+ else log(` ${COLORS.dim}Run ${COLORS.dim}npx @ijuantm/simpl-addon --list${COLORS.reset} to see available add-ons`);
406
417
  console.log();
407
-
408
418
  process.exit(1);
409
419
  }
410
420
 
@@ -416,7 +426,6 @@ const main = async () => {
416
426
  if (skipped.length > 0) {
417
427
  console.log();
418
428
  log(` ${COLORS.gray}○${COLORS.reset} ${COLORS.dim}Skipped ${skipped.length} file${skipped.length !== 1 ? 's' : ''} (no merge markers):${COLORS.reset}`);
419
-
420
429
  skipped.forEach(file => log(` ${COLORS.dim}• ${file}${COLORS.reset}`));
421
430
  }
422
431
 
@@ -436,7 +445,6 @@ const main = async () => {
436
445
  console.log();
437
446
  log(` ${COLORS.yellow}⚠${COLORS.reset} ${COLORS.yellow}${failed.length} file${failed.length !== 1 ? 's' : ''} failed to merge${COLORS.reset}`);
438
447
  log(` ${COLORS.yellow}Please review manually:${COLORS.reset}`);
439
-
440
448
  failed.forEach(file => log(` ${COLORS.cyan}• ${file}${COLORS.reset}`));
441
449
  }
442
450
  }
@@ -446,8 +454,7 @@ const main = async () => {
446
454
  console.log();
447
455
  };
448
456
 
449
- main().catch(err => {
450
- log(`\n ${COLORS.red}✗${COLORS.reset} Fatal error: ${err.message}\n`, 'red');
451
-
457
+ main().catch(() => {
458
+ log(`\n ${COLORS.red}✗${COLORS.reset} Fatal error occurred\n`, 'red');
452
459
  process.exit(1);
453
460
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ijuantm/simpl-addon",
3
3
  "description": "CLI tool to install Simpl framework add-ons.",
4
- "version": "2.0.0",
4
+ "version": "2.1.0",
5
5
  "scripts": {
6
6
  "link": "npm link",
7
7
  "unlink": "npm unlink -g @ijuantm/simpl-addon"
@@ -11,7 +11,7 @@
11
11
  "simpl-addon": "install.js"
12
12
  },
13
13
  "engines": {
14
- "node": ">=20.0.0"
14
+ "node": ">=22"
15
15
  },
16
16
  "repository": {
17
17
  "type": "git",