@c6fc/spellcraft 0.1.1 → 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 () => {
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
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "dependencies": {
3
+ "@colors/colors": "^1.6.0",
3
4
  "@hanazuki/node-jsonnet": "^0.4.2",
4
5
  "js-yaml": "^4.1.0",
5
6
  "yargs": "^17.2.1"
6
7
  },
7
8
  "name": "@c6fc/spellcraft",
8
9
  "description": "Extensible JSonnet CLI platform",
9
- "version": "0.1.1",
10
+ "version": "0.1.2",
10
11
  "main": "src/index.js",
11
12
  "directories": {
12
13
  "lib": "lib"
package/src/index.js CHANGED
@@ -64,7 +64,8 @@ exports.SpellFrame = class SpellFrame {
64
64
  this.jsonnet = new Jsonnet()
65
65
  .addJpath(path.join(__dirname, '../lib'))
66
66
  // REFACTOR: Look in the local project's node_modules for explicit imports
67
- .addJpath(path.join(baseDir, 'node_modules'));
67
+ .addJpath(path.join(baseDir, 'node_modules'))
68
+ .addJpath(path.join(baseDir, '.spellcraft'));
68
69
 
69
70
  // Built-in native functions
70
71
  this.addNativeFunction("envvar", (name) => process.env[name] || false, "name");
@@ -72,6 +73,9 @@ exports.SpellFrame = class SpellFrame {
72
73
 
73
74
  // REFACTOR: Automatically find and register plugins from package.json
74
75
  this.loadPluginsFromDependencies();
76
+
77
+ // 2. Load Local Magic Modules (Rapid Prototyping Mode)
78
+ this.loadLocalMagicModules();
75
79
  }
76
80
 
77
81
  _generateCacheKey(functionName, args) {
@@ -144,23 +148,109 @@ exports.SpellFrame = class SpellFrame {
144
148
  } catch (e) { return; }
145
149
 
146
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);
147
154
 
148
155
  Object.keys(deps).forEach(depName => {
149
156
  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`);
153
-
154
- // Only load if it marks itself as a spellcraft module
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
155
165
  if (depPkg.spellcraft || depPkg.keywords?.includes("spellcraft-module")) {
156
- this.loadPlugin(depName, depPkg.main ? path.join(depPath, depPkg.main) : null);
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);
157
170
  }
158
171
  } catch (e) {
159
172
  // Dependency might not be installed or resolvable, skip quietly
173
+ console.warn(`Debug: Could not load potential plugin ${depName}: ${e.message}`);
160
174
  }
161
175
  });
162
176
  }
163
177
 
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;
192
+ }
193
+
194
+ // Ensure hidden directory exists
195
+ if (!fs.existsSync(generatedDir)) fs.mkdirSync(generatedDir, { recursive: true });
196
+
197
+ const jsFiles = fs.readdirSync(localModulesDir).filter(f => f.endsWith('.js'));
198
+
199
+ let jsonnetContentParts = [];
200
+
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
+ }
214
+
215
+ let fileMethods = [];
216
+
217
+ Object.keys(moduleExports).forEach(funcName => {
218
+ if (funcName === '_spellcraft_metadata') return; // Skip metadata
219
+
220
+ let func, params;
221
+ // Handle [func, "arg1", "arg2"] syntax or plain function
222
+ if (Array.isArray(moduleExports[funcName])) {
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;
230
+ }
231
+
232
+ // Register with a unique local prefix
233
+ const uniqueId = `local_${moduleName}_${funcName}`;
234
+ this.addNativeFunction(uniqueId, func, ...params);
235
+
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
+ });
241
+
242
+ console.log(`[+] Loaded [${Object.keys(moduleExports).join(", ")}] from [${file}].`);
243
+
244
+ if (fileMethods.length > 0) {
245
+ jsonnetContentParts.push(` ${moduleName}: {\n${fileMethods.join(",\n")}\n }`);
246
+ }
247
+ });
248
+
249
+ // Generate the file
250
+ const finalContent = "{\n" + jsonnetContentParts.join(",\n") + "\n}";
251
+ fs.writeFileSync(aggregateFile, finalContent, 'utf-8');
252
+ }
253
+
164
254
  /**
165
255
  * REFACTOR: Loads a specific plugin JS file.
166
256
  * Namespaces native functions using the package name to prevent collisions.