@c6fc/spellcraft 0.1.3 → 0.1.4

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.
Files changed (2) hide show
  1. package/package.json +7 -10
  2. package/src/index.js +95 -2
package/package.json CHANGED
@@ -3,11 +3,11 @@
3
3
  "@colors/colors": "^1.6.0",
4
4
  "@hanazuki/node-jsonnet": "^0.4.2",
5
5
  "js-yaml": "^4.1.0",
6
- "yargs": "^17.2.1"
6
+ "yargs": "^18.0.0"
7
7
  },
8
8
  "name": "@c6fc/spellcraft",
9
9
  "description": "Extensible JSonnet CLI platform",
10
- "version": "0.1.3",
10
+ "version": "0.1.4",
11
11
  "main": "src/index.js",
12
12
  "directories": {
13
13
  "lib": "lib"
@@ -16,23 +16,20 @@
16
16
  "spellcraft": "bin/spellcraft.js"
17
17
  },
18
18
  "scripts": {
19
- "test": "node src/test.js",
20
- "doc": "node src/doc-generator.js"
19
+ "test": "node src/test.js"
21
20
  },
22
21
  "repository": {
23
22
  "type": "git",
24
23
  "url": "git+https://github.com/c6fc/spellcraft.git"
25
24
  },
26
25
  "keywords": [
27
- "jsonnet"
26
+ "jsonnet",
27
+ "spellcraft"
28
28
  ],
29
29
  "author": "Brad Woodward (brad@bradwoodward.io)",
30
30
  "license": "MIT",
31
31
  "bugs": {
32
32
  "url": "https://github.com/c6fc/spellcraft/issues"
33
33
  },
34
- "homepage": "https://github.com/c6fc/spellcraft#readme",
35
- "devDependencies": {
36
- "clean-jsdoc-theme": "^4.3.0"
37
- }
38
- }
34
+ "homepage": "https://github.com/c6fc/spellcraft#readme"
35
+ }
package/src/index.js CHANGED
@@ -60,6 +60,9 @@ exports.SpellFrame = class SpellFrame {
60
60
  this.functionContext = {};
61
61
  this.lastRender = null;
62
62
  this.activePath = null;
63
+ this.visitedPlugins = new Set();
64
+ this.loadedPlugins = new Map();
65
+ this.isInitialized = false;
63
66
 
64
67
  this.jsonnet = new Jsonnet()
65
68
  .addJpath(path.join(__dirname, '../lib'))
@@ -73,6 +76,8 @@ exports.SpellFrame = class SpellFrame {
73
76
 
74
77
  // REFACTOR: Automatically find and register plugins from package.json
75
78
  this.loadPluginsFromDependencies();
79
+ this.loadPluginsRecursively(baseDir);
80
+ this.validatePluginRequirements();
76
81
 
77
82
  // 2. Load Local Magic Modules (Rapid Prototyping Mode)
78
83
  this.loadLocalMagicModules();
@@ -117,20 +122,27 @@ exports.SpellFrame = class SpellFrame {
117
122
  this.addFileTypeHandler(pattern, handler);
118
123
  });
119
124
  }
125
+
120
126
  if (metadata.cliExtensions) {
121
127
  this.cliExtensions.push(...(Array.isArray(metadata.cliExtensions) ? metadata.cliExtensions : [metadata.cliExtensions]));
122
128
  }
123
- if (metadata.initFn) {
124
- this.initFn.push(...(Array.isArray(metadata.initFn) ? metadata.initFn : [metadata.initFn]));
129
+
130
+ if (metadata.init) {
131
+ this.initFn.push(...(Array.isArray(metadata.init) ? metadata.init : [metadata.init]));
125
132
  }
133
+
126
134
  Object.assign(this.functionContext, metadata.functionContext || {});
127
135
  return this;
128
136
  }
129
137
 
130
138
  async init() {
139
+ if (this.isInitialized) return;
140
+
131
141
  for (const step of this.initFn) {
132
142
  await step.call();
133
143
  }
144
+
145
+ this.isInitialized = true;
134
146
  }
135
147
 
136
148
  loadPluginsFromDependencies() {
@@ -244,6 +256,10 @@ exports.SpellFrame = class SpellFrame {
244
256
  loadPlugin(packageName, jsMainPath) {
245
257
  if (!jsMainPath || !fs.existsSync(jsMainPath)) return;
246
258
 
259
+ if (this.loadedPlugins.has(packageName)) {
260
+ return;
261
+ }
262
+
247
263
  let moduleExports;
248
264
  try {
249
265
  moduleExports = require(jsMainPath);
@@ -254,6 +270,11 @@ exports.SpellFrame = class SpellFrame {
254
270
 
255
271
  if (moduleExports._spellcraft_metadata) {
256
272
  this.extendWithModuleMetadata(moduleExports._spellcraft_metadata);
273
+
274
+ this.loadedPlugins.set(packageName, {
275
+ name: packageName,
276
+ requires: moduleExports._spellcraft_metadata.requires || []
277
+ });
257
278
  }
258
279
 
259
280
  Object.keys(moduleExports).forEach(key => {
@@ -279,7 +300,63 @@ exports.SpellFrame = class SpellFrame {
279
300
  });
280
301
  }
281
302
 
303
+ loadPluginsRecursively(currentDir) {
304
+ const packageJsonPath = path.join(currentDir, 'package.json');
305
+
306
+ // If we've already scanned this specific directory, stop (Circular Dep protection)
307
+ if (this.visitedPlugins.has(packageJsonPath)) return;
308
+ this.visitedPlugins.add(packageJsonPath);
309
+
310
+ if (!fs.existsSync(packageJsonPath)) return;
311
+
312
+ let pkg;
313
+ try {
314
+ pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
315
+ } catch (e) { return; }
316
+
317
+ // Combine dependencies (devDeps are usually only relevant at the root,
318
+ // but we scan both for completeness at the root level).
319
+ // For sub-dependencies, standard 'dependencies' is usually what matters.
320
+ const deps = { ...pkg.dependencies, ...(currentDir === baseDir ? pkg.devDependencies : {}) };
321
+
322
+ // Create a resolver anchored to the CURRENT directory.
323
+ // This is crucial: it tells Node "Find dependencies relative to THIS module",
324
+ // not relative to the root project.
325
+ const localResolver = require('module').createRequire(packageJsonPath);
326
+
327
+ Object.keys(deps).forEach(depName => {
328
+ try {
329
+ // 1. Resolve where this dependency actually lives on disk
330
+ const depManifestPath = localResolver.resolve(`${depName}/package.json`);
331
+ const depDir = path.dirname(depManifestPath);
332
+
333
+ // 2. Load its package.json
334
+ const depPkg = require(depManifestPath);
335
+
336
+ // 3. Check if it is a SpellCraft module
337
+ if (depPkg.spellcraft) {
338
+
339
+ // A. Load the Plugin Logic
340
+ const jsMainPath = path.join(depDir, depPkg.main || 'index.js');
341
+ this.loadPlugin(depPkg.name, jsMainPath);
342
+
343
+ // B. Recurse!
344
+ // Now scan *this* dependency's dependencies
345
+ this.loadPluginsRecursively(depDir);
346
+ }
347
+ } catch (e) {
348
+ // Dependency might be optional or failed to resolve; skip gracefully
349
+ // console.warn(`Debug: Skipped ${depName} from ${currentDir}: ${e.message}`);
350
+ }
351
+ });
352
+ }
353
+
282
354
  async render(file) {
355
+
356
+ if (!this.isInitialized) {
357
+ await this.init();
358
+ }
359
+
283
360
  const absoluteFilePath = path.resolve(file);
284
361
  if (!fs.existsSync(absoluteFilePath)) {
285
362
  throw new Error(`SpellCraft Render Error: Input file ${absoluteFilePath} does not exist.`);
@@ -313,6 +390,22 @@ exports.SpellFrame = class SpellFrame {
313
390
 
314
391
  return this.lastRender;
315
392
  }
393
+
394
+ validatePluginRequirements() {
395
+ for (const [pluginName, data] of this.loadedPlugins.entries()) {
396
+ if (!data.requires || data.requires.length === 0) continue;
397
+
398
+ data.requires.forEach(req => {
399
+ if (!this.loadedPlugins.has(req)) {
400
+ throw new Error(
401
+ `[SpellCraft Dependency Error] The module '${pluginName}' requires '${req}', ` +
402
+ `but '${req}' was not found or failed to load. \n` +
403
+ ` -> Try running: npm install --save ${req}`
404
+ );
405
+ }
406
+ });
407
+ }
408
+ }
316
409
 
317
410
  write(filesToWrite = this.lastRender) {
318
411
  if (!filesToWrite || typeof filesToWrite !== 'object') return this;