@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 +1 -1
- package/package.json +1 -1
- package/src/index.js +65 -101
- package/bin/spellcraft.js.bak +0 -47
- package/src/index.js.bak +0 -403
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
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
|
-
|
346
|
-
|
347
|
-
|
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
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
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
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
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
|
-
|
367
|
-
|
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
|
-
|
370
|
-
|
377
|
+
const packagesKey = name || spellcraftConfig.spellcraft_module_default_name;
|
378
|
+
packages[packagesKey] = npmPackageBaseName; // Store the clean package name
|
371
379
|
|
372
|
-
|
373
|
-
|
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
|
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
|
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
|
432
|
+
// Clean up the modules folder after rendering,
|
420
433
|
// as it's specific to this render pass and its discovered modules.
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
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
|
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|
|
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
|
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
|
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}'
|
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
|
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
|
-
|
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
|
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})`);
|
package/bin/spellcraft.js.bak
DELETED
@@ -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
|
-
}
|