@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 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
@@ -9,8 +9,7 @@
9
9
  "source": {
10
10
  "include": [
11
11
  "src/",
12
- "bin/",
13
- "lib/"
12
+ "bin/"
14
13
  ],
15
14
  "includePattern": ".",
16
15
  "excludePattern": "\\.bak$"
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.0.11",
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": "echo \"Error: no test specified\" && exit 1",
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
- exports.SpellFrame = class SpellFrame {
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 = {}; // Initialize 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.renderPath = path.resolve(this.currentPackagePath, this.renderPath);
49
- this.modulePath = path.resolve(this.currentPackagePath, this.spellcraftModuleRelativePath);
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
- this.jsonnet = new Jsonnet();
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
- this.addJpath(path.join(__dirname, '../lib')) // For core SpellCraft libsonnet files
54
- .addJpath(path.join(this.modulePath)) // For dynamically generated module imports
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
- this.loadedModules = this.loadModulesFromPackageList();
60
- this.loadModulesFromModuleDirectory();
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, // Or true if overrides should be easy
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
- getCwdPackage() {
143
- return require(path.resolve(this.getCwdPackagePath(), 'package.json'));
144
- }
145
-
146
- getCwdPackagePath() {
147
- let depth = 0;
148
- const maxdepth = 3
149
- let checkPath = process.cwd();
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
- while (!fs.existsSync(path.join(checkPath, 'package.json')) && depth < maxdepth) {
152
- path = path.join(checkPath, '..');
153
- depth++;
154
- }
145
+ let pkg;
146
+ try {
147
+ pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
148
+ } catch (e) { return; }
155
149
 
156
- if (fs.existsSync(path.join(checkPath, 'package.json'))) {
157
- return checkPath;
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
- return false;
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
- getModulePackage(name) {
164
- // For backwards compatability
165
- if (name == '..') {
166
- return this.currentPackage;
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
- return require(require.resolve(name, { paths: [this.currentPackagePath] }));
170
- }
194
+ // Ensure hidden directory exists
195
+ if (!fs.existsSync(generatedDir)) fs.mkdirSync(generatedDir, { recursive: true });
171
196
 
172
- getModulePackagePath(name) {
173
- // For backwards compatability
174
- if (name == '..') {
175
- return this.currentPackagePath;
176
- }
197
+ const jsFiles = fs.readdirSync(localModulesDir).filter(f => f.endsWith('.js'));
198
+
199
+ let jsonnetContentParts = [];
177
200
 
178
- return path.dirname(require.resolve(name, { paths: [this.currentPackagePath] }));
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
- loadFunctionsFromFile(file, as) {
182
-
183
- const moduleExports = require(file);
215
+ let fileMethods = [];
184
216
 
185
- const magicContentSnippets = [];
186
- if (moduleExports._spellcraft_metadata) {
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 (typeof moduleExports[funcName] === "object" && Array.isArray(moduleExports[funcName])) {
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
- if (typeof func !== 'function') {
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;
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
- return this;
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
- loadModulesFromFileList(jsModuleFiles, as) {
267
- let allRegisteredFunctions = [];
268
- let allMagicContent = [];
242
+ console.log(`[+] Loaded [${Object.keys(moduleExports).join(", ")}] from [${file}].`);
269
243
 
270
- jsModuleFiles.forEach(file => {
271
- this.loadFunctionsFromFile(file, as);
272
- console.log(`[+] Loaded [${this.registeredFunctions[as].join(', ').cyan}] from ${path.basename(file).green} into modules.${as.green}`);
244
+ if (fileMethods.length > 0) {
245
+ jsonnetContentParts.push(` ${moduleName}: {\n${fileMethods.join(",\n")}\n }`);
246
+ }
273
247
  });
274
248
 
275
- return this;
249
+ // Generate the file
250
+ const finalContent = "{\n" + jsonnetContentParts.join(",\n") + "\n}";
251
+ fs.writeFileSync(aggregateFile, finalContent, 'utf-8');
276
252
  }
277
253
 
278
- loadModulesFromModuleDirectory() {
279
- const spellcraftModulesPath = path.join(this.currentPackagePath, 'spellcraft_modules');
280
- if (!fs.existsSync(spellcraftModulesPath)) {
281
- return this;
282
- }
283
-
284
- if (!!this.currentPackage?.config?.spellcraft_module_default_name) {
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
- packagePath = fs.existsSync(this.getModulePackagePath(npmPackage));
264
+ moduleExports = require(jsMainPath);
302
265
  } catch (e) {
303
- packagePath = false;
266
+ console.warn(`[!] Failed to load plugin ${packageName}: ${e.message}`);
267
+ return;
304
268
  }
305
269
 
306
- if (!packagePath) {
307
- console.log(`[*] Attempting to install ${npmPackage.blue}...`);
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
- // Only link if this package is not a module itself.
329
- if (!!!currentPackageConfig?.spellcraft_module_default_name) {
330
-
331
- const packagesDirPath = path.join(this.currentPackagePath, 'spellcraft_modules');
332
- if (!fs.existsSync(packagesDirPath)) {
333
- fs.mkdirSync(packagesDirPath, { recursive: true });
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
- const packagesFilePath = path.join(packagesDirPath, 'packages.json');
337
- let packages = {};
338
- if (fs.existsSync(packagesFilePath)) {
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
- }
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
- } else {
359
- console.log(`[*] Module installed, but not linked because the current project is also a module.`);
360
- console.log(`---> You can use the module's JS native functions, or import its JSonnet modules.`);
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); // Set active path for relative 'path()' calls
303
+ this.activePath = path.dirname(absoluteFilePath);
371
304
 
372
- this.magicContent.modules.push(this.loadedModules.flatMap(e => {
373
- return `\t${e}:: import '${e}.libsonnet'`;
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
- delete this.magicContent.modules;
382
-
383
- Object.keys(this.magicContent).forEach(e => {
384
- fs.appendFileSync(path.join(this.modulePath, `${e}.libsonnet`), ` + {\n${this.magicContent[e].join(",\n")}\n}`, 'utf-8');
385
- console.log(`[+] Registered native functions [${this.registeredFunctions[e].join(', ').cyan}] to modules.${e.green} `);
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
- console.log(`[+] Evaluating Jsonnet file ${path.basename(absoluteFilePath).green}`);
389
- this.lastRender = JSON.parse(await this.jsonnet.evaluateFile(absoluteFilePath));
319
+ async renderString(snippet) {
390
320
 
391
- if (this.cleanModulesAfterRender) {
392
- this.cleanModulePath();
321
+ this.activePath = process.cwd();
393
322
 
394
- fs.rmdirSync(this.modulePath);
395
- } else {
396
- console.log(`[*] Leaving ${this.spellcraftModuleRelativePath} in place.`.magenta);
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
- toString() {
403
- return this.lastRender ?? null;
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' || Object.keys(filesToWrite).length === 0) {
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
- try {
413
- if (!fs.existsSync(this.renderPath)) {
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
- console.log(`[+] Cleaning render path: ${this.renderPath}`);
422
- try {
345
+ // ... (Cleaning logic remains the same)
346
+ try {
423
347
  Object.keys(this.fileTypeHandlers).forEach(regexPattern => {
424
- const regex = new RegExp(regexPattern, "i"); // Case-insensitive match
425
- fs.readdirSync(this.renderPath)
426
- .filter(f => regex.test(f))
427
- .forEach(f => {
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
- try {
445
- for (const filename in filesToWrite) {
446
- if (Object.prototype.hasOwnProperty.call(filesToWrite, filename)) {
447
- const outputFilePath = path.join(this.renderPath, filename);
448
- const fileContent = filesToWrite[filename];
449
-
450
- // Find the appropriate handler or use default
451
- const [, handlerFn] = Object.entries(this.fileTypeHandlers)
452
- .find(([pattern]) => new RegExp(pattern).test(filename)) || [null, defaultFileHandler];
453
-
454
- try {
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 sonnetry = new SpellFrame({
6
+ const spell = new SpellFrame({
7
7
  renderPath: './render',
8
8
  cleanBeforeRender: true
9
9
  });
10
10
 
11
11
  (async () => {
12
12
 
13
- testBootstrap = sonnetry.render(`local spellcraft = import 'spellcraft'; { test: spellcraft.path() }`);
13
+ testBootstrap = await spell.renderString(`local spellcraft = import 'spellcraft'; { test: spellcraft.path() }`);
14
14
  console.log(testBootstrap);
15
15
 
16
16
  })();