@c6fc/spellcraft 0.0.1 → 0.0.2

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
@@ -7,7 +7,7 @@ Spellcraft is an opinionated approach to manifesting and applying configurations
7
7
  Install spellcraft with `npm install` and add plugins with `npx spellcraft importModule`
8
8
 
9
9
  ```sh
10
- $ npm i -p spellcraft
10
+ $ npm i -p @c6fc/spellcraft
11
11
  $ npx spellcraft importModule @c6fc/spellcraft-packer packer
12
12
  ```
13
13
 
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "name": "@c6fc/spellcraft",
9
9
  "description": "Extensible JSonnet CLI platform",
10
- "version": "0.0.1",
10
+ "version": "0.0.2",
11
11
  "main": "src/index.js",
12
12
  "directories": {
13
13
  "lib": "lib"
package/src/index.js CHANGED
@@ -314,7 +314,6 @@ exports.SpellFrame = class SpellFrame {
314
314
  */
315
315
  async importSpellCraftModuleFromNpm(npmPackage, name = false) {
316
316
  const npmPath = path.resolve(baseDir, 'node_modules', npmPackage);
317
-
318
317
  if (!fs.existsSync(npmPath)) {
319
318
  console.log(`[+] Attempting to install ${npmPackage}...`);
320
319
  // Note: `spawnSync` is blocking. For a CLI tool, this might be acceptable.
@@ -342,40 +341,54 @@ exports.SpellFrame = class SpellFrame {
342
341
  throw new Error(`[!] No import name specified for ${npmPackage}, and it has no 'spellcraft_module_default_name' in its package.json config.`);
343
342
  }
344
343
 
345
- const packagesDirPath = path.join(baseDir, 'spellcraft_modules');
346
- if (!fs.existsSync(packagesDirPath)) {
347
- fs.mkdirSync(packagesDirPath, { recursive: true });
344
+ // Check if this project is also a module, which shouldn't contain a spellcraft_modules directory.
345
+ const thisPackageJsonPath = path.join(baseDir, 'package.json');
346
+ if (!fs.existsSync(npmPath)) {
347
+ throw new Error(`[!] There's no package.json for your project. Create one with 'npm init' first`);
348
348
  }
349
349
 
350
- const packagesFilePath = path.join(packagesDirPath, 'packages.json');
351
- let packages = {};
352
- if (fs.existsSync(packagesFilePath)) {
353
- try {
354
- packages = JSON.parse(fs.readFileSync(packagesFilePath, 'utf-8'));
355
- } catch (e) {
356
- console.warn(`[!] Could not parse existing ${packagesFilePath}. Starting fresh. Error: ${e.message}`);
357
- packages = {};
350
+ const thisPackageConfig = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
351
+ const thisSpellcraftConfig = packageConfig.config || packageConfig.spellcraft; // Allow 'spellcraft' key too
352
+
353
+ // Only link if this package is not a module itself.
354
+ if (!!!thisSpellcraftConfig?.spellcraft_module_default_name) {
355
+
356
+ const packagesDirPath = path.join(baseDir, 'spellcraft_modules');
357
+ if (!fs.existsSync(packagesDirPath)) {
358
+ fs.mkdirSync(packagesDirPath, { recursive: true });
358
359
  }
359
- }
360
360
 
361
- // Derive the base name to store (e.g., "my-package" from "my-package@1.0.0")
362
- const npmPackageBaseName = npmPackage.startsWith("@") ?
363
- `@${npmPackage.split('/')[1].split('@')[0]}` : // Handles @scope/name and @scope/name@version
364
- npmPackage.split('@')[0]; // Handles name and name@version
361
+ const packagesFilePath = path.join(packagesDirPath, 'packages.json');
362
+ let packages = {};
363
+ if (fs.existsSync(packagesFilePath)) {
364
+ try {
365
+ packages = JSON.parse(fs.readFileSync(packagesFilePath, 'utf-8'));
366
+ } catch (e) {
367
+ console.warn(`[!] Could not parse existing ${packagesFilePath}. Starting fresh. Error: ${e.message}`);
368
+ packages = {};
369
+ }
370
+ }
365
371
 
366
- const packagesKey = name || spellcraftConfig.spellcraft_module_default_name;
367
- packages[packagesKey] = npmPackageBaseName; // Store the clean package name
372
+ // Derive the base name to store (e.g., "my-package" from "my-package@1.0.0")
373
+ const npmPackageBaseName = npmPackage.startsWith("@") ?
374
+ `@${npmPackage.split('/')[1].split('@')[0]}` : // Handles @scope/name and @scope/name@version
375
+ npmPackage.split('@')[0]; // Handles name and name@version
368
376
 
369
- fs.writeFileSync(packagesFilePath, JSON.stringify(packages, null, "\t"));
370
- console.log(`[+] Linked ${npmPackage} as SpellCraft module '${packagesKey}'`);
377
+ const packagesKey = name || spellcraftConfig.spellcraft_module_default_name;
378
+ packages[packagesKey] = npmPackageBaseName; // Store the clean package name
371
379
 
372
- // After linking, ensure it's loaded for the current session
373
- this.loadModuleByName(packagesKey, npmPackageBaseName);
380
+ fs.writeFileSync(packagesFilePath, JSON.stringify(packages, null, "\t"));
381
+ console.log(`[+] Linked ${npmPackage} as SpellCraft module '${packagesKey}'`);
382
+
383
+ } else {
384
+ console.log(`[+] Module installed, but not linked because the current project is also a module.`);
385
+ console.log(` You can use the module's JS native functions, or import its JSonnet modules.`);
386
+ }
374
387
  }
375
388
 
376
389
  /**
377
390
  * Evaluates a Jsonnet file and stores the result.
378
- * This also sets up the dynamic `modules/modules.libsonnet` import aggregator.
391
+ * This also sets up the dynamic `modules/modules` import aggregator.
379
392
  * @param {string} file - The path to the main Jsonnet file to evaluate.
380
393
  * @async
381
394
  * @returns {Promise<object>} The parsed JSON object from the Jsonnet evaluation.
@@ -390,7 +403,7 @@ exports.SpellFrame = class SpellFrame {
390
403
  this.activePath = path.dirname(absoluteFilePath); // Set active path for relative 'path()' calls
391
404
 
392
405
  // Path to the dynamically generated libsonnet file that imports all modules
393
- const dynamicModulesImportFile = path.resolve(__dirname, '../modules/modules.libsonnet');
406
+ const dynamicModulesImportFile = path.resolve(__dirname, '../modules/modules');
394
407
 
395
408
  // It's crucial this file is managed carefully.
396
409
  // If it exists from a previous failed run or other reasons, it might cause issues.
@@ -416,16 +429,17 @@ exports.SpellFrame = class SpellFrame {
416
429
  throw new Error(`Jsonnet Evaluation Error for ${absoluteFilePath}: ${e.message || e}`);
417
430
  }
418
431
 
419
- // Clean up the dynamically generated modules.libsonnet after rendering,
432
+ // Clean up the modules folder after rendering,
420
433
  // as it's specific to this render pass and its discovered modules.
421
- // This was previously cleaning the whole modules directory, which might be too broad
422
- // if other static .libsonnet files are meant to be there.
423
- if (fs.existsSync(dynamicModulesImportFile)) {
424
- try {
425
- fs.unlinkSync(dynamicModulesImportFile);
426
- } catch (e) {
427
- console.warn(`[!] Could not clean up temporary module file ${dynamicModulesImportFile}: ${e.message}`);
428
- }
434
+ try {
435
+ const modulePath = path.resolve(__dirname, '../modules');
436
+
437
+ fs.readdirSync(modulePath)
438
+ .map(e => path.join(modulePath, e))
439
+ // .forEach(e => fs.unlinkSync(e));
440
+
441
+ } catch (e) {
442
+ console.warn(`[!] Could not clean up temporary module file ${dynamicModulesImportFile}: ${e.message}`);
429
443
  }
430
444
 
431
445
  return this.lastRender;
@@ -433,7 +447,7 @@ exports.SpellFrame = class SpellFrame {
433
447
 
434
448
  /**
435
449
  * Loads SpellCraft modules by scanning the `spellcraft_modules` directory for `.js` files.
436
- * Generates a `modules.libsonnet` file in `../modules/` to make them importable in Jsonnet.
450
+ * Generates a `modules` file in `../modules/` to make them importable in Jsonnet.
437
451
  * @param {string} aggregateModuleFile - The path where the aggregating libsonnet file will be written.
438
452
  * @returns {Array<string>} A list of registered function names from the loaded modules.
439
453
  */
@@ -445,6 +459,13 @@ exports.SpellFrame = class SpellFrame {
445
459
  return [];
446
460
  }
447
461
 
462
+ const spellcraftConfig = packageConfig.config || packageConfig.spellcraft; // Allow 'spellcraft' key too
463
+
464
+ if (!!spellcraftConfig?.spellcraft_module_default_name) {
465
+ console.log("This package is a SpellCraft module. Skipping directory-based module import.");
466
+ return []
467
+ }
468
+
448
469
  const jsModuleFiles = fs.readdirSync(spellcraftModulesPath)
449
470
  .filter(f => f.endsWith('.js')) // Simpler check for .js files
450
471
  .map(f => path.join(spellcraftModulesPath, f));
@@ -484,13 +505,13 @@ exports.SpellFrame = class SpellFrame {
484
505
  * @private
485
506
  * @param {string} moduleKey - The key (alias) under which the module is registered.
486
507
  * @param {string} npmPackageName - The actual npm package name.
487
- * @returns {string|null} The moduleKey if successful, null otherwise.
508
+ * @returns {string|false} The moduleKey if successful, null otherwise.
488
509
  */
489
510
  loadModuleByName(moduleKey, npmPackageName) {
490
511
  const packageJsonPath = path.join(baseDir, 'node_modules', npmPackageName, 'package.json');
491
512
  if (!fs.existsSync(packageJsonPath)) {
492
513
  console.warn(`[!] package.json not found for module '${moduleKey}' (package: ${npmPackageName}) at ${packageJsonPath}. Skipping.`);
493
- return null;
514
+ return false;
494
515
  }
495
516
 
496
517
  let packageConfig;
@@ -498,7 +519,7 @@ exports.SpellFrame = class SpellFrame {
498
519
  packageConfig = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
499
520
  } catch (e) {
500
521
  console.warn(`[!] Error parsing package.json for module '${moduleKey}' (package: ${npmPackageName}): ${e.message}. Skipping.`);
501
- return null;
522
+ return false;
502
523
  }
503
524
 
504
525
  if (!packageConfig.main) {
@@ -506,7 +527,8 @@ exports.SpellFrame = class SpellFrame {
506
527
  } else {
507
528
  const jsMainFilePath = path.resolve(baseDir, 'node_modules', npmPackageName, packageConfig.main);
508
529
  if (fs.existsSync(jsMainFilePath)) {
509
- this.loadFunctionsFromFile(jsMainFilePath);
530
+ const { functions } = this.loadFunctionsFromFile(jsMainFilePath);
531
+ console.log(`[+] Imported JavaScript native [${functions.join(', ')}] into module '${moduleKey}'`);
510
532
  } else {
511
533
  console.warn(`[!] Main JS file '${jsMainFilePath}' not found for module '${moduleKey}'. Skipping JS function loading.`);
512
534
  }
@@ -537,7 +559,7 @@ exports.SpellFrame = class SpellFrame {
537
559
  // Ensure the target directory exists
538
560
  fs.mkdirSync(path.dirname(targetLibsonnetPath), { recursive: true });
539
561
  fs.copyFileSync(sourceLibsonnetPath, targetLibsonnetPath);
540
- console.log(`[+] Copied libsonnet module for '${moduleKey}' from ${sourceLibsonnetPath} to ${targetLibsonnetPath}`);
562
+ console.log(`[+] Copied libsonnet module for '${moduleKey}'`);
541
563
  } catch (e) {
542
564
  console.warn(`[!] Failed to copy libsonnet module for '${moduleKey}': ${e.message}`);
543
565
  }
@@ -550,7 +572,7 @@ exports.SpellFrame = class SpellFrame {
550
572
 
551
573
  /**
552
574
  * Loads native functions from a list of JavaScript module files and generates
553
- * an aggregate Jsonnet import file (`modules.libsonnet` by default convention).
575
+ * an aggregate Jsonnet import file (`modules` by default convention).
554
576
  * @param {string[]} jsModuleFiles - An array of absolute paths to JS module files.
555
577
  * @param {string} aggregateModuleFile - The path to the Jsonnet file that will aggregate imports.
556
578
  * @returns {{registeredFunctions: string[], magicContent: string[]}}
@@ -579,66 +601,8 @@ exports.SpellFrame = class SpellFrame {
579
601
  if (!fs.existsSync(aggregateFileDir)) {
580
602
  fs.mkdirSync(aggregateFileDir, { recursive: true });
581
603
  }
582
- // This content creates a Jsonnet object where keys are module names
583
- // and values are their corresponding .libsonnet imports.
584
- // It's assumed that loadModuleByName has already copied relevant .libsonnet files
585
- // into the ../modules directory, named like <moduleKey>.libsonnet
586
- const packagesConfigPath = path.join(baseDir, 'spellcraft_modules', 'packages.json');
587
- let moduleImportsString = "";
588
- if (fs.existsSync(packagesConfigPath)) {
589
- try {
590
- const packages = JSON.parse(fs.readFileSync(packagesConfigPath, 'utf-8'));
591
- moduleImportsString = Object.keys(packages)
592
- .map(key => ` ${key}: import "${key}.libsonnet"`) // Assumes <key>.libsonnet exists in jpath
593
- .join(",\n");
594
- } catch (e) {
595
- console.warn(`[!] Error processing packages.json for libsonnet aggregation: ${e.message}`);
596
- }
597
- }
598
604
 
599
- // The aggregateModuleFile ('modules/modules.libsonnet') will now look like:
600
- // {
601
- // moduleKey1: import "moduleKey1.libsonnet",
602
- // moduleKey2: import "moduleKey2.libsonnet",
603
- // // ... any other native functions directly defined
604
- // }
605
- // Native functions defined via loadFunctionsFromFile are added directly to jsonnet instance,
606
- // and their `std.native` calls are generated by loadFunctionsFromFile's `magicContent`.
607
- // The `modules.libsonnet` is primarily for importing `.libsonnet` files from packages.
608
- // The `magicContent` from JS files might need a different aggregation strategy
609
- // if they are meant to be part of this central `modules.libsonnet`.
610
- // For now, `allMagicContent` seems to be intended for a different purpose or needs clarification.
611
- // If `magicContent` is for defining native functions in a libsonnet structure, it needs to be integrated.
612
- // The current `magicContent.join(",\n")` assumes it's creating entries for a Jsonnet object.
613
- // Let's assume modules.libsonnet is for Jsonnet library imports, and native functions are separate.
614
- //
615
- // Revision: If `allMagicContent` is for `std.native` declarations,
616
- // those are typically not put inside `modules.libsonnet` but rather are available globally
617
- // once `addNativeFunction` is called. The `modules.libsonnet` file is for Jsonnet code.
618
- // The `magicContent` was generating strings like `\t${e}(${parameters.join(', ')}):: std.native('${e}')(${parameters.join(', ')})`
619
- // This is Jsonnet code. If this is meant to be *the* content of `modules.libsonnet`,
620
- // then the `moduleImportsString` part is separate.
621
- //
622
- // Let's clarify:
623
- // 1. `../modules/<moduleKey>.libsonnet`: Copied from each package.
624
- // 2. `../modules/modules.libsonnet`: This file should make #1 easily accessible.
625
- // Example: `{ myModule: import "myModule.libsonnet" }`
626
- // 3. Native JS functions: Registered directly with `this.jsonnet.nativeCallback`.
627
- // They are globally available in Jsonnet via `std.native('funcName')`.
628
- // The `magicContent` seems to be an attempt to create a .libsonnet wrapper for native functions.
629
- // This is good practice! So `modules.libsonnet` could combine these.
630
-
631
- let finalModulesContent = "{\n";
632
- if (moduleImportsString) {
633
- finalModulesContent += moduleImportsString;
634
- }
635
- if (allMagicContent.length > 0) {
636
- if (moduleImportsString) finalModulesContent += ",\n"; // Add comma if previous content exists
637
- finalModulesContent += allMagicContent.join(",\n");
638
- }
639
- finalModulesContent += "\n}";
640
-
641
- fs.writeFileSync(aggregateModuleFile, finalModulesContent, 'utf-8');
605
+ fs.writeFileSync(aggregateModuleFile, `{\n${magicContent.join(",\n")}\n}`, 'utf-8');
642
606
 
643
607
  if (jsModuleFiles.length > 0) {
644
608
  console.log(`[+] Processed ${jsModuleFiles.length} JS module file(s) for native functions.`);
@@ -698,7 +662,7 @@ exports.SpellFrame = class SpellFrame {
698
662
  return null;
699
663
  }
700
664
 
701
- // For `modules.libsonnet` to provide convenient wrappers:
665
+ // For `modules` to provide convenient wrappers:
702
666
  // e.g. myNativeFunc(a,b):: std.native('myNativeFunc')(a,b)
703
667
  const paramString = params.join(', ');
704
668
  magicContentSnippets.push(` ${funcName}(${paramString}):: std.native('${funcName}')(${paramString})`);
@@ -1,47 +0,0 @@
1
- #! /usr/bin/env node
2
-
3
- const fs = require('fs');
4
- const yargs = require('yargs')
5
- const { SpellFrame } = require('../src/index.js');
6
-
7
- const spellframe = new SpellFrame();
8
-
9
- (async () => {
10
-
11
- yargs
12
- .usage("Syntax: $0 <command> [options]")
13
- .command("*", "That's too arcane", (yargs) => {
14
- yargs
15
- }, (argv) => {
16
- console.log("[~] That's too arcane. (Unrecognized command)");
17
- })
18
- .command("generate <filename>", "Generates files from a configuration", (yargs) => {
19
- return yargs.positional('filename', {
20
- describe: 'Jsonnet configuration file to consume'
21
- })
22
- }, async (argv) => {
23
-
24
- await spellframe.init();
25
- await spellframe.render(argv.filename);
26
- await spellframe.write();
27
-
28
- })
29
- .command("importModule <npmPackage> [name]", "Configures the current project to use a SpellCraft plugin as an import", (yargs) => {
30
- return yargs.positional('npmPackage', {
31
- describe: 'The NPM package name of a SpellCraft Plugin to import'
32
- }).positional('name', {
33
- describe: 'What name to use as the import namespace for this plugin'
34
- })
35
- }, async (argv) => {
36
-
37
- await spellframe.importSpellCraftModuleFromNpm(argv['npmPackage'], argv['name']);
38
-
39
- });
40
-
41
- spellframe.cliExtensions.forEach((e, i) => e(yargs, spellframe));
42
-
43
- yargs
44
- .showHelpOnFail(false)
45
- .help("help")
46
- .argv;
47
- })();
package/src/index.js.bak DELETED
@@ -1,403 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require("fs");
4
- const os = require("os");
5
- const ini = require("ini");
6
- const path = require("path");
7
- const yaml = require('js-yaml');
8
- const crypto = require('crypto');
9
- const readline = require("readline");
10
- const { spawnSync } = require('child_process');
11
- const { Jsonnet } = require("@hanazuki/node-jsonnet");
12
-
13
- const baseDir = process.cwd();
14
-
15
- exports.SpellFrame = class {
16
- renderPath; cliExtensions; cleanBeforeRender; fileTypeHandlers; initFn; jsonnet; lastRender; activePath; functionContext; useDefaultFileHandlers;
17
-
18
- constructor(options) {
19
-
20
- const defaults = {
21
- renderPath: "./render",
22
- cleanBeforeRender: true,
23
- useDefaultFileHandlers: true
24
- };
25
-
26
- Object.keys(defaults).forEach(e => {
27
- this[e] = options?.[e] ?? defaults[e];
28
- });
29
-
30
- // An array of functions aggregated from all plugins that must all succeed before files are processed
31
- this.initFn = [];
32
-
33
- // A cache of synchronous function execution results.
34
- this.cache = {};
35
-
36
- // An array of functions that extend the CLI 'yargs' argument.
37
- this.cliExtensions = [];
38
-
39
- this.fileTypeHandlers = (this.useDefaultFileHandlers) ? defaultFileTypeHandlers : {};
40
-
41
- // An object to pass as `this` to all functions invoked via JSonnet.
42
- this.functionContext = {};
43
-
44
- this.jsonnet = new Jsonnet()
45
- .addJpath(path.join(__dirname, '../lib'))
46
- .addJpath(path.join(__dirname, '../modules'));
47
-
48
- this.addFunction("envvar", (name) => {
49
- return process.env?.[name] ?? false;
50
- }, "name");
51
-
52
- this.addFunction("path", () => {
53
- return `${process.cwd()}`;
54
- });
55
-
56
- this.loadModulesFromPackageList();
57
-
58
- return this;
59
- }
60
-
61
- _cacheKey(...args) {
62
- return crypto.createHash('sha256').update(JSON.stringify(args)).digest('hex');
63
- }
64
-
65
- addFileTypeHander(pattern, handler) {
66
- Object.defineProperty(this.fileTypeHandlers, pattern, {
67
- value: handler,
68
- writable: false
69
- });
70
-
71
- return this.fileTypeHandlers;
72
- }
73
-
74
- addFunction(name, fn, ...parameters) {
75
- this.jsonnet.nativeCallback(name, (...args) => {
76
-
77
- let key = this._cacheKey(name, args);
78
- if (!!this.cache?.[key]) {
79
- return this.cache[key];
80
- }
81
-
82
- this.cache[key] = fn.call(this.functionContext, ...args);
83
-
84
- return this.cache[key];
85
- }, ...parameters);
86
- }
87
-
88
- export(name, value) {
89
- if (typeof value !== "string") {
90
- value = JSON.stringify(value);
91
- }
92
-
93
- this.jsonnet = this.jsonnet.extCode(name, value);
94
- return this;
95
- }
96
-
97
- extendWithModuleMetadata(metadata) {
98
-
99
- }
100
-
101
- import(path) {
102
- this.jsonnet = this.jsonnet.addJpath(path);
103
-
104
- return this;
105
- }
106
-
107
- async init() {
108
- for (const step of this.initFn) {
109
- await step();
110
- }
111
- }
112
-
113
- async importSpellCraftModuleFromNpm(npmPackage, name = false) {
114
- const npmPath = path.resolve(path.join(baseDir, 'node_modules', npmPackage));
115
- if (!fs.existsSync(npmPath)) {
116
- const install = spawnSync(`npm`, ['install', '-p', npmPackage], {
117
- cwd: this.renderPath,
118
- stdio: [process.stdin, process.stdout, process.stderr]
119
- });
120
- }
121
-
122
- const configFile = path.join(npmPath, 'package.json');
123
-
124
- if (!fs.existsSync(configFile)) {
125
- console.log(`[!] Package ${npmPackage} is missing package.json`);
126
- process.exit(1);
127
- }
128
-
129
- const { config } = JSON.parse(fs.readFileSync(path.join(configFile)));
130
-
131
- if (!name && !config?.spellcraft_module_default_name) {
132
- console.log(config);
133
- console.log(`[!] No import name specified, and ${npmPackage} has no default import name`);
134
- process.exit(1);
135
- }
136
-
137
- const packagesDirPath = path.join(baseDir, 'spellcraft_modules');
138
- if (!fs.existsSync(packagesDirPath)) {
139
- fs.mkdirSync(packagesDirPath);
140
- }
141
-
142
- let packages = {};
143
- const packagesFilePath = path.join(baseDir, 'spellcraft_modules', 'packages.json')
144
- if (fs.existsSync(packagesFilePath)) {
145
- packages = JSON.parse(fs.readFileSync(packagesFilePath));
146
- }
147
-
148
- let npmPackageBaseName;
149
-
150
- // If the package is namespaced,
151
- if (npmPackage[0] == "@") {
152
- [ , npmPackageBaseName ] = npmPackage.split('@');
153
- npmPackageBaseName = `@${npmPackageBaseName}`;
154
- } else {
155
- [ npmPackageBaseName ] = npmPackage.split('@');
156
- }
157
-
158
- const packagesKey = name || config.spellcraft_module_default_name;
159
- packages[packagesKey] = npmPackageBaseName;
160
-
161
- fs.writeFileSync(packagesFilePath, JSON.stringify(packages, null, "\t"));
162
-
163
- console.log(`[+] Linked ${npmPackage} as ${packagesKey}`);
164
-
165
- }
166
-
167
- async render(file) {
168
- if (!fs.existsSync(file)) {
169
- throw new Error(`Sonnetry Error: ${file} does not exist.`);
170
- }
171
-
172
- this.activePath = path.dirname(path.resolve(file));
173
-
174
- const moduleFile = path.resolve(path.join(__dirname, '../modules/modules'));
175
-
176
- if (fs.existsSync(moduleFile)) {
177
- throw new Error(`[!] The module target file [${moduleFile}] already exists. Remove or rename it before continuing.`);
178
- }
179
-
180
- this.loadModulesFromModuleDirectory(moduleFile);
181
-
182
- console.log(this.renderPath);
183
-
184
- this.renderPath = (this.renderPath.split("").slice(-1)[0] == "/") ?
185
- this.renderPath.split("").slice(0, -1).join("") :
186
- this.renderPath;
187
-
188
- try {
189
- this.lastRender = JSON.parse(await this.jsonnet.evaluateFile(file));
190
- } catch (e) {
191
- throw new Error(`Error parsing Jsonnet file: ${e}`);
192
- }
193
-
194
- const modulePath = path.resolve(path.join(__dirname, '../modules/'));
195
-
196
- fs.readdirSync(modulePath)
197
- .map(e => path.join(modulePath, e))
198
- .forEach(e => fs.unlinkSync(e));
199
-
200
- return this.lastRender;
201
- }
202
-
203
- loadModulesFromModuleDirectory(moduleFile) {
204
-
205
- const modulePath = path.join(baseDir, 'spellcraft_modules');
206
-
207
- if (!fs.existsSync(modulePath)) {
208
- return [];
209
- }
210
-
211
- const regex = /.*?\.js$/
212
- const fileList = fs.readdirSync(modulePath)
213
- .filter(f => regex.test(f))
214
- .map(f => path.join(modulePath, f));
215
-
216
- return this.loadModulesFromFileList(fileList, moduleFile);
217
- }
218
-
219
- loadModulesFromPackageList() {
220
- const packagePath = path.join(baseDir, 'spellcraft_modules', 'packages.json');
221
-
222
- if (!fs.existsSync(packagePath)) {
223
- console.log('No spellcraft_modules/packages.json file found. Skip package-based module import');
224
- return [];
225
- }
226
-
227
- const packages = JSON.parse(fs.readFileSync(packagePath));
228
-
229
- return Object.keys(packages).map(k => {
230
- const configFile = path.join(baseDir, 'node_modules', packages[k], 'package.json');
231
- const config = JSON.parse(fs.readFileSync(configFile));
232
-
233
- const jsMainFile = path.join(baseDir, 'node_modules', packages[k], config.main);
234
- this.loadFunctionsFromFile(jsMainFile);
235
-
236
- const moduleFile = path.resolve(path.join(__dirname, '..', 'modules', k));
237
- const importFile = path.resolve(path.join(jsMainFile, '..', 'module.libsonnet'));
238
-
239
- if (fs.existsSync(importFile)) {
240
- fs.copyFileSync(importFile, moduleFile);
241
- }
242
-
243
- return k;
244
- });
245
- }
246
-
247
- loadModulesFromFileList(fileList, moduleFile) {
248
-
249
- let registeredFunctions = [];
250
-
251
- if (fileList.length < 1) {
252
- return [];
253
- }
254
-
255
- let magicContent = [];
256
-
257
- fileList.map(file => {
258
- const { functions, magic } = this.loadFunctionsFromFile(file);
259
- registeredFunctions = registeredFunctions.concat(functions);
260
- magicContent = magicContent.concat(magic);
261
- });
262
-
263
- fs.writeFileSync(moduleFile, `{\n${magicContent.join(",\n")}\n}`);
264
-
265
- console.log(`[+] Registered ${fileList.length} module${(fileList.length > 1) ? 's' : ''} as '${path.basename(moduleFile)}' comprising ${registeredFunctions.length} function${(registeredFunctions.length > 1) ? 's' : ''}: [ ${registeredFunctions.sort().join(', ')} ]`)
266
-
267
- return { registeredFunctions, magicContent };
268
- }
269
-
270
- loadFunctionsFromFile(file) {
271
- const functions = require(file);
272
-
273
- const magicContent = [];
274
- if (functions._spellcraft_metadata) {
275
- const metadata = functions._spellcraft_metadata;
276
- ['fileTypeHandlers', 'functionContext'].forEach(e => Object.assign(this[e], metadata[e] ?? {}));
277
-
278
- ['cliExtensions'].forEach(e => metadata[e] && this[e].push(metadata[e]));
279
-
280
- metadata.initFn && this.init.push(metadata.initFn);
281
- }
282
-
283
- const registeredFunctions = Object.keys(functions).filter(e => e !== '_spellcraft_metadata').map(e => {
284
-
285
- let fn, parameters;
286
-
287
- if (typeof functions[e] == "object") {
288
- [fn, ...parameters] = functions[e];
289
- }
290
-
291
- if (typeof functions[e] == "function") {
292
- fn = functions[e];
293
- parameters = getFunctionParameterList(fn);
294
- }
295
-
296
- magicContent.push(`\t${e}(${parameters.join(', ')}):: std.native('${e}')(${parameters.join(', ')})`);
297
-
298
- this.addFunction(e, fn, ...parameters);
299
- return e;
300
- });
301
-
302
- return { functions: registeredFunctions, magic: magicContent };
303
- }
304
-
305
- toString() {
306
- if (this?.lastRender) {
307
- return this.lastRender
308
- }
309
-
310
- return null;
311
- }
312
-
313
- write(files = this.lastRender) {
314
- try {
315
- if (!fs.existsSync(this.renderPath)) {
316
- fs.mkdirSync(this.renderPath, { recursive: true });
317
- }
318
- } catch (e) {
319
- throw new Error(`Spellcraft Error: renderPath could not be created. ${e}`);
320
- }
321
-
322
- if (this.cleanBeforeRender) {
323
- try {
324
- Object.keys(this.fileTypeHandlers).forEach(regex => {
325
- // console.log(regex);
326
- fs.readdirSync(this.renderPath)
327
- .filter(f => new RegExp(regex, "i").test(f))
328
- .map(f => fs.unlinkSync(`${this.renderPath}/${f}`));
329
- });
330
- } catch (e) {
331
- throw new Error(`Failed to remove files from renderPath. ${e}`);
332
- }
333
- }
334
-
335
- try {
336
- for (const filename in files) {
337
- const outputPath = `${this.renderPath}/${filename}`;
338
-
339
- const [, handler] = Object.entries(this.fileTypeHandlers)
340
- .find(([pattern, ]) => new RegExp(pattern).test(filename)) || [false, defaultFileHandler];
341
-
342
- fs.writeFileSync(outputPath, handler(files[filename]));
343
- console.log(' ' + path.basename(outputPath));
344
- }
345
- } catch (e) {
346
- throw new Error(`Failed to write to renderPath. ${e}`);
347
- }
348
-
349
- return this;
350
- }
351
- };
352
-
353
- const defaultFileTypeHandlers = {
354
- // JSON files
355
- '.*?\.json$': (e) => {
356
- // console.log('Using JSON encoder');
357
- return JSON.stringify(e, null, 4)
358
- },
359
-
360
- // YAML files
361
- '.*?\.yaml$': (e) => {
362
- // console.log('Using YAML encoder');
363
- yaml.dump(e, {
364
- indent: 4
365
- })
366
- },
367
-
368
- '.*?\.yml$': (e) => {
369
- // console.log('Using YAML encoder');
370
- yaml.dump(e, {
371
- indent: 4
372
- })
373
- },
374
- };
375
-
376
- const defaultFileHandler = (e) => JSON.stringify(e, null, 4);
377
-
378
- function getFunctionParameterList(fn) {
379
-
380
- let str = fn.toString();
381
-
382
- str = str.replace(/\/\*[\s\S]*?\*\//g, '')
383
- .replace(/\/\/(.)*/g, '')
384
- .replace(/{[\s\S]*}/, '')
385
- .replace(/=>/g, '')
386
- .trim();
387
-
388
- const start = str.indexOf("(") + 1;
389
- const end = str.length - 1;
390
-
391
- const result = str.substring(start, end).split(", ");
392
-
393
- const params = [];
394
- result.forEach(element => {
395
- element = element.replace(/=[\s\S]*/g, '').trim();
396
-
397
- if(element.length > 0) {
398
- params.push(element);
399
- }
400
- });
401
-
402
- return params;
403
- }