@c6fc/spellcraft 0.0.4 → 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 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) => { // No JSDoc for internal handler
109
- try {
110
- await sfInstance.importSpellCraftModuleFromNpm(argv.npmPackage, argv.name);
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.4",
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
- // Note: `spawnSync` is blocking. For a CLI tool, this might be acceptable.
322
- // Consider an async alternative if this needs to be non-blocking.
323
+
323
324
  const install = spawnSync(`npm`, ['install', '--save', npmPackage], { // Using --save-dev for local project context
324
- cwd: baseDir, // Install in the project's node_modules, not renderPath
325
- stdio: 'inherit' // Show npm output directly
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
- console.log(`[+] Successfully installed ${npmPackage}.`);
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 && !spellcraftConfig?.spellcraft_module_default_name) {
336
- console.log("Package config:", thisPackage);
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 || spellcraftConfig.spellcraft_module_default_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
- this.loadModulesFromModuleDirectory(dynamicModulesImportFile);
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
- try {
408
- console.log(`[+] Evaluating Jsonnet file: ${absoluteFilePath}`);
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
- // .forEach(e => fs.unlinkSync(e));
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
- * Generates a `modules` file in `../modules/` to make them importable in Jsonnet.
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(aggregateModuleFile) {
454
+ loadModulesFromModuleDirectory() {
437
455
  const spellcraftModulesPath = path.join(baseDir, 'spellcraft_modules');
438
456
  if (!fs.existsSync(spellcraftModulesPath)) {
439
- // Ensure the aggregate file is empty if no modules found
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, aggregateModuleFile);
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
- }).filter(Boolean); // Filter out any undefined results if a module fails to load
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, null otherwise.
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
- console.log(`[+] Imported JavaScript native [${functions.join(', ')}] into module '${moduleKey}'`);
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
- console.log(`[+] Copied libsonnet module for '${moduleKey}'`);
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, aggregateModuleFile) {
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(' -> ' + path.basename(outputFilePath));
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