@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.
- package/README.md +1 -1
- package/install.js +62 -55
- 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**: >=
|
|
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
|
|
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
|
|
110
|
-
|
|
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
|
|
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
|
-
|
|
274
|
+
const extractZip = async (zipPath, destDir) => {
|
|
275
|
+
fs.mkdirSync(destDir, {recursive: true});
|
|
270
276
|
|
|
271
|
-
if (process.platform === 'win32') {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
278
|
-
|
|
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
|
|
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())
|
|
294
|
-
|
|
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
|
|
318
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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}
|
|
405
|
-
log(` ${COLORS.dim}
|
|
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(
|
|
450
|
-
log(`\n ${COLORS.red}✗${COLORS.reset} Fatal error
|
|
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.
|
|
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": ">=
|
|
14
|
+
"node": ">=22"
|
|
15
15
|
},
|
|
16
16
|
"repository": {
|
|
17
17
|
"type": "git",
|