@c6fc/spellcraft 0.0.11 → 0.1.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/bin/spellcraft.js +0 -39
- package/jsdoc.json +1 -2
- package/package.json +2 -3
- package/src/index.js +219 -317
- package/src/test.js +2 -2
package/bin/spellcraft.js
CHANGED
|
@@ -37,27 +37,6 @@ const spellframe = new SpellFrame();
|
|
|
37
37
|
* spellcraft generate ./myconfig.jsonnet
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
|
-
/**
|
|
41
|
-
* Links an npm package as a SpellCraft module for the current project.
|
|
42
|
-
* This command installs the specified npm package (if not already present) and
|
|
43
|
-
* registers it within the project's SpellCraft module configuration, making its
|
|
44
|
-
* functionalities available during the rendering process.
|
|
45
|
-
*
|
|
46
|
-
* **Usage:** `spellcraft importModule <npmPackage> [name]`
|
|
47
|
-
*
|
|
48
|
-
* @function importModule
|
|
49
|
-
* @name module:spellcraft-cli.importModule
|
|
50
|
-
* @param {object} argv - The arguments object provided by yargs.
|
|
51
|
-
* @param {string} argv.npmPackage The NPM package name of the SpellCraft Plugin to import. (Required)
|
|
52
|
-
* @param {string} [argv.name] An optional alias name to use for this module within SpellCraft.
|
|
53
|
-
* If not provided, a default name from the package may be used.
|
|
54
|
-
*
|
|
55
|
-
* @example
|
|
56
|
-
* spellcraft importModule my-spellcraft-enhancer
|
|
57
|
-
* @example
|
|
58
|
-
* spellcraft importModule @my-scope/spellcraft-utils customUtils
|
|
59
|
-
*/
|
|
60
|
-
|
|
61
40
|
// --- End of JSDoc Blocks for CLI Commands ---
|
|
62
41
|
|
|
63
42
|
(async () => {
|
|
@@ -100,24 +79,6 @@ const spellframe = new SpellFrame();
|
|
|
100
79
|
}*/
|
|
101
80
|
})
|
|
102
81
|
|
|
103
|
-
.command("importModule <npmPackage> [name]", "Configures the current project to use a SpellCraft plugin as an import", (yargsInstance) => {
|
|
104
|
-
return yargsInstance
|
|
105
|
-
.positional('npmPackage', {
|
|
106
|
-
describe: 'The NPM package name of a SpellCraft Plugin to import',
|
|
107
|
-
type: 'string',
|
|
108
|
-
demandOption: true,
|
|
109
|
-
})
|
|
110
|
-
.positional('name', {
|
|
111
|
-
describe: 'Optional alias name for the module in SpellCraft',
|
|
112
|
-
type: 'string',
|
|
113
|
-
default: undefined,
|
|
114
|
-
});
|
|
115
|
-
},
|
|
116
|
-
async (argv) => {
|
|
117
|
-
await sfInstance.importSpellCraftModuleFromNpm(argv.npmPackage, argv.name);
|
|
118
|
-
console.log(`[+] Module '${argv.npmPackage.green}' ${argv.name ? `(aliased as ${argv.name.green}) ` : ''}linked successfully.`);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
82
|
// No JSDoc for CLI extensions loop if considered internal detail
|
|
122
83
|
if (sfInstance.cliExtensions && sfInstance.cliExtensions.length > 0) {
|
|
123
84
|
sfInstance.cliExtensions.forEach((extensionFn) => {
|
package/jsdoc.json
CHANGED
package/package.json
CHANGED
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
"dependencies": {
|
|
3
3
|
"@colors/colors": "^1.6.0",
|
|
4
4
|
"@hanazuki/node-jsonnet": "^0.4.2",
|
|
5
|
-
"ini": "^5.0.0",
|
|
6
5
|
"js-yaml": "^4.1.0",
|
|
7
6
|
"yargs": "^17.2.1"
|
|
8
7
|
},
|
|
9
8
|
"name": "@c6fc/spellcraft",
|
|
10
9
|
"description": "Extensible JSonnet CLI platform",
|
|
11
|
-
"version": "0.
|
|
10
|
+
"version": "0.1.2",
|
|
12
11
|
"main": "src/index.js",
|
|
13
12
|
"directories": {
|
|
14
13
|
"lib": "lib"
|
|
@@ -17,7 +16,7 @@
|
|
|
17
16
|
"spellcraft": "bin/spellcraft.js"
|
|
18
17
|
},
|
|
19
18
|
"scripts": {
|
|
20
|
-
"test": "
|
|
19
|
+
"test": "node src/test.js",
|
|
21
20
|
"doc": "jsdoc -c jsdoc.json --verbose"
|
|
22
21
|
},
|
|
23
22
|
"repository": {
|
package/src/index.js
CHANGED
|
@@ -4,79 +4,78 @@ const fs = require("fs");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const yaml = require('js-yaml');
|
|
6
6
|
const crypto = require('crypto');
|
|
7
|
-
const colors = require('@colors/colors');
|
|
8
|
-
const { spawnSync } = require('child_process');
|
|
9
7
|
const { Jsonnet } = require("@hanazuki/node-jsonnet");
|
|
10
8
|
|
|
9
|
+
const baseDir = process.cwd();
|
|
10
|
+
|
|
11
11
|
const defaultFileTypeHandlers = {
|
|
12
|
-
// JSON files
|
|
13
12
|
'.*?\.json$': (content) => JSON.stringify(content, null, 4),
|
|
14
|
-
// YAML files
|
|
15
13
|
'.*?\.yaml$': (content) => yaml.dump(content, { indent: 4 }),
|
|
16
14
|
'.*?\.yml$': (content) => yaml.dump(content, { indent: 4 }),
|
|
17
15
|
};
|
|
18
16
|
|
|
19
17
|
const defaultFileHandler = (content) => JSON.stringify(content, null, 4);
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
function getFunctionParameterList(func) {
|
|
20
|
+
let funcStr = func.toString()
|
|
21
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
22
|
+
.replace(/\/\/(.)*/g, '')
|
|
23
|
+
.replace(/{[\s\S]*}/, '')
|
|
24
|
+
.replace(/=>/g, '')
|
|
25
|
+
.trim();
|
|
26
|
+
|
|
27
|
+
const paramStartIndex = funcStr.indexOf("(") + 1;
|
|
28
|
+
const paramEndIndex = funcStr.lastIndexOf(")");
|
|
29
|
+
|
|
30
|
+
if (paramStartIndex === 0 || paramEndIndex === -1 || paramStartIndex >= paramEndIndex) {
|
|
31
|
+
const potentialSingleArg = funcStr.split('=>')[0].trim();
|
|
32
|
+
if (potentialSingleArg && !potentialSingleArg.includes('(') && !potentialSingleArg.includes(')')) {
|
|
33
|
+
return [potentialSingleArg].filter(p => p.length > 0);
|
|
34
|
+
}
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const paramsString = funcStr.substring(paramStartIndex, paramEndIndex);
|
|
39
|
+
if (!paramsString.trim()) return [];
|
|
22
40
|
|
|
41
|
+
return paramsString.split(",")
|
|
42
|
+
.map(param => param.replace(/=[\s\S]*/g, '').trim())
|
|
43
|
+
.filter(param => param.length > 0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
exports.SpellFrame = class SpellFrame {
|
|
23
47
|
constructor(options = {}) {
|
|
24
48
|
const defaults = {
|
|
25
|
-
renderPath: "render",
|
|
26
|
-
spellcraftModuleRelativePath: ".spellcraft_linked_modules",
|
|
49
|
+
renderPath: "./render",
|
|
27
50
|
cleanBeforeRender: true,
|
|
28
|
-
cleanModulesAfterRender: true,
|
|
29
51
|
useDefaultFileHandlers: true
|
|
30
52
|
};
|
|
31
53
|
|
|
32
|
-
// Assign options, falling back to defaults
|
|
33
54
|
Object.assign(this, defaults, options);
|
|
34
55
|
|
|
35
56
|
this.initFn = [];
|
|
36
|
-
this._cache = {};
|
|
57
|
+
this._cache = {};
|
|
37
58
|
this.cliExtensions = [];
|
|
38
|
-
this.currentPackage = this.getCwdPackage();
|
|
39
|
-
this.currentPackagePath = this.getCwdPackagePath();
|
|
40
59
|
this.fileTypeHandlers = (this.useDefaultFileHandlers) ? { ...defaultFileTypeHandlers } : {};
|
|
41
60
|
this.functionContext = {};
|
|
42
61
|
this.lastRender = null;
|
|
43
62
|
this.activePath = null;
|
|
44
|
-
this.loadedModules = [];
|
|
45
|
-
this.magicContent = {}; // { modulefile: [...snippets] }
|
|
46
|
-
this.registeredFunctions = {}; // { modulefile: [...functionNames] }
|
|
47
63
|
|
|
48
|
-
this.
|
|
49
|
-
|
|
64
|
+
this.jsonnet = new Jsonnet()
|
|
65
|
+
.addJpath(path.join(__dirname, '../lib'))
|
|
66
|
+
// REFACTOR: Look in the local project's node_modules for explicit imports
|
|
67
|
+
.addJpath(path.join(baseDir, 'node_modules'))
|
|
68
|
+
.addJpath(path.join(baseDir, '.spellcraft'));
|
|
50
69
|
|
|
51
|
-
|
|
70
|
+
// Built-in native functions
|
|
71
|
+
this.addNativeFunction("envvar", (name) => process.env[name] || false, "name");
|
|
72
|
+
this.addNativeFunction("path", () => this.activePath || process.cwd());
|
|
52
73
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
.addNativeFunction("envvar", (name) => process.env[name] || false, "name")
|
|
56
|
-
.addNativeFunction("path", () => this.activePath || process.cwd()) // Use activePath if available
|
|
57
|
-
.cleanModulePath();
|
|
74
|
+
// REFACTOR: Automatically find and register plugins from package.json
|
|
75
|
+
this.loadPluginsFromDependencies();
|
|
58
76
|
|
|
59
|
-
|
|
60
|
-
this.
|
|
61
|
-
|
|
62
|
-
return this;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
cleanModulePath() {
|
|
66
|
-
if (!fs.existsSync(this.modulePath)) {
|
|
67
|
-
fs.mkdirSync(this.modulePath, { recursive: true });
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
fs.readdirSync(this.modulePath)
|
|
72
|
-
.map(e => path.join(this.modulePath, e))
|
|
73
|
-
.forEach(e => fs.unlinkSync(e));
|
|
74
|
-
|
|
75
|
-
} catch (e) {
|
|
76
|
-
throw new Error(`[!] Could not create/clean up temporary module folder ${path.dirname(this.modulePath).green}: ${e.message.red}`);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return this;
|
|
77
|
+
// 2. Load Local Magic Modules (Rapid Prototyping Mode)
|
|
78
|
+
this.loadLocalMagicModules();
|
|
80
79
|
}
|
|
81
80
|
|
|
82
81
|
_generateCacheKey(functionName, args) {
|
|
@@ -84,11 +83,9 @@ exports.SpellFrame = class SpellFrame {
|
|
|
84
83
|
}
|
|
85
84
|
|
|
86
85
|
addFileTypeHandler(pattern, handler) {
|
|
87
|
-
// Making it writable: false by default is a strong choice.
|
|
88
|
-
// If flexibility to override is needed later, this could be a simple assignment.
|
|
89
86
|
Object.defineProperty(this.fileTypeHandlers, pattern, {
|
|
90
87
|
value: handler,
|
|
91
|
-
writable: false,
|
|
88
|
+
writable: false,
|
|
92
89
|
enumerable: true,
|
|
93
90
|
configurable: true
|
|
94
91
|
});
|
|
@@ -101,8 +98,6 @@ exports.SpellFrame = class SpellFrame {
|
|
|
101
98
|
if (this._cache[key] !== undefined) {
|
|
102
99
|
return this._cache[key];
|
|
103
100
|
}
|
|
104
|
-
|
|
105
|
-
// Execute the function with `this.functionContext` as its `this` value.
|
|
106
101
|
const result = func.apply(this.functionContext, args);
|
|
107
102
|
this._cache[key] = result;
|
|
108
103
|
return result;
|
|
@@ -110,6 +105,12 @@ exports.SpellFrame = class SpellFrame {
|
|
|
110
105
|
return this;
|
|
111
106
|
}
|
|
112
107
|
|
|
108
|
+
addExternalCode(name, value) {
|
|
109
|
+
const finalValue = (typeof value === "string") ? value : JSON.stringify(value);
|
|
110
|
+
this.jsonnet = this.jsonnet.extCode(name, finalValue);
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
|
|
113
114
|
extendWithModuleMetadata(metadata) {
|
|
114
115
|
if (metadata.fileTypeHandlers) {
|
|
115
116
|
Object.entries(metadata.fileTypeHandlers).forEach(([pattern, handler]) => {
|
|
@@ -123,13 +124,6 @@ exports.SpellFrame = class SpellFrame {
|
|
|
123
124
|
this.initFn.push(...(Array.isArray(metadata.initFn) ? metadata.initFn : [metadata.initFn]));
|
|
124
125
|
}
|
|
125
126
|
Object.assign(this.functionContext, metadata.functionContext || {});
|
|
126
|
-
|
|
127
|
-
return this;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
addJpath(jpath) {
|
|
131
|
-
// console.log(`[*] Adding Jpath ${jpath}`);
|
|
132
|
-
this.jsonnet.addJpath(jpath);
|
|
133
127
|
return this;
|
|
134
128
|
}
|
|
135
129
|
|
|
@@ -139,226 +133,165 @@ exports.SpellFrame = class SpellFrame {
|
|
|
139
133
|
}
|
|
140
134
|
}
|
|
141
135
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
136
|
+
/**
|
|
137
|
+
* REFACTOR: Scans the project's package.json for dependencies.
|
|
138
|
+
* If a dependency has a 'spellcraft' key in its package.json,
|
|
139
|
+
* load its JS entrypoint and register native functions safely.
|
|
140
|
+
*/
|
|
141
|
+
loadPluginsFromDependencies() {
|
|
142
|
+
const packageJsonPath = path.join(baseDir, 'package.json');
|
|
143
|
+
if (!fs.existsSync(packageJsonPath)) return;
|
|
150
144
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
145
|
+
let pkg;
|
|
146
|
+
try {
|
|
147
|
+
pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
148
|
+
} catch (e) { return; }
|
|
155
149
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
150
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
151
|
+
|
|
152
|
+
// Create a require function that operates as if it's inside the user's project
|
|
153
|
+
const userProjectRequire = require('module').createRequire(packageJsonPath);
|
|
159
154
|
|
|
160
|
-
|
|
155
|
+
Object.keys(deps).forEach(depName => {
|
|
156
|
+
try {
|
|
157
|
+
// 1. Find the path to the dependency's package.json using the USER'S context
|
|
158
|
+
const depPackageJsonPath = userProjectRequire.resolve(`${depName}/package.json`);
|
|
159
|
+
|
|
160
|
+
// 2. Load that package.json using the absolute path
|
|
161
|
+
const depPkg = require(depPackageJsonPath);
|
|
162
|
+
const depDir = path.dirname(depPackageJsonPath);
|
|
163
|
+
|
|
164
|
+
// 3. Check for SpellCraft metadata
|
|
165
|
+
if (depPkg.spellcraft || depPkg.keywords?.includes("spellcraft-module")) {
|
|
166
|
+
const jsMainPath = path.join(depDir, depPkg.main || 'index.js');
|
|
167
|
+
|
|
168
|
+
// 4. Load the plugin using the calculated absolute path
|
|
169
|
+
this.loadPlugin(depName, jsMainPath);
|
|
170
|
+
}
|
|
171
|
+
} catch (e) {
|
|
172
|
+
// Dependency might not be installed or resolvable, skip quietly
|
|
173
|
+
console.warn(`Debug: Could not load potential plugin ${depName}: ${e.message}`);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
161
176
|
}
|
|
162
177
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
178
|
+
/**
|
|
179
|
+
* Scans the local 'spellcraft_modules' directory.
|
|
180
|
+
* 1. Registers JS exports as native functions (prefixed with 'local_<filename>_').
|
|
181
|
+
* 2. Generates a .spellcraft/modules.libsonnet file to allow `import 'modules'`.
|
|
182
|
+
*/
|
|
183
|
+
loadLocalMagicModules() {
|
|
184
|
+
const localModulesDir = path.join(baseDir, 'spellcraft_modules');
|
|
185
|
+
const generatedDir = path.join(baseDir, '.spellcraft');
|
|
186
|
+
const aggregateFile = path.join(generatedDir, 'modules');
|
|
187
|
+
|
|
188
|
+
if (!fs.existsSync(localModulesDir)) {
|
|
189
|
+
// Clean up if it exists so imports fail gracefully if folder is deleted
|
|
190
|
+
if(fs.existsSync(aggregateFile)) fs.unlinkSync(aggregateFile);
|
|
191
|
+
return;
|
|
167
192
|
}
|
|
168
193
|
|
|
169
|
-
|
|
170
|
-
|
|
194
|
+
// Ensure hidden directory exists
|
|
195
|
+
if (!fs.existsSync(generatedDir)) fs.mkdirSync(generatedDir, { recursive: true });
|
|
171
196
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
return this.currentPackagePath;
|
|
176
|
-
}
|
|
197
|
+
const jsFiles = fs.readdirSync(localModulesDir).filter(f => f.endsWith('.js'));
|
|
198
|
+
|
|
199
|
+
let jsonnetContentParts = [];
|
|
177
200
|
|
|
178
|
-
|
|
179
|
-
|
|
201
|
+
jsFiles.forEach(file => {
|
|
202
|
+
const moduleName = path.basename(file, '.js');
|
|
203
|
+
const fullPath = path.join(localModulesDir, file);
|
|
204
|
+
|
|
205
|
+
let moduleExports;
|
|
206
|
+
try {
|
|
207
|
+
// Cache busting for dev speed
|
|
208
|
+
delete require.cache[require.resolve(fullPath)];
|
|
209
|
+
moduleExports = require(fullPath);
|
|
210
|
+
} catch (e) {
|
|
211
|
+
console.warn(`[!] Error loading local module ${file}: ${e.message}`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
180
214
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const moduleExports = require(file);
|
|
215
|
+
let fileMethods = [];
|
|
184
216
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
this.extendWithModuleMetadata(moduleExports._spellcraft_metadata);
|
|
188
|
-
}
|
|
217
|
+
Object.keys(moduleExports).forEach(funcName => {
|
|
218
|
+
if (funcName === '_spellcraft_metadata') return; // Skip metadata
|
|
189
219
|
|
|
190
|
-
const registeredFunctionNames = Object.keys(moduleExports)
|
|
191
|
-
.filter(key => key !== '_spellcraft_metadata' && typeof moduleExports[key] !== 'undefined')
|
|
192
|
-
.map(funcName => {
|
|
193
220
|
let func, params;
|
|
194
|
-
|
|
195
|
-
if (
|
|
196
|
-
// Expects [function, paramName1, paramName2, ...]
|
|
221
|
+
// Handle [func, "arg1", "arg2"] syntax or plain function
|
|
222
|
+
if (Array.isArray(moduleExports[funcName])) {
|
|
197
223
|
[func, ...params] = moduleExports[funcName];
|
|
224
|
+
} else if (typeof moduleExports[funcName] === 'function') {
|
|
225
|
+
func = moduleExports[funcName];
|
|
226
|
+
// You'll need the getFunctionParameterList helper from before
|
|
227
|
+
params = getFunctionParameterList(func);
|
|
228
|
+
} else {
|
|
229
|
+
return;
|
|
198
230
|
}
|
|
199
231
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// For `modules` to provide convenient wrappers:
|
|
206
|
-
// e.g. myNativeFunc(a,b):: std.native('myNativeFunc')(a,b)
|
|
207
|
-
const paramString = params.join(', ');
|
|
208
|
-
magicContentSnippets.push(`\t${funcName}(${paramString}):: std.native('${funcName}')(${paramString})`);
|
|
209
|
-
|
|
210
|
-
this.addNativeFunction(funcName, func, ...params);
|
|
211
|
-
return funcName;
|
|
212
|
-
}).filter(Boolean); // Remove nulls from skipped items
|
|
213
|
-
|
|
214
|
-
this.registeredFunctions[as] = registeredFunctionNames;
|
|
215
|
-
this.magicContent[as] = magicContentSnippets;
|
|
216
|
-
|
|
217
|
-
return this;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
loadModulesFromPackageList() {
|
|
221
|
-
const packagesConfigPath = path.join(this.currentPackagePath, 'spellcraft_modules', 'packages.json');
|
|
222
|
-
|
|
223
|
-
if (!fs.existsSync(packagesConfigPath)) {
|
|
224
|
-
// console.log('[+] No spellcraft_modules/packages.json file found. Skipping package-based module import.');
|
|
225
|
-
return [];
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
let packages;
|
|
229
|
-
try {
|
|
230
|
-
packages = JSON.parse(fs.readFileSync(packagesConfigPath, 'utf-8'));
|
|
231
|
-
} catch (e) {
|
|
232
|
-
console.error(`[!] Error parsing ${packagesConfigPath.green}: ${e.message.red}. Skipping package-based module import.`);
|
|
233
|
-
return [];
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return Object.entries(packages).map(([npmPackageName, moduleKey]) => {
|
|
237
|
-
this.loadModuleByName(moduleKey, npmPackageName);
|
|
238
|
-
return moduleKey;
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
loadCurrentPackageAsModule(moduleKey) {
|
|
243
|
-
return this.loadModuleByName(moduleKey, '..');
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
loadModuleByName(moduleKey, npmPackageName) {
|
|
247
|
-
const importModuleConfig = this.getModulePackage(npmPackageName);
|
|
248
|
-
const importModulePath = this.getModulePackagePath(npmPackageName);
|
|
249
|
-
|
|
250
|
-
this.loadFunctionsFromFile(path.resolve(importModulePath, 'module.js'), moduleKey);
|
|
251
|
-
|
|
252
|
-
const sourceLibsonnetPath = path.resolve(importModulePath, 'module.libsonnet');
|
|
253
|
-
const targetLibsonnetPath = path.resolve(this.modulePath, `${moduleKey}.libsonnet`);
|
|
254
|
-
|
|
255
|
-
if (fs.existsSync(targetLibsonnetPath)) {
|
|
256
|
-
throw new Error(`[!] Module library ${path.basename(targetLibsonnetPath)} already exists. This means there is a conflict with package link names.`);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
fs.copyFileSync(sourceLibsonnetPath, targetLibsonnetPath);
|
|
260
|
-
|
|
261
|
-
console.log(`[+] Linked ${(npmPackageName == '..') ? 'this package'.green : npmPackageName.green} as ${path.basename(targetLibsonnetPath).green}`);
|
|
232
|
+
// Register with a unique local prefix
|
|
233
|
+
const uniqueId = `local_${moduleName}_${funcName}`;
|
|
234
|
+
this.addNativeFunction(uniqueId, func, ...params);
|
|
262
235
|
|
|
263
|
-
|
|
264
|
-
|
|
236
|
+
// Create the Jsonnet wrapper string
|
|
237
|
+
// e.g. myFunc(a, b):: std.native("local_utils_myFunc")(a, b)
|
|
238
|
+
const paramStr = params.join(", ");
|
|
239
|
+
fileMethods.push(` ${funcName}(${paramStr}):: std.native("${uniqueId}")(${paramStr})`);
|
|
240
|
+
});
|
|
265
241
|
|
|
266
|
-
|
|
267
|
-
let allRegisteredFunctions = [];
|
|
268
|
-
let allMagicContent = [];
|
|
242
|
+
console.log(`[+] Loaded [${Object.keys(moduleExports).join(", ")}] from [${file}].`);
|
|
269
243
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
244
|
+
if (fileMethods.length > 0) {
|
|
245
|
+
jsonnetContentParts.push(` ${moduleName}: {\n${fileMethods.join(",\n")}\n }`);
|
|
246
|
+
}
|
|
273
247
|
});
|
|
274
248
|
|
|
275
|
-
|
|
249
|
+
// Generate the file
|
|
250
|
+
const finalContent = "{\n" + jsonnetContentParts.join(",\n") + "\n}";
|
|
251
|
+
fs.writeFileSync(aggregateFile, finalContent, 'utf-8');
|
|
276
252
|
}
|
|
277
253
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (
|
|
285
|
-
console.log("[-] This package is a SpellCraft module. Skipping directory-based module import.");
|
|
286
|
-
return { registeredFunctions: [], magicContent: [] };
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const jsModuleFiles = fs.readdirSync(spellcraftModulesPath)
|
|
290
|
-
.filter(f => f.endsWith('.js')) // Simpler check for .js files
|
|
291
|
-
.map(f => path.join(spellcraftModulesPath, f));
|
|
292
|
-
|
|
293
|
-
return this.loadModulesFromFileList(jsModuleFiles, 'modules');
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
async importSpellCraftModuleFromNpm(npmPackage, name = false) {
|
|
297
|
-
|
|
298
|
-
let packagePath;
|
|
254
|
+
/**
|
|
255
|
+
* REFACTOR: Loads a specific plugin JS file.
|
|
256
|
+
* Namespaces native functions using the package name to prevent collisions.
|
|
257
|
+
* e.g., @c6fc/spellcraft-aws-auth exports 'aws' -> registered as '@c6fc/spellcraft-aws-auth:aws'
|
|
258
|
+
*/
|
|
259
|
+
loadPlugin(packageName, jsMainPath) {
|
|
260
|
+
if (!jsMainPath || !fs.existsSync(jsMainPath)) return;
|
|
299
261
|
|
|
262
|
+
let moduleExports;
|
|
300
263
|
try {
|
|
301
|
-
|
|
264
|
+
moduleExports = require(jsMainPath);
|
|
302
265
|
} catch (e) {
|
|
303
|
-
|
|
266
|
+
console.warn(`[!] Failed to load plugin ${packageName}: ${e.message}`);
|
|
267
|
+
return;
|
|
304
268
|
}
|
|
305
269
|
|
|
306
|
-
if (
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const install = spawnSync(`npm`, ['install', '--save', npmPackage], {
|
|
310
|
-
cwd: this.currentPackagePath,
|
|
311
|
-
stdio: 'inherit'
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
if (install.error || install.status !== 0) {
|
|
315
|
-
throw new Error(`Failed to install npm package ${npmPackage.blue}. Error: ${install.error.red || install.stderr.toString().red}`);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
console.log(`[+] Successfully installed ${npmPackage.blue}.`);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const importModuleConfig = this.getModulePackage(`${npmPackage}/package.json`).config;
|
|
322
|
-
const currentPackageConfig = this.currentPackage.config;
|
|
323
|
-
|
|
324
|
-
if (!name && !!!importModuleConfig?.spellcraft_module_default_name) {
|
|
325
|
-
throw new Error(`[!] No import name specified for ${npmPackage.blue}, and it has no 'spellcraft_module_default_name' in its package.json config.`.red);
|
|
270
|
+
if (moduleExports._spellcraft_metadata) {
|
|
271
|
+
this.extendWithModuleMetadata(moduleExports._spellcraft_metadata);
|
|
326
272
|
}
|
|
327
273
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (
|
|
333
|
-
|
|
274
|
+
Object.keys(moduleExports).forEach(key => {
|
|
275
|
+
if (key === '_spellcraft_metadata') return;
|
|
276
|
+
|
|
277
|
+
let func, params;
|
|
278
|
+
if (Array.isArray(moduleExports[key])) {
|
|
279
|
+
[func, ...params] = moduleExports[key];
|
|
280
|
+
} else if (typeof moduleExports[key] === "function") {
|
|
281
|
+
func = moduleExports[key];
|
|
282
|
+
params = getFunctionParameterList(func);
|
|
283
|
+
} else {
|
|
284
|
+
return;
|
|
334
285
|
}
|
|
335
286
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
packages = JSON.parse(fs.readFileSync(packagesFilePath, 'utf-8'));
|
|
341
|
-
} catch (e) {
|
|
342
|
-
console.warn(`[!] Could not parse existing ${packagesFilePath}. Starting fresh. Error: ${e.message}`.red);
|
|
343
|
-
packages = {};
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Derive the base name to store (e.g., "my-package" from "my-package@1.0.0")
|
|
348
|
-
const npmPackageBaseName = npmPackage.startsWith("@") ?
|
|
349
|
-
`@${npmPackage.split('/')[1].split('@')[0]}` : // Handles @scope/name and @scope/name@version
|
|
350
|
-
npmPackage.split('@')[0]; // Handles name and name@version
|
|
351
|
-
|
|
352
|
-
const packagesKey = name || importModuleConfig.spellcraft_module_default_name;
|
|
353
|
-
packages[npmPackage] = packagesKey; // Store the clean package name
|
|
354
|
-
|
|
355
|
-
fs.writeFileSync(packagesFilePath, JSON.stringify(packages, null, "\t"));
|
|
356
|
-
console.log(`[+] Linked ${npmPackage} as SpellCraft module '${packagesKey}'`);
|
|
287
|
+
// REGISTER WITH NAMESPACE
|
|
288
|
+
// This is the key fix. We prefix the function name with the package name.
|
|
289
|
+
const uniqueId = `${packageName}:${key}`;
|
|
290
|
+
this.addNativeFunction(uniqueId, func, ...params);
|
|
357
291
|
|
|
358
|
-
|
|
359
|
-
console.log(`[
|
|
360
|
-
|
|
361
|
-
}
|
|
292
|
+
// Optional: Log debug info
|
|
293
|
+
// console.log(`[+] Registered native function: ${uniqueId}`);
|
|
294
|
+
});
|
|
362
295
|
}
|
|
363
296
|
|
|
364
297
|
async render(file) {
|
|
@@ -367,105 +300,74 @@ exports.SpellFrame = class SpellFrame {
|
|
|
367
300
|
throw new Error(`SpellCraft Render Error: Input file ${absoluteFilePath} does not exist.`);
|
|
368
301
|
}
|
|
369
302
|
|
|
370
|
-
this.activePath = path.dirname(absoluteFilePath);
|
|
303
|
+
this.activePath = path.dirname(absoluteFilePath);
|
|
371
304
|
|
|
372
|
-
this.
|
|
373
|
-
|
|
374
|
-
}));
|
|
375
|
-
|
|
376
|
-
if (this.registeredFunctions.modules.length > 0) {
|
|
377
|
-
fs.writeFileSync(path.join(this.modulePath, `modules`), `{\n${this.magicContent.modules.join(",\n")}\n}`, 'utf-8');
|
|
378
|
-
console.log(`[+] Registered native functions [${this.registeredFunctions.modules.join(', ').cyan}] to modules.${'modules'.green}`);
|
|
305
|
+
if (this.renderPath.endsWith(path.sep)) {
|
|
306
|
+
this.renderPath = this.renderPath.slice(0, -1);
|
|
379
307
|
}
|
|
380
308
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
309
|
+
try {
|
|
310
|
+
console.log(`[+] Evaluating Jsonnet file: ${absoluteFilePath}`);
|
|
311
|
+
this.lastRender = JSON.parse(await this.jsonnet.evaluateFile(absoluteFilePath));
|
|
312
|
+
} catch (e) {
|
|
313
|
+
throw new Error(`Jsonnet Evaluation Error: ${e.message || e}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return this.lastRender;
|
|
317
|
+
}
|
|
387
318
|
|
|
388
|
-
|
|
389
|
-
this.lastRender = JSON.parse(await this.jsonnet.evaluateFile(absoluteFilePath));
|
|
319
|
+
async renderString(snippet) {
|
|
390
320
|
|
|
391
|
-
|
|
392
|
-
this.cleanModulePath();
|
|
321
|
+
this.activePath = process.cwd();
|
|
393
322
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
323
|
+
try {
|
|
324
|
+
this.lastRender = JSON.parse(await this.jsonnet.evaluateSnippet(snippet));
|
|
325
|
+
} catch (e) {
|
|
326
|
+
throw new Error(`Jsonnet Evaluation Error: ${e.message || e}`);
|
|
397
327
|
}
|
|
398
328
|
|
|
399
329
|
return this.lastRender;
|
|
400
330
|
}
|
|
401
331
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
332
|
+
// Removed: importSpellCraftModuleFromNpm
|
|
333
|
+
// Removed: loadModulesFromModuleDirectory
|
|
334
|
+
// Removed: loadModulesFromPackageList
|
|
335
|
+
// Removed: loadModuleByName (file copier)
|
|
405
336
|
|
|
406
337
|
write(filesToWrite = this.lastRender) {
|
|
407
|
-
if (!filesToWrite || typeof filesToWrite !== 'object'
|
|
408
|
-
console.log("[+] No files to write from the last render or provided input.");
|
|
409
|
-
return this;
|
|
410
|
-
}
|
|
338
|
+
if (!filesToWrite || typeof filesToWrite !== 'object') return this;
|
|
411
339
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
fs.mkdirSync(this.renderPath, { recursive: true });
|
|
415
|
-
}
|
|
416
|
-
} catch (e) {
|
|
417
|
-
throw new Error(`SpellCraft Write Error: renderPath '${this.renderPath}' could not be created. ${e.message}`);
|
|
340
|
+
if (!fs.existsSync(this.renderPath)) {
|
|
341
|
+
fs.mkdirSync(this.renderPath, { recursive: true });
|
|
418
342
|
}
|
|
419
343
|
|
|
420
344
|
if (this.cleanBeforeRender) {
|
|
421
|
-
|
|
422
|
-
|
|
345
|
+
// ... (Cleaning logic remains the same)
|
|
346
|
+
try {
|
|
423
347
|
Object.keys(this.fileTypeHandlers).forEach(regexPattern => {
|
|
424
|
-
const regex = new RegExp(regexPattern, "i");
|
|
425
|
-
fs.
|
|
426
|
-
.filter(f => regex.test(f))
|
|
427
|
-
|
|
428
|
-
const filePathToClean = path.join(this.renderPath, f);
|
|
429
|
-
try {
|
|
430
|
-
fs.unlinkSync(filePathToClean);
|
|
431
|
-
// console.log(` - Removed ${filePathToClean}`);
|
|
432
|
-
} catch (cleanError) {
|
|
433
|
-
console.warn(` [!] Failed to remove ${filePathToClean}: ${cleanError.message}`);
|
|
434
|
-
}
|
|
435
|
-
});
|
|
348
|
+
const regex = new RegExp(regexPattern, "i");
|
|
349
|
+
if(fs.existsSync(this.renderPath)) {
|
|
350
|
+
fs.readdirSync(this.renderPath).filter(f => regex.test(f)).forEach(f => fs.unlinkSync(path.join(this.renderPath, f)));
|
|
351
|
+
}
|
|
436
352
|
});
|
|
437
|
-
} catch (e) {
|
|
438
|
-
// This error is for readdirSync itself, less likely but possible
|
|
439
|
-
throw new Error(`SpellCraft Clean Error: Failed to read/clean files from renderPath '${this.renderPath}'. ${e.message}`);
|
|
440
|
-
}
|
|
353
|
+
} catch (e) {}
|
|
441
354
|
}
|
|
442
355
|
|
|
443
356
|
console.log(`[+] Writing files to: ${this.renderPath}`);
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const processedContent = handlerFn(fileContent);
|
|
456
|
-
fs.writeFileSync(outputFilePath, processedContent, 'utf-8');
|
|
457
|
-
console.log(` -> ${path.basename(outputFilePath).green}`);
|
|
458
|
-
} catch (handlerError) {
|
|
459
|
-
console.error(` [!] Error processing or writing file ${filename}: ${handlerError.message}`);
|
|
460
|
-
// Optionally re-throw or collect errors
|
|
461
|
-
}
|
|
357
|
+
for (const filename in filesToWrite) {
|
|
358
|
+
if (Object.prototype.hasOwnProperty.call(filesToWrite, filename)) {
|
|
359
|
+
const outputFilePath = path.join(this.renderPath, filename);
|
|
360
|
+
const [, handlerFn] = Object.entries(this.fileTypeHandlers)
|
|
361
|
+
.find(([pattern]) => new RegExp(pattern).test(filename)) || [null, defaultFileHandler];
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
fs.writeFileSync(outputFilePath, handlerFn(filesToWrite[filename]), 'utf-8');
|
|
365
|
+
console.log(' -> ' + path.basename(outputFilePath));
|
|
366
|
+
} catch (e) {
|
|
367
|
+
console.error(` [!] Error writing ${filename}: ${e.message}`);
|
|
462
368
|
}
|
|
463
369
|
}
|
|
464
|
-
} catch (e) {
|
|
465
|
-
// This would catch errors in the loop structure itself, less likely for file operations
|
|
466
|
-
throw new Error(`SpellCraft Write Error: Failed during file writing loop. ${e.message}`);
|
|
467
370
|
}
|
|
468
|
-
|
|
469
371
|
return this;
|
|
470
372
|
}
|
|
471
373
|
};
|
package/src/test.js
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const { SpellFrame } = require('./index.js');
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const spell = new SpellFrame({
|
|
7
7
|
renderPath: './render',
|
|
8
8
|
cleanBeforeRender: true
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
(async () => {
|
|
12
12
|
|
|
13
|
-
testBootstrap =
|
|
13
|
+
testBootstrap = await spell.renderString(`local spellcraft = import 'spellcraft'; { test: spellcraft.path() }`);
|
|
14
14
|
console.log(testBootstrap);
|
|
15
15
|
|
|
16
16
|
})();
|