@c6fc/spellcraft 0.0.3 → 0.0.5
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/bin/spellcraft.js +5 -10
- package/package.json +2 -1
- package/src/index.js +58 -61
package/bin/spellcraft.js
CHANGED
@@ -11,6 +11,7 @@
|
|
11
11
|
'use strict';
|
12
12
|
|
13
13
|
const yargs = require('yargs');
|
14
|
+
const colors = require('@colors/colors');
|
14
15
|
const { hideBin } = require('yargs/helpers');
|
15
16
|
const { SpellFrame } = require('../src/index.js');
|
16
17
|
|
@@ -82,12 +83,11 @@ const spellframe = new SpellFrame();
|
|
82
83
|
async (argv) => { // No JSDoc for internal handler
|
83
84
|
try {
|
84
85
|
await sfInstance.init();
|
85
|
-
console.log(`[+] Rendering configuration from: ${argv.filename}`);
|
86
86
|
await sfInstance.render(argv.filename);
|
87
87
|
await sfInstance.write();
|
88
88
|
console.log("[+] Generation complete.");
|
89
89
|
} catch (error) {
|
90
|
-
console.error(`[!] Error during generation: ${error.message}`);
|
90
|
+
console.error(`[!] Error during generation: ${error.message.red}`);
|
91
91
|
process.exit(1);
|
92
92
|
}
|
93
93
|
})
|
@@ -105,14 +105,9 @@ const spellframe = new SpellFrame();
|
|
105
105
|
default: undefined,
|
106
106
|
});
|
107
107
|
},
|
108
|
-
async (argv) => {
|
109
|
-
|
110
|
-
|
111
|
-
console.log(`[+] Module '${argv.npmPackage}' ${argv.name ? `(aliased as ${argv.name}) ` : ''}linked successfully.`);
|
112
|
-
} catch (error) {
|
113
|
-
console.error(`[!] Error importing module: ${error.message}`);
|
114
|
-
process.exit(1);
|
115
|
-
}
|
108
|
+
async (argv) => {
|
109
|
+
await sfInstance.importSpellCraftModuleFromNpm(argv.npmPackage, argv.name);
|
110
|
+
console.log(`[+] Module '${argv.npmPackage.green}' ${argv.name ? `(aliased as ${argv.name.green})` : ''} linked successfully.`);
|
116
111
|
});
|
117
112
|
|
118
113
|
// No JSDoc for CLI extensions loop if considered internal detail
|
package/package.json
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"dependencies": {
|
3
|
+
"@colors/colors": "^1.6.0",
|
3
4
|
"@hanazuki/node-jsonnet": "^0.4.2",
|
4
5
|
"ini": "^5.0.0",
|
5
6
|
"js-yaml": "^4.1.0",
|
@@ -7,7 +8,7 @@
|
|
7
8
|
},
|
8
9
|
"name": "@c6fc/spellcraft",
|
9
10
|
"description": "Extensible JSonnet CLI platform",
|
10
|
-
"version": "0.0.
|
11
|
+
"version": "0.0.5",
|
11
12
|
"main": "src/index.js",
|
12
13
|
"directories": {
|
13
14
|
"lib": "lib"
|
package/src/index.js
CHANGED
@@ -5,6 +5,7 @@ const fs = require("fs");
|
|
5
5
|
const path = require("path");
|
6
6
|
const yaml = require('js-yaml');
|
7
7
|
const crypto = require('crypto');
|
8
|
+
const colors = require('@colors/colors');
|
8
9
|
const { spawnSync } = require('child_process');
|
9
10
|
const { Jsonnet } = require("@hanazuki/node-jsonnet");
|
10
11
|
|
@@ -181,6 +182,7 @@ exports.SpellFrame = class SpellFrame {
|
|
181
182
|
this.functionContext = {};
|
182
183
|
this.lastRender = null;
|
183
184
|
this.activePath = null;
|
185
|
+
this.loadedModules = [];
|
184
186
|
|
185
187
|
this.jsonnet = new Jsonnet()
|
186
188
|
.addJpath(path.join(__dirname, '../lib')) // For core SpellCraft libsonnet files
|
@@ -190,7 +192,7 @@ exports.SpellFrame = class SpellFrame {
|
|
190
192
|
this.addNativeFunction("envvar", (name) => process.env[name] || false, "name");
|
191
193
|
this.addNativeFunction("path", () => this.activePath || process.cwd()); // Use activePath if available
|
192
194
|
|
193
|
-
this.loadModulesFromPackageList();
|
195
|
+
this.loadedModules = this.loadModulesFromPackageList();
|
194
196
|
}
|
195
197
|
|
196
198
|
/**
|
@@ -318,23 +320,27 @@ exports.SpellFrame = class SpellFrame {
|
|
318
320
|
const npmPath = path.resolve(baseDir, 'node_modules', npmPackage);
|
319
321
|
if (!fs.existsSync(npmPath)) {
|
320
322
|
console.log(`[+] Attempting to install ${npmPackage}...`);
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
stdio: 'inherit' // Show npm output directly
|
323
|
+
|
324
|
+
const install = spawnSync(`npm`, ['install', '--save', npmPackage], { // Using --save-dev for local project context
|
325
|
+
cwd: baseDir,
|
326
|
+
stdio: 'inherit'
|
326
327
|
});
|
328
|
+
|
327
329
|
if (install.error || install.status !== 0) {
|
328
|
-
throw new Error(`Failed to install npm package ${npmPackage}. Error: ${install.error || install.stderr.toString()}`);
|
330
|
+
throw new Error(`Failed to install npm package ${npmPackage.blue}. Error: ${install.error.red || install.stderr.toString().red}`);
|
329
331
|
}
|
330
|
-
|
332
|
+
|
333
|
+
console.log(`[+] Successfully installed ${npmPackage.blue}.`);
|
331
334
|
}
|
332
335
|
|
336
|
+
const moduleJson = JSON.parse(fs.readFileSync(path.join(npmPath, 'package.json')));
|
337
|
+
const moduleConfig = moduleJson.config || moduleJson.spellcraft; // Allow 'spellcraft' key too
|
338
|
+
|
333
339
|
const spellcraftConfig = thisPackage.config || thisPackage.spellcraft; // Allow 'spellcraft' key too
|
334
340
|
|
335
|
-
if (!name &&
|
336
|
-
console.log("Package config:",
|
337
|
-
throw new Error(`[!] No import name specified for ${npmPackage}, and it has no 'spellcraft_module_default_name' in its package.json config
|
341
|
+
if (!name && !!!moduleConfig?.spellcraft_module_default_name) {
|
342
|
+
console.log("Package config:", moduleJson);
|
343
|
+
throw new Error(`[!] No import name specified for ${npmPackage}, and it has no 'spellcraft_module_default_name' in its package.json config.`.red);
|
338
344
|
}
|
339
345
|
|
340
346
|
// Only link if this package is not a module itself.
|
@@ -351,7 +357,7 @@ exports.SpellFrame = class SpellFrame {
|
|
351
357
|
try {
|
352
358
|
packages = JSON.parse(fs.readFileSync(packagesFilePath, 'utf-8'));
|
353
359
|
} catch (e) {
|
354
|
-
console.warn(`[!] Could not parse existing ${packagesFilePath}. Starting fresh. Error: ${e.message}
|
360
|
+
console.warn(`[!] Could not parse existing ${packagesFilePath}. Starting fresh. Error: ${e.message}`.red);
|
355
361
|
packages = {};
|
356
362
|
}
|
357
363
|
}
|
@@ -361,7 +367,7 @@ exports.SpellFrame = class SpellFrame {
|
|
361
367
|
`@${npmPackage.split('/')[1].split('@')[0]}` : // Handles @scope/name and @scope/name@version
|
362
368
|
npmPackage.split('@')[0]; // Handles name and name@version
|
363
369
|
|
364
|
-
const packagesKey = name ||
|
370
|
+
const packagesKey = name || moduleConfig.spellcraft_module_default_name;
|
365
371
|
packages[npmPackage] = packagesKey; // Store the clean package name
|
366
372
|
|
367
373
|
fs.writeFileSync(packagesFilePath, JSON.stringify(packages, null, "\t"));
|
@@ -396,20 +402,34 @@ exports.SpellFrame = class SpellFrame {
|
|
396
402
|
fs.unlinkSync(dynamicModulesImportFile);
|
397
403
|
}
|
398
404
|
|
405
|
+
const { magicContent, registeredFunctions } = this.loadModulesFromModuleDirectory(dynamicModulesImportFile);
|
406
|
+
|
407
|
+
const aggregateFileDir = path.dirname(dynamicModulesImportFile);
|
408
|
+
if (!fs.existsSync(aggregateFileDir)) {
|
409
|
+
fs.mkdirSync(aggregateFileDir, { recursive: true });
|
410
|
+
}
|
411
|
+
|
412
|
+
magicContent.push(this.loadedModules.flatMap(e => {
|
413
|
+
return `\t${e}: import '${e}.libsonnet',`
|
414
|
+
}));
|
399
415
|
|
400
|
-
|
416
|
+
fs.writeFileSync(dynamicModulesImportFile, `{\n${magicContent.join(",\n")}\n}`, 'utf-8');
|
417
|
+
|
418
|
+
if (registeredFunctions.length > 0) {
|
419
|
+
console.log(`[+] Registered ${registeredFunctions.length} native function(s): [ ${registeredFunctions.sort().join(', ').cyan} ]`);
|
420
|
+
}
|
421
|
+
|
422
|
+
if (magicContent.length > 0) {
|
423
|
+
console.log(`[+] Aggregated modules written to '${path.basename(dynamicModulesImportFile).green}'`);
|
424
|
+
}
|
401
425
|
|
402
426
|
// Ensure renderPath does not have a trailing slash for consistency
|
403
427
|
if (this.renderPath.endsWith(path.sep)) {
|
404
428
|
this.renderPath = this.renderPath.slice(0, -1);
|
405
429
|
}
|
406
430
|
|
407
|
-
|
408
|
-
|
409
|
-
this.lastRender = JSON.parse(await this.jsonnet.evaluateFile(absoluteFilePath));
|
410
|
-
} catch (e) {
|
411
|
-
throw new Error(`Jsonnet Evaluation Error for ${absoluteFilePath}: ${e.message || e}`);
|
412
|
-
}
|
431
|
+
console.log(`[+] Evaluating Jsonnet file ${path.basename(absoluteFilePath).green}`);
|
432
|
+
this.lastRender = JSON.parse(await this.jsonnet.evaluateFile(absoluteFilePath));
|
413
433
|
|
414
434
|
// Clean up the modules folder after rendering,
|
415
435
|
// as it's specific to this render pass and its discovered modules.
|
@@ -418,7 +438,7 @@ exports.SpellFrame = class SpellFrame {
|
|
418
438
|
|
419
439
|
fs.readdirSync(modulePath)
|
420
440
|
.map(e => path.join(modulePath, e))
|
421
|
-
|
441
|
+
.forEach(e => fs.unlinkSync(e));
|
422
442
|
|
423
443
|
} catch (e) {
|
424
444
|
console.warn(`[!] Could not clean up temporary module file ${dynamicModulesImportFile}: ${e.message}`);
|
@@ -429,30 +449,26 @@ exports.SpellFrame = class SpellFrame {
|
|
429
449
|
|
430
450
|
/**
|
431
451
|
* Loads SpellCraft modules by scanning the `spellcraft_modules` directory for `.js` files.
|
432
|
-
*
|
433
|
-
* @param {string} aggregateModuleFile - The path where the aggregating libsonnet file will be written.
|
434
|
-
* @returns {Array<string>} A list of registered function names from the loaded modules.
|
452
|
+
* @returns {Object} A list of registered function names from the loaded modules.
|
435
453
|
*/
|
436
|
-
loadModulesFromModuleDirectory(
|
454
|
+
loadModulesFromModuleDirectory() {
|
437
455
|
const spellcraftModulesPath = path.join(baseDir, 'spellcraft_modules');
|
438
456
|
if (!fs.existsSync(spellcraftModulesPath)) {
|
439
|
-
|
440
|
-
fs.writeFileSync(aggregateModuleFile, '{}', 'utf-8');
|
441
|
-
return [];
|
457
|
+
return { registeredFunctions: [], magicContent: [] };
|
442
458
|
}
|
443
459
|
|
444
460
|
const spellcraftConfig = thisPackage.config || thisPackage.spellcraft; // Allow 'spellcraft' key too
|
445
461
|
|
446
462
|
if (!!spellcraftConfig?.spellcraft_module_default_name) {
|
447
463
|
console.log("[-] This package is a SpellCraft module. Skipping directory-based module import.");
|
448
|
-
return []
|
464
|
+
return { registeredFunctions: [], magicContent: [] };
|
449
465
|
}
|
450
466
|
|
451
467
|
const jsModuleFiles = fs.readdirSync(spellcraftModulesPath)
|
452
468
|
.filter(f => f.endsWith('.js')) // Simpler check for .js files
|
453
469
|
.map(f => path.join(spellcraftModulesPath, f));
|
454
470
|
|
455
|
-
return this.loadModulesFromFileList(jsModuleFiles
|
471
|
+
return this.loadModulesFromFileList(jsModuleFiles);
|
456
472
|
}
|
457
473
|
|
458
474
|
|
@@ -479,7 +495,7 @@ exports.SpellFrame = class SpellFrame {
|
|
479
495
|
|
480
496
|
return Object.entries(packages).map(([npmPackageName, moduleKey]) => {
|
481
497
|
return this.loadModuleByName(moduleKey, npmPackageName);
|
482
|
-
})
|
498
|
+
}); // Filter out any undefined results if a module fails to load
|
483
499
|
}
|
484
500
|
|
485
501
|
/**
|
@@ -487,7 +503,7 @@ exports.SpellFrame = class SpellFrame {
|
|
487
503
|
* @private
|
488
504
|
* @param {string} moduleKey - The key (alias) under which the module is registered.
|
489
505
|
* @param {string} npmPackageName - The actual npm package name.
|
490
|
-
* @returns {string|false} The moduleKey if successful,
|
506
|
+
* @returns {string|false} The moduleKey if successful, false otherwise.
|
491
507
|
*/
|
492
508
|
loadModuleByName(moduleKey, npmPackageName) {
|
493
509
|
const packageJsonPath = path.join(baseDir, 'node_modules', npmPackageName, 'package.json');
|
@@ -505,14 +521,16 @@ exports.SpellFrame = class SpellFrame {
|
|
505
521
|
}
|
506
522
|
|
507
523
|
if (!packageConfig.main) {
|
508
|
-
console.warn(`[!] 'main' field missing in package.json for module '${moduleKey}' (package: ${npmPackageName}). Skipping JS function loading
|
524
|
+
console.warn(`[!] 'main' field missing in package.json for module '${moduleKey}' (package: ${npmPackageName}). Skipping JS function loading.`.red);
|
509
525
|
} else {
|
510
526
|
const jsMainFilePath = path.resolve(baseDir, 'node_modules', npmPackageName, packageConfig.main);
|
511
527
|
if (fs.existsSync(jsMainFilePath)) {
|
512
528
|
const { functions } = this.loadFunctionsFromFile(jsMainFilePath);
|
513
|
-
|
529
|
+
if (functions.length > 0) {
|
530
|
+
console.log(`[+] Imported JavaScript native [${functions.join(', ').cyan}] from module ${moduleKey.green}`);
|
531
|
+
}
|
514
532
|
} else {
|
515
|
-
console.warn(`[!] Main JS file '${jsMainFilePath}' not found for module '${moduleKey}'. Skipping JS function loading.`);
|
533
|
+
console.warn(`[!] Main JS file '${jsMainFilePath.red}' not found for module '${moduleKey.red}'. Skipping JS function loading.`);
|
516
534
|
}
|
517
535
|
}
|
518
536
|
|
@@ -541,13 +559,15 @@ exports.SpellFrame = class SpellFrame {
|
|
541
559
|
// Ensure the target directory exists
|
542
560
|
fs.mkdirSync(path.dirname(targetLibsonnetPath), { recursive: true });
|
543
561
|
fs.copyFileSync(sourceLibsonnetPath, targetLibsonnetPath);
|
544
|
-
|
562
|
+
|
563
|
+
console.log(`[+] Linked libsonnet module for ${npmPackageName == '..' ? 'this package'.blue : npmPackageName.green} as modules.${moduleKey.green}`);
|
545
564
|
} catch (e) {
|
546
565
|
console.warn(`[!] Failed to copy libsonnet module for '${moduleKey}': ${e.message}`);
|
547
566
|
}
|
548
567
|
} else {
|
549
568
|
// console.log(`[+] No .libsonnet module found for '${moduleKey}' at expected paths.`);
|
550
569
|
}
|
570
|
+
|
551
571
|
return moduleKey;
|
552
572
|
}
|
553
573
|
|
@@ -560,15 +580,10 @@ exports.SpellFrame = class SpellFrame {
|
|
560
580
|
* @returns {{registeredFunctions: string[], magicContent: string[]}}
|
561
581
|
* An object containing lists of registered function names and the Jsonnet "magic" import strings.
|
562
582
|
*/
|
563
|
-
loadModulesFromFileList(jsModuleFiles
|
583
|
+
loadModulesFromFileList(jsModuleFiles) {
|
564
584
|
let allRegisteredFunctions = [];
|
565
585
|
let allMagicContent = [];
|
566
586
|
|
567
|
-
if (jsModuleFiles.length < 1) {
|
568
|
-
fs.writeFileSync(aggregateModuleFile, '{}', 'utf-8'); // Write empty object if no modules
|
569
|
-
return { registeredFunctions: [], magicContent: [] };
|
570
|
-
}
|
571
|
-
|
572
587
|
jsModuleFiles.forEach(file => {
|
573
588
|
try {
|
574
589
|
const { functions, magic } = this.loadFunctionsFromFile(file);
|
@@ -579,24 +594,6 @@ exports.SpellFrame = class SpellFrame {
|
|
579
594
|
}
|
580
595
|
});
|
581
596
|
|
582
|
-
const aggregateFileDir = path.dirname(aggregateModuleFile);
|
583
|
-
if (!fs.existsSync(aggregateFileDir)) {
|
584
|
-
fs.mkdirSync(aggregateFileDir, { recursive: true });
|
585
|
-
}
|
586
|
-
|
587
|
-
fs.writeFileSync(aggregateModuleFile, `{\n${magicContent.join(",\n")}\n}`, 'utf-8');
|
588
|
-
|
589
|
-
if (jsModuleFiles.length > 0) {
|
590
|
-
console.log(`[+] Processed ${jsModuleFiles.length} JS module file(s) for native functions.`);
|
591
|
-
}
|
592
|
-
if (allRegisteredFunctions.length > 0) {
|
593
|
-
console.log(`[+] Registered ${allRegisteredFunctions.length} native function(s): [ ${allRegisteredFunctions.sort().join(', ')} ]`);
|
594
|
-
}
|
595
|
-
if (moduleImportsString || allMagicContent.length > 0) {
|
596
|
-
console.log(`[+] Aggregated modules written to '${path.basename(aggregateModuleFile)}'`);
|
597
|
-
}
|
598
|
-
|
599
|
-
|
600
597
|
return { registeredFunctions: allRegisteredFunctions, magicContent: allMagicContent };
|
601
598
|
}
|
602
599
|
|
@@ -725,7 +722,7 @@ exports.SpellFrame = class SpellFrame {
|
|
725
722
|
try {
|
726
723
|
const processedContent = handlerFn(fileContent);
|
727
724
|
fs.writeFileSync(outputFilePath, processedContent, 'utf-8');
|
728
|
-
console.log(
|
725
|
+
console.log(` -> ${path.basename(outputFilePath).green}`);
|
729
726
|
} catch (handlerError) {
|
730
727
|
console.error(` [!] Error processing or writing file ${filename}: ${handlerError.message}`);
|
731
728
|
// Optionally re-throw or collect errors
|