@c6fc/spellcraft 0.0.11 → 0.1.1
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 -18
- package/package.json +2 -4
- package/src/index.js +145 -333
- package/src/test.js +2 -2
package/bin/spellcraft.js
CHANGED
|
@@ -100,24 +100,6 @@ const spellframe = new SpellFrame();
|
|
|
100
100
|
}*/
|
|
101
101
|
})
|
|
102
102
|
|
|
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
103
|
// No JSDoc for CLI extensions loop if considered internal detail
|
|
122
104
|
if (sfInstance.cliExtensions && sfInstance.cliExtensions.length > 0) {
|
|
123
105
|
sfInstance.cliExtensions.forEach((extensionFn) => {
|
package/package.json
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"dependencies": {
|
|
3
|
-
"@colors/colors": "^1.6.0",
|
|
4
3
|
"@hanazuki/node-jsonnet": "^0.4.2",
|
|
5
|
-
"ini": "^5.0.0",
|
|
6
4
|
"js-yaml": "^4.1.0",
|
|
7
5
|
"yargs": "^17.2.1"
|
|
8
6
|
},
|
|
9
7
|
"name": "@c6fc/spellcraft",
|
|
10
8
|
"description": "Extensible JSonnet CLI platform",
|
|
11
|
-
"version": "0.
|
|
9
|
+
"version": "0.1.1",
|
|
12
10
|
"main": "src/index.js",
|
|
13
11
|
"directories": {
|
|
14
12
|
"lib": "lib"
|
|
@@ -17,7 +15,7 @@
|
|
|
17
15
|
"spellcraft": "bin/spellcraft.js"
|
|
18
16
|
},
|
|
19
17
|
"scripts": {
|
|
20
|
-
"test": "
|
|
18
|
+
"test": "node src/test.js",
|
|
21
19
|
"doc": "jsdoc -c jsdoc.json --verbose"
|
|
22
20
|
},
|
|
23
21
|
"repository": {
|
package/src/index.js
CHANGED
|
@@ -4,79 +4,74 @@ 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
|
-
|
|
48
|
-
this.renderPath = path.resolve(this.currentPackagePath, this.renderPath);
|
|
49
|
-
this.modulePath = path.resolve(this.currentPackagePath, this.spellcraftModuleRelativePath);
|
|
50
|
-
|
|
51
|
-
this.jsonnet = new Jsonnet();
|
|
52
63
|
|
|
53
|
-
this.
|
|
54
|
-
.addJpath(path.join(
|
|
55
|
-
|
|
56
|
-
.
|
|
57
|
-
.cleanModulePath();
|
|
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'));
|
|
58
68
|
|
|
59
|
-
|
|
60
|
-
this.
|
|
69
|
+
// Built-in native functions
|
|
70
|
+
this.addNativeFunction("envvar", (name) => process.env[name] || false, "name");
|
|
71
|
+
this.addNativeFunction("path", () => this.activePath || process.cwd());
|
|
61
72
|
|
|
62
|
-
|
|
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;
|
|
73
|
+
// REFACTOR: Automatically find and register plugins from package.json
|
|
74
|
+
this.loadPluginsFromDependencies();
|
|
80
75
|
}
|
|
81
76
|
|
|
82
77
|
_generateCacheKey(functionName, args) {
|
|
@@ -84,11 +79,9 @@ exports.SpellFrame = class SpellFrame {
|
|
|
84
79
|
}
|
|
85
80
|
|
|
86
81
|
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
82
|
Object.defineProperty(this.fileTypeHandlers, pattern, {
|
|
90
83
|
value: handler,
|
|
91
|
-
writable: false,
|
|
84
|
+
writable: false,
|
|
92
85
|
enumerable: true,
|
|
93
86
|
configurable: true
|
|
94
87
|
});
|
|
@@ -101,8 +94,6 @@ exports.SpellFrame = class SpellFrame {
|
|
|
101
94
|
if (this._cache[key] !== undefined) {
|
|
102
95
|
return this._cache[key];
|
|
103
96
|
}
|
|
104
|
-
|
|
105
|
-
// Execute the function with `this.functionContext` as its `this` value.
|
|
106
97
|
const result = func.apply(this.functionContext, args);
|
|
107
98
|
this._cache[key] = result;
|
|
108
99
|
return result;
|
|
@@ -110,6 +101,12 @@ exports.SpellFrame = class SpellFrame {
|
|
|
110
101
|
return this;
|
|
111
102
|
}
|
|
112
103
|
|
|
104
|
+
addExternalCode(name, value) {
|
|
105
|
+
const finalValue = (typeof value === "string") ? value : JSON.stringify(value);
|
|
106
|
+
this.jsonnet = this.jsonnet.extCode(name, finalValue);
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
|
|
113
110
|
extendWithModuleMetadata(metadata) {
|
|
114
111
|
if (metadata.fileTypeHandlers) {
|
|
115
112
|
Object.entries(metadata.fileTypeHandlers).forEach(([pattern, handler]) => {
|
|
@@ -123,13 +120,6 @@ exports.SpellFrame = class SpellFrame {
|
|
|
123
120
|
this.initFn.push(...(Array.isArray(metadata.initFn) ? metadata.initFn : [metadata.initFn]));
|
|
124
121
|
}
|
|
125
122
|
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
123
|
return this;
|
|
134
124
|
}
|
|
135
125
|
|
|
@@ -139,226 +129,79 @@ exports.SpellFrame = class SpellFrame {
|
|
|
139
129
|
}
|
|
140
130
|
}
|
|
141
131
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
while (!fs.existsSync(path.join(checkPath, 'package.json')) && depth < maxdepth) {
|
|
152
|
-
path = path.join(checkPath, '..');
|
|
153
|
-
depth++;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (fs.existsSync(path.join(checkPath, 'package.json'))) {
|
|
157
|
-
return checkPath;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return false;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
getModulePackage(name) {
|
|
164
|
-
// For backwards compatability
|
|
165
|
-
if (name == '..') {
|
|
166
|
-
return this.currentPackage;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return require(require.resolve(name, { paths: [this.currentPackagePath] }));
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
getModulePackagePath(name) {
|
|
173
|
-
// For backwards compatability
|
|
174
|
-
if (name == '..') {
|
|
175
|
-
return this.currentPackagePath;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return path.dirname(require.resolve(name, { paths: [this.currentPackagePath] }));
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
loadFunctionsFromFile(file, as) {
|
|
182
|
-
|
|
183
|
-
const moduleExports = require(file);
|
|
184
|
-
|
|
185
|
-
const magicContentSnippets = [];
|
|
186
|
-
if (moduleExports._spellcraft_metadata) {
|
|
187
|
-
this.extendWithModuleMetadata(moduleExports._spellcraft_metadata);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const registeredFunctionNames = Object.keys(moduleExports)
|
|
191
|
-
.filter(key => key !== '_spellcraft_metadata' && typeof moduleExports[key] !== 'undefined')
|
|
192
|
-
.map(funcName => {
|
|
193
|
-
let func, params;
|
|
194
|
-
|
|
195
|
-
if (typeof moduleExports[funcName] === "object" && Array.isArray(moduleExports[funcName])) {
|
|
196
|
-
// Expects [function, paramName1, paramName2, ...]
|
|
197
|
-
[func, ...params] = moduleExports[funcName];
|
|
198
|
-
}
|
|
132
|
+
/**
|
|
133
|
+
* REFACTOR: Scans the project's package.json for dependencies.
|
|
134
|
+
* If a dependency has a 'spellcraft' key in its package.json,
|
|
135
|
+
* load its JS entrypoint and register native functions safely.
|
|
136
|
+
*/
|
|
137
|
+
loadPluginsFromDependencies() {
|
|
138
|
+
const packageJsonPath = path.join(baseDir, 'package.json');
|
|
139
|
+
if (!fs.existsSync(packageJsonPath)) return;
|
|
199
140
|
|
|
200
|
-
|
|
201
|
-
console.warn(`[!] Export '${funcName}' in module ${file} is not a valid function for native binding.`);
|
|
202
|
-
return null;
|
|
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;
|
|
141
|
+
let pkg;
|
|
229
142
|
try {
|
|
230
|
-
|
|
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
|
-
}
|
|
143
|
+
pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
144
|
+
} catch (e) { return; }
|
|
245
145
|
|
|
246
|
-
|
|
247
|
-
const importModuleConfig = this.getModulePackage(npmPackageName);
|
|
248
|
-
const importModulePath = this.getModulePackagePath(npmPackageName);
|
|
146
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
249
147
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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}`);
|
|
262
|
-
|
|
263
|
-
return this;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
loadModulesFromFileList(jsModuleFiles, as) {
|
|
267
|
-
let allRegisteredFunctions = [];
|
|
268
|
-
let allMagicContent = [];
|
|
148
|
+
Object.keys(deps).forEach(depName => {
|
|
149
|
+
try {
|
|
150
|
+
// Resolve the package.json of the dependency
|
|
151
|
+
const depPath = path.dirname(require.resolve(`${depName}/package.json`, { paths: [baseDir] }));
|
|
152
|
+
const depPkg = require(`${depName}/package.json`);
|
|
269
153
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
154
|
+
// Only load if it marks itself as a spellcraft module
|
|
155
|
+
if (depPkg.spellcraft || depPkg.keywords?.includes("spellcraft-module")) {
|
|
156
|
+
this.loadPlugin(depName, depPkg.main ? path.join(depPath, depPkg.main) : null);
|
|
157
|
+
}
|
|
158
|
+
} catch (e) {
|
|
159
|
+
// Dependency might not be installed or resolvable, skip quietly
|
|
160
|
+
}
|
|
273
161
|
});
|
|
274
|
-
|
|
275
|
-
return this;
|
|
276
162
|
}
|
|
277
163
|
|
|
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;
|
|
164
|
+
/**
|
|
165
|
+
* REFACTOR: Loads a specific plugin JS file.
|
|
166
|
+
* Namespaces native functions using the package name to prevent collisions.
|
|
167
|
+
* e.g., @c6fc/spellcraft-aws-auth exports 'aws' -> registered as '@c6fc/spellcraft-aws-auth:aws'
|
|
168
|
+
*/
|
|
169
|
+
loadPlugin(packageName, jsMainPath) {
|
|
170
|
+
if (!jsMainPath || !fs.existsSync(jsMainPath)) return;
|
|
299
171
|
|
|
172
|
+
let moduleExports;
|
|
300
173
|
try {
|
|
301
|
-
|
|
174
|
+
moduleExports = require(jsMainPath);
|
|
302
175
|
} catch (e) {
|
|
303
|
-
|
|
176
|
+
console.warn(`[!] Failed to load plugin ${packageName}: ${e.message}`);
|
|
177
|
+
return;
|
|
304
178
|
}
|
|
305
179
|
|
|
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);
|
|
180
|
+
if (moduleExports._spellcraft_metadata) {
|
|
181
|
+
this.extendWithModuleMetadata(moduleExports._spellcraft_metadata);
|
|
326
182
|
}
|
|
327
183
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
try {
|
|
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
|
-
}
|
|
184
|
+
Object.keys(moduleExports).forEach(key => {
|
|
185
|
+
if (key === '_spellcraft_metadata') return;
|
|
186
|
+
|
|
187
|
+
let func, params;
|
|
188
|
+
if (Array.isArray(moduleExports[key])) {
|
|
189
|
+
[func, ...params] = moduleExports[key];
|
|
190
|
+
} else if (typeof moduleExports[key] === "function") {
|
|
191
|
+
func = moduleExports[key];
|
|
192
|
+
params = getFunctionParameterList(func);
|
|
193
|
+
} else {
|
|
194
|
+
return;
|
|
345
195
|
}
|
|
346
196
|
|
|
347
|
-
//
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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}'`);
|
|
197
|
+
// REGISTER WITH NAMESPACE
|
|
198
|
+
// This is the key fix. We prefix the function name with the package name.
|
|
199
|
+
const uniqueId = `${packageName}:${key}`;
|
|
200
|
+
this.addNativeFunction(uniqueId, func, ...params);
|
|
357
201
|
|
|
358
|
-
|
|
359
|
-
console.log(`[
|
|
360
|
-
|
|
361
|
-
}
|
|
202
|
+
// Optional: Log debug info
|
|
203
|
+
// console.log(`[+] Registered native function: ${uniqueId}`);
|
|
204
|
+
});
|
|
362
205
|
}
|
|
363
206
|
|
|
364
207
|
async render(file) {
|
|
@@ -367,105 +210,74 @@ exports.SpellFrame = class SpellFrame {
|
|
|
367
210
|
throw new Error(`SpellCraft Render Error: Input file ${absoluteFilePath} does not exist.`);
|
|
368
211
|
}
|
|
369
212
|
|
|
370
|
-
this.activePath = path.dirname(absoluteFilePath);
|
|
371
|
-
|
|
372
|
-
this.magicContent.modules.push(this.loadedModules.flatMap(e => {
|
|
373
|
-
return `\t${e}:: import '${e}.libsonnet'`;
|
|
374
|
-
}));
|
|
213
|
+
this.activePath = path.dirname(absoluteFilePath);
|
|
375
214
|
|
|
376
|
-
if (this.
|
|
377
|
-
|
|
378
|
-
console.log(`[+] Registered native functions [${this.registeredFunctions.modules.join(', ').cyan}] to modules.${'modules'.green}`);
|
|
215
|
+
if (this.renderPath.endsWith(path.sep)) {
|
|
216
|
+
this.renderPath = this.renderPath.slice(0, -1);
|
|
379
217
|
}
|
|
380
218
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
219
|
+
try {
|
|
220
|
+
console.log(`[+] Evaluating Jsonnet file: ${absoluteFilePath}`);
|
|
221
|
+
this.lastRender = JSON.parse(await this.jsonnet.evaluateFile(absoluteFilePath));
|
|
222
|
+
} catch (e) {
|
|
223
|
+
throw new Error(`Jsonnet Evaluation Error: ${e.message || e}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return this.lastRender;
|
|
227
|
+
}
|
|
387
228
|
|
|
388
|
-
|
|
389
|
-
this.lastRender = JSON.parse(await this.jsonnet.evaluateFile(absoluteFilePath));
|
|
229
|
+
async renderString(snippet) {
|
|
390
230
|
|
|
391
|
-
|
|
392
|
-
this.cleanModulePath();
|
|
231
|
+
this.activePath = process.cwd();
|
|
393
232
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
233
|
+
try {
|
|
234
|
+
this.lastRender = JSON.parse(await this.jsonnet.evaluateSnippet(snippet));
|
|
235
|
+
} catch (e) {
|
|
236
|
+
throw new Error(`Jsonnet Evaluation Error: ${e.message || e}`);
|
|
397
237
|
}
|
|
398
238
|
|
|
399
239
|
return this.lastRender;
|
|
400
240
|
}
|
|
401
241
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
242
|
+
// Removed: importSpellCraftModuleFromNpm
|
|
243
|
+
// Removed: loadModulesFromModuleDirectory
|
|
244
|
+
// Removed: loadModulesFromPackageList
|
|
245
|
+
// Removed: loadModuleByName (file copier)
|
|
405
246
|
|
|
406
247
|
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
|
-
}
|
|
248
|
+
if (!filesToWrite || typeof filesToWrite !== 'object') return this;
|
|
411
249
|
|
|
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}`);
|
|
250
|
+
if (!fs.existsSync(this.renderPath)) {
|
|
251
|
+
fs.mkdirSync(this.renderPath, { recursive: true });
|
|
418
252
|
}
|
|
419
253
|
|
|
420
254
|
if (this.cleanBeforeRender) {
|
|
421
|
-
|
|
422
|
-
|
|
255
|
+
// ... (Cleaning logic remains the same)
|
|
256
|
+
try {
|
|
423
257
|
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
|
-
});
|
|
258
|
+
const regex = new RegExp(regexPattern, "i");
|
|
259
|
+
if(fs.existsSync(this.renderPath)) {
|
|
260
|
+
fs.readdirSync(this.renderPath).filter(f => regex.test(f)).forEach(f => fs.unlinkSync(path.join(this.renderPath, f)));
|
|
261
|
+
}
|
|
436
262
|
});
|
|
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
|
-
}
|
|
263
|
+
} catch (e) {}
|
|
441
264
|
}
|
|
442
265
|
|
|
443
266
|
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
|
-
}
|
|
267
|
+
for (const filename in filesToWrite) {
|
|
268
|
+
if (Object.prototype.hasOwnProperty.call(filesToWrite, filename)) {
|
|
269
|
+
const outputFilePath = path.join(this.renderPath, filename);
|
|
270
|
+
const [, handlerFn] = Object.entries(this.fileTypeHandlers)
|
|
271
|
+
.find(([pattern]) => new RegExp(pattern).test(filename)) || [null, defaultFileHandler];
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
fs.writeFileSync(outputFilePath, handlerFn(filesToWrite[filename]), 'utf-8');
|
|
275
|
+
console.log(' -> ' + path.basename(outputFilePath));
|
|
276
|
+
} catch (e) {
|
|
277
|
+
console.error(` [!] Error writing ${filename}: ${e.message}`);
|
|
462
278
|
}
|
|
463
279
|
}
|
|
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
280
|
}
|
|
468
|
-
|
|
469
281
|
return this;
|
|
470
282
|
}
|
|
471
283
|
};
|
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
|
})();
|