@c6fc/spellcraft 0.1.2 → 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.
- package/README.md +82 -105
- package/bin/spellcraft.js +15 -45
- package/lib/spellcraft +0 -2
- package/package.json +7 -11
- package/src/doc-generator.js +208 -0
- package/src/index.js +95 -22
- package/jsdoc.json +0 -32
package/README.md
CHANGED
|
@@ -2,192 +2,169 @@
|
|
|
2
2
|
|
|
3
3
|
**The Sorcerer's Toolkit for Unified Configuration Management.**
|
|
4
4
|
|
|
5
|
-
SpellCraft is a
|
|
5
|
+
SpellCraft is a plugin framework for [Jsonnet](https://jsonnet.org/) that bridges the gap between declarative configuration and the Node.js ecosystem. It allows you to import NPM packages directly into your Jsonnet logic, execute native JavaScript functions during configuration generation, and manage complex infrastructure-as-code requirements from a single, gnostic workflow.
|
|
6
6
|
|
|
7
|
-
SpellCraft provides a single, unified source of truth,
|
|
7
|
+
SpellCraft provides a single, unified source of truth, letting you orchestrate any tool that needs machine-readable configurations (like Terraform, Packer, Kubernetes, or Ansible) from one place.
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/@c6fc/spellcraft)
|
|
10
|
-
[](https://github.com/
|
|
10
|
+
[](https://github.com/c6fc/spellcraft/blob/main/LICENSE)
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
## The SpellCraft Philosophy
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
2. **Seamless Extensibility (Node.js):** Sometimes, declarative logic isn't enough. SpellCraft allows you to "escape" the confines of Jsonnet by writing custom logic in Node.js. Need to fetch a secret from a vault, call a third-party API, or perform complex data manipulation? Simply write a JavaScript function and expose it directly to your Jsonnet code as a `std.native()` function.
|
|
21
|
-
|
|
22
|
-
3. **Robust Modularity (NPM):** SpellCraft's module system is built on the battle-tested foundation of NPM. This means you can version, share, and manage your infrastructure modules just like any other software dependency. Leverage public or private NPM registries to build a reusable, maintainable, and collaborative infrastructure codebase.
|
|
16
|
+
1. **Declarative Power (Jsonnet):** Configurations are written in Jsonnet. Variables, functions, and inheritance allow you to define components once and reuse them everywhere.
|
|
17
|
+
2. **Native Node.js Resolution:** No custom registries. No hidden magic. SpellCraft modules are just NPM packages. If you can `npm install` it, SpellCraft can load it.
|
|
18
|
+
3. **Scoped Extensibility:** Native JavaScript functions are automatically namespaced based on their package name, ensuring that dependencies never clash, even if multiple modules use different versions of the same library.
|
|
23
19
|
|
|
24
20
|
## Quick Start
|
|
25
21
|
|
|
26
|
-
Get up and running with SpellCraft in minutes.
|
|
27
|
-
|
|
28
22
|
### 1. Installation
|
|
29
23
|
|
|
30
|
-
Install the
|
|
24
|
+
Install the CLI and core library.
|
|
31
25
|
|
|
32
26
|
```sh
|
|
33
27
|
npm install --save @c6fc/spellcraft
|
|
34
28
|
```
|
|
35
29
|
|
|
36
|
-
### 2.
|
|
30
|
+
### 2. Install a Plugin
|
|
37
31
|
|
|
38
|
-
|
|
32
|
+
Install a SpellCraft-compatible plugin using standard NPM.
|
|
39
33
|
|
|
40
34
|
```sh
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# Expected Output:
|
|
44
|
-
# [*] Attempting to install @c6fc/spellcraft-aws-auth...
|
|
45
|
-
# [+] Successfully installed @c6fc/spellcraft-aws-auth.
|
|
46
|
-
# [+] Linked @c6fc/spellcraft-aws-auth as SpellCraft module 'awsauth'
|
|
35
|
+
npm install --save @c6fc/spellcraft-aws-auth
|
|
47
36
|
```
|
|
48
|
-
This makes the `@c6fc/spellcraft-aws-auth` package available in your Jsonnet files under the name `awsauth`.
|
|
49
37
|
|
|
50
|
-
### 3.
|
|
38
|
+
### 3. Write Your Spell
|
|
51
39
|
|
|
52
|
-
|
|
40
|
+
Create a `manifest.jsonnet` file. Unlike previous versions of SpellCraft, you import modules explicitly using standard Node resolution.
|
|
53
41
|
|
|
54
|
-
Create a file named `manifest.jsonnet`:
|
|
55
42
|
```jsonnet
|
|
56
|
-
//
|
|
57
|
-
local
|
|
43
|
+
// Import the library directly from node_modules
|
|
44
|
+
local aws = import '@c6fc/spellcraft-aws-auth/module.libsonnet';
|
|
58
45
|
|
|
59
46
|
{
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
'aws-identity.json': modules.awsauth.getCallerIdentity(),
|
|
47
|
+
// Use functions provided by the module
|
|
48
|
+
'aws-identity.json': aws.getCallerIdentity(),
|
|
63
49
|
|
|
64
|
-
// We can also create YAML files. SpellCraft has built-in handlers for common types.
|
|
65
50
|
'config.yaml': {
|
|
66
51
|
apiVersion: 'v1',
|
|
67
52
|
kind: 'ConfigMap',
|
|
68
|
-
metadata: {
|
|
69
|
-
name: 'my-app-config',
|
|
70
|
-
},
|
|
53
|
+
metadata: { name: 'my-app-config' },
|
|
71
54
|
data: {
|
|
55
|
+
// Use built-in native functions
|
|
72
56
|
region: std.native('envvar')('AWS_REGION') || 'us-east-1',
|
|
73
|
-
|
|
74
|
-
callerArn: modules.awsauth.getCallerIdentity().Arn,
|
|
57
|
+
callerArn: aws.getCallerIdentity().Arn,
|
|
75
58
|
},
|
|
76
59
|
},
|
|
77
60
|
}
|
|
78
61
|
```
|
|
79
62
|
|
|
80
|
-
### 4. Generate
|
|
63
|
+
### 4. Generate Artifacts
|
|
81
64
|
|
|
82
|
-
|
|
65
|
+
Run the generator. SpellCraft automatically detects installed plugins in your `package.json`, registers their native functions, and renders your configuration.
|
|
83
66
|
|
|
84
67
|
```sh
|
|
85
68
|
npx spellcraft generate manifest.jsonnet
|
|
86
69
|
|
|
87
70
|
# Expected Output:
|
|
88
|
-
# [+]
|
|
89
|
-
# ...
|
|
90
|
-
# [+] Registered native functions [getCallerIdentity, ...] to modules.awsauth
|
|
91
|
-
# [+] Evaluating Jsonnet file manifest.jsonnet
|
|
71
|
+
# [+] Evaluating Jsonnet file: .../manifest.jsonnet
|
|
92
72
|
# [+] Writing files to: render
|
|
93
73
|
# -> aws-identity.json
|
|
94
74
|
# -> config.yaml
|
|
95
75
|
# [+] Generation complete.
|
|
96
76
|
```
|
|
97
77
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
```
|
|
101
|
-
.
|
|
102
|
-
├── manifest.jsonnet
|
|
103
|
-
├── node_modules/
|
|
104
|
-
├── package.json
|
|
105
|
-
└── render/
|
|
106
|
-
├── aws-identity.json
|
|
107
|
-
└── config.yaml
|
|
108
|
-
```
|
|
78
|
+
---
|
|
109
79
|
|
|
110
|
-
##
|
|
80
|
+
## Rapid Prototyping (Local Modules)
|
|
111
81
|
|
|
112
|
-
|
|
82
|
+
Sometimes you need a custom function just for your current project, and you don't want to publish a full NPM package. SpellCraft provides a **Local Magic** folder for this.
|
|
113
83
|
|
|
114
|
-
|
|
84
|
+
1. Create a folder named `spellcraft_modules` in your project root.
|
|
85
|
+
2. Create a JavaScript file, e.g., `spellcraft_modules/utils.js`:
|
|
115
86
|
|
|
116
|
-
|
|
117
|
-
|
|
87
|
+
```javascript
|
|
88
|
+
// spellcraft_modules/utils.js
|
|
89
|
+
exports.shout = (text) => text.toUpperCase() + "!!!";
|
|
90
|
+
exports.add = (a, b) => a + b;
|
|
118
91
|
|
|
119
|
-
|
|
92
|
+
// Use standard functions to access 'this', which is extended by plugins:
|
|
93
|
+
exports.know_thyself = function() {
|
|
94
|
+
this.aws.getCallerIdentity()
|
|
95
|
+
}
|
|
96
|
+
```
|
|
120
97
|
|
|
121
|
-
|
|
98
|
+
3. In your Jsonnet file, import `modules` to access your exported functions:
|
|
122
99
|
|
|
123
|
-
|
|
100
|
+
```jsonnet
|
|
101
|
+
// Import the automatically generated local module aggregator
|
|
102
|
+
local modules = import 'modules';
|
|
124
103
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
104
|
+
{
|
|
105
|
+
'test.json': {
|
|
106
|
+
// Access your local JS functions here
|
|
107
|
+
// Our file was named 'utils.js', so the exported
|
|
108
|
+
// functions are accessed via 'modules.utils'.
|
|
109
|
+
message: modules.utils.shout("hello world"),
|
|
110
|
+
sum: modules.utils.add(10, 5)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
134
113
|
```
|
|
135
114
|
|
|
136
|
-
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## The SpellCraft CLI
|
|
118
|
+
|
|
119
|
+
The CLI is automatically extended by installed modules.
|
|
137
120
|
|
|
138
|
-
|
|
121
|
+
* `spellcraft generate <filename>`: Renders a Jsonnet file to the `render/` directory.
|
|
122
|
+
* `spellcraft --help`: Lists all available commands, including those added by plugins (e.g., `spellcraft aws-identity`).
|
|
139
123
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Programmatic API
|
|
127
|
+
|
|
128
|
+
You can embed SpellCraft into your own Node.js scripts for advanced automation.
|
|
145
129
|
|
|
146
130
|
```javascript
|
|
147
|
-
// my-automation-script.js
|
|
148
131
|
const { SpellFrame } = require('@c6fc/spellcraft');
|
|
149
132
|
const path = require('path');
|
|
150
133
|
|
|
151
|
-
|
|
152
|
-
// Options allow you to customize output paths, cleaning behavior, etc.
|
|
153
|
-
const frame = new SpellFrame({
|
|
154
|
-
renderPath: "dist", // Output to 'dist/' instead of 'render/'
|
|
155
|
-
cleanBeforeRender: true,
|
|
156
|
-
});
|
|
134
|
+
const frame = new SpellFrame();
|
|
157
135
|
|
|
158
136
|
(async () => {
|
|
159
|
-
|
|
160
|
-
// 2. Initialize modules before rendering.
|
|
137
|
+
// 1. Initialize: Scans package.json for plugins and loads them
|
|
161
138
|
await frame.init();
|
|
162
139
|
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
console.log('Rendered Manifest:', JSON.stringify(manifest, null, 2));
|
|
140
|
+
// 2. Render: Evaluates the Jsonnet
|
|
141
|
+
// Note: The result is a pure JS object
|
|
142
|
+
const result = await frame.render(path.resolve('./manifest.jsonnet'));
|
|
143
|
+
console.log(result);
|
|
168
144
|
|
|
169
|
-
//
|
|
170
|
-
// the most recent 'render'.
|
|
145
|
+
// 3. Write: Outputs files to disk (applying JSON/YAML transformations)
|
|
171
146
|
frame.write();
|
|
147
|
+
})();
|
|
148
|
+
```
|
|
172
149
|
|
|
173
|
-
|
|
150
|
+
## Creating Modules
|
|
174
151
|
|
|
175
|
-
|
|
152
|
+
A SpellCraft module is simply an NPM package with specific metadata. You can get a head-start with:
|
|
153
|
+
```bash
|
|
154
|
+
npm init spellcraft-module @your_org/your_module
|
|
176
155
|
```
|
|
177
156
|
|
|
178
|
-
|
|
157
|
+
Learn more at [**create-spellcraft-module**](https://www.npmjs.com/package/create-spellcraft-module)
|
|
179
158
|
|
|
180
|
-
|
|
159
|
+
## Community Modules
|
|
181
160
|
|
|
182
|
-
|Package|Description|
|
|
161
|
+
| Package | Description |
|
|
183
162
|
|---|---|
|
|
184
|
-
|[**@c6fc/spellcraft-aws-auth**](https://www.npmjs.com/package/@c6fc/spellcraft-aws-auth)|
|
|
185
|
-
|[**@c6fc/spellcraft-terraform**](https://www.npmjs.com/package/@c6fc/spellcraft-terraform)|
|
|
186
|
-
|[**@c6fc/spellcraft-packer**](https://www.npmjs.com/package/@c6fc/spellcraft-packer)|Brings Packer into SpellCraft CLI and SpellFrames, allowing you to run builds against the results of your Spells.|
|
|
187
|
-
|[**@c6fc/spellcraft-aws-terraform**](https://www.npmjs.com/package/@c6fc/spellcraft-aws-terraform)|Contains shortcuts for common use-cases when using Terraform to deploy configurations to AWS. Including backend bootstrapping, artifact repositories, and remote state access. It also demonstrates how modules can build on the capabilities of other modules.|
|
|
188
|
-
|[**@c6fc/spellcraft-aws-s3**](https://www.npmjs.com/package/@c6fc/spellcraft-aws-s3)|A simple but powerful module for creating AWS S3 buckets. It uses secure defaults, exposes common use-cases as optional 'types', and grants extensive control with minimal code.|
|
|
163
|
+
| [**@c6fc/spellcraft-aws-auth**](https://www.npmjs.com/package/@c6fc/spellcraft-aws-auth) | AWS SDK authentication and API calls directly from Jsonnet. |
|
|
164
|
+
| [**@c6fc/spellcraft-terraform**](https://www.npmjs.com/package/@c6fc/spellcraft-terraform) | Terraform integration and state management. |
|
|
189
165
|
|
|
166
|
+
---
|
|
190
167
|
|
|
191
|
-
##
|
|
168
|
+
## License
|
|
192
169
|
|
|
193
|
-
|
|
170
|
+
MIT © [Brad Woodward](https://github.com/c6fc)
|
package/bin/spellcraft.js
CHANGED
|
@@ -1,46 +1,16 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* @fileOverview SpellCraft CLI tool.
|
|
5
|
-
* This script provides a command-line interface for interacting with the SpellFrame
|
|
6
|
-
* rendering engine. It allows users to generate configurations from Jsonnet files
|
|
7
|
-
* and manage SpellCraft modules.
|
|
8
|
-
* @module spellcraft-cli
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
3
|
'use strict';
|
|
12
4
|
|
|
13
5
|
const yargs = require('yargs');
|
|
14
6
|
const colors = require('@colors/colors');
|
|
15
7
|
const { hideBin } = require('yargs/helpers');
|
|
16
8
|
const { SpellFrame } = require('../src/index.js');
|
|
9
|
+
const DocGenerator = require('../src/doc-generator');
|
|
17
10
|
|
|
18
11
|
const spellframe = new SpellFrame();
|
|
19
12
|
|
|
20
|
-
// --- JSDoc Blocks for CLI Commands ---
|
|
21
|
-
// These blocks define the commands for JSDoc.
|
|
22
|
-
// They are not directly attached to yargs code but describe the CLI's public interface.
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Generates files from a Jsonnet configuration.
|
|
26
|
-
* This command processes a specified Jsonnet configuration file using the SpellFrame engine,
|
|
27
|
-
* renders the output, and writes the resulting files to the configured directory.
|
|
28
|
-
*
|
|
29
|
-
* **Usage:** `spellcraft generate <filename>`
|
|
30
|
-
*
|
|
31
|
-
* @function generate
|
|
32
|
-
* @name module:spellcraft-cli.generate
|
|
33
|
-
* @param {object} argv - The arguments object provided by yargs.
|
|
34
|
-
* @param {string} argv.filename The path to the Jsonnet configuration file to consume. (Required)
|
|
35
|
-
*
|
|
36
|
-
* @example
|
|
37
|
-
* spellcraft generate ./myconfig.jsonnet
|
|
38
|
-
*/
|
|
39
|
-
|
|
40
|
-
// --- End of JSDoc Blocks for CLI Commands ---
|
|
41
|
-
|
|
42
13
|
(async () => {
|
|
43
|
-
// No JSDoc for setupCli as it's an internal helper
|
|
44
14
|
function setupCli(sfInstance) {
|
|
45
15
|
let cli = yargs(hideBin(process.argv))
|
|
46
16
|
.usage("Syntax: $0 <command> [options]")
|
|
@@ -52,6 +22,12 @@ const spellframe = new SpellFrame();
|
|
|
52
22
|
console.log("[~] That's too arcane. (Unrecognized command)");
|
|
53
23
|
})
|
|
54
24
|
|
|
25
|
+
.command("doc", "Generates Markdown documentation for the current module and updates README.md", () => {},
|
|
26
|
+
(argv) => {
|
|
27
|
+
const generator = new DocGenerator(process.cwd());
|
|
28
|
+
generator.generate();
|
|
29
|
+
})
|
|
30
|
+
|
|
55
31
|
.command("generate <filename>", "Generates files from a configuration", (yargsInstance) => {
|
|
56
32
|
return yargsInstance.positional('filename', {
|
|
57
33
|
describe: 'Jsonnet configuration file to consume',
|
|
@@ -63,23 +39,17 @@ const spellframe = new SpellFrame();
|
|
|
63
39
|
description: 'Leave temporary modules intact after rendering'
|
|
64
40
|
});
|
|
65
41
|
},
|
|
66
|
-
async (argv) => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
42
|
+
async (argv) => {
|
|
43
|
+
if (argv['s']) {
|
|
44
|
+
sfInstance.cleanModulesAfterRender = false;
|
|
45
|
+
}
|
|
71
46
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
/*} catch (error) {
|
|
77
|
-
console.error(`[!] Error during generation: ${error.message.red}`);
|
|
78
|
-
process.exit(1);
|
|
79
|
-
}*/
|
|
47
|
+
await sfInstance.init();
|
|
48
|
+
await sfInstance.render(argv.filename);
|
|
49
|
+
await sfInstance.write();
|
|
50
|
+
console.log("[+] Generation complete.");
|
|
80
51
|
})
|
|
81
52
|
|
|
82
|
-
// No JSDoc for CLI extensions loop if considered internal detail
|
|
83
53
|
if (sfInstance.cliExtensions && sfInstance.cliExtensions.length > 0) {
|
|
84
54
|
sfInstance.cliExtensions.forEach((extensionFn) => {
|
|
85
55
|
if (typeof extensionFn === 'function') {
|
package/lib/spellcraft
CHANGED
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": "^
|
|
6
|
+
"yargs": "^18.0.0"
|
|
7
7
|
},
|
|
8
8
|
"name": "@c6fc/spellcraft",
|
|
9
9
|
"description": "Extensible JSonnet CLI platform",
|
|
10
|
-
"version": "0.1.
|
|
10
|
+
"version": "0.1.4",
|
|
11
11
|
"main": "src/index.js",
|
|
12
12
|
"directories": {
|
|
13
13
|
"lib": "lib"
|
|
@@ -16,24 +16,20 @@
|
|
|
16
16
|
"spellcraft": "bin/spellcraft.js"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
|
-
"test": "node src/test.js"
|
|
20
|
-
"doc": "jsdoc -c jsdoc.json --verbose"
|
|
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
|
-
|
|
36
|
-
"clean-jsdoc-theme": "^4.3.0",
|
|
37
|
-
"jsdoc": "^4.0.4"
|
|
38
|
-
}
|
|
39
|
-
}
|
|
34
|
+
"homepage": "https://github.com/c6fc/spellcraft#readme"
|
|
35
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
class DocGenerator {
|
|
5
|
+
constructor(baseDir) {
|
|
6
|
+
this.baseDir = baseDir;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
generate() {
|
|
10
|
+
const readmePath = path.join(this.baseDir, 'README.md');
|
|
11
|
+
if (!fs.existsSync(readmePath)) {
|
|
12
|
+
console.error("[!] No README.md found.");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let readmeContent = fs.readFileSync(readmePath, 'utf-8');
|
|
17
|
+
const apiDocs = this.parseJsonnetDocs();
|
|
18
|
+
const cliDocs = this.parseCliDocs();
|
|
19
|
+
|
|
20
|
+
readmeContent = this.replaceSection(readmeContent, 'API', apiDocs);
|
|
21
|
+
readmeContent = this.replaceSection(readmeContent, 'CLI', cliDocs);
|
|
22
|
+
|
|
23
|
+
fs.writeFileSync(readmePath, readmeContent);
|
|
24
|
+
console.log("[+] README.md updated with generated documentation.");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Helper to replace content between <!-- SPELLCRAFT_DOCS_XYZ_START --> tags
|
|
28
|
+
replaceSection(content, sectionName, newContent) {
|
|
29
|
+
const startTag = `<!-- SPELLCRAFT_DOCS_${sectionName}_START -->`;
|
|
30
|
+
const endTag = `<!-- SPELLCRAFT_DOCS_${sectionName}_END -->`;
|
|
31
|
+
const regex = new RegExp(`${startTag}[\\s\\S]*?${endTag}`, 'g');
|
|
32
|
+
|
|
33
|
+
if (!regex.test(content)) {
|
|
34
|
+
console.log(`[-] No tags exist for ${sectionName}. Skipping.`);
|
|
35
|
+
// Do nothing if the tags don't exist.
|
|
36
|
+
return content;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return content.replace(regex, `${startTag}\n${newContent}\n${endTag}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
parseJsonnetDocs() {
|
|
43
|
+
const libPath = path.join(this.baseDir, 'module.libsonnet');
|
|
44
|
+
if (!fs.existsSync(libPath)) return '';
|
|
45
|
+
|
|
46
|
+
const content = fs.readFileSync(libPath, 'utf-8');
|
|
47
|
+
// Regex to find /** comments */ followed by a function definition
|
|
48
|
+
// Captures: 1=Comment content, 2=FunctionName, 3=Args
|
|
49
|
+
const regex = /\/\*\*([\s\S]*?)\*\/\s*\n\s*([\w]+)\(([^)]*)\)/g;
|
|
50
|
+
|
|
51
|
+
let match;
|
|
52
|
+
let markdown = "## API Reference\n\n";
|
|
53
|
+
|
|
54
|
+
while ((match = regex.exec(content)) !== null) {
|
|
55
|
+
const rawCommentLines = match[1].split('\n').map(line =>
|
|
56
|
+
// Remove the " * " from the start of lines
|
|
57
|
+
line.replace(/^\s*\*\s?/, '')
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const funcName = match[2];
|
|
61
|
+
const args = match[3];
|
|
62
|
+
|
|
63
|
+
let description = [];
|
|
64
|
+
let params = [];
|
|
65
|
+
let examples = []; // Array of arrays (one per example block)
|
|
66
|
+
let currentExampleBlock = null;
|
|
67
|
+
let mode = 'description';
|
|
68
|
+
|
|
69
|
+
rawCommentLines.forEach(line => {
|
|
70
|
+
const trimmed = line.trim();
|
|
71
|
+
|
|
72
|
+
// 1. Detect new @example block
|
|
73
|
+
if (trimmed.startsWith('@example')) {
|
|
74
|
+
mode = 'example';
|
|
75
|
+
currentExampleBlock = []; // Start a new container
|
|
76
|
+
examples.push(currentExampleBlock);
|
|
77
|
+
return; // Skip the tag line itself
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 2. Detect Metadata tags (@param, @return)
|
|
81
|
+
// We handle these regardless of mode, assuming they aren't part of the code example
|
|
82
|
+
if (trimmed.startsWith('@param') || trimmed.startsWith('@return')) {
|
|
83
|
+
// Strip the @ and format as a list item
|
|
84
|
+
params.push(`- ${trimmed.substring(1)}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 3. Capture Content
|
|
89
|
+
if (mode === 'example') {
|
|
90
|
+
// Add line to the currently active example block
|
|
91
|
+
if (currentExampleBlock) {
|
|
92
|
+
currentExampleBlock.push(line);
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
// Add line to general description
|
|
96
|
+
description.push(line);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// --- Build Markdown Output ---
|
|
101
|
+
|
|
102
|
+
markdown += `### \`${funcName}(${args})\`\n\n`;
|
|
103
|
+
|
|
104
|
+
// 1. Description
|
|
105
|
+
if (description.length > 0) {
|
|
106
|
+
markdown += description.join('\n').trim() + "\n\n";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 2. Parameters / Returns
|
|
110
|
+
if (params.length > 0) {
|
|
111
|
+
markdown += params.join('\n') + "\n\n";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 3. Examples (Loop through the array)
|
|
115
|
+
if (examples.length > 0) {
|
|
116
|
+
markdown += "**Examples:**\n\n";
|
|
117
|
+
examples.forEach(exBlock => {
|
|
118
|
+
// Polish: Join lines and trim empty leading/trailing newlines
|
|
119
|
+
const code = exBlock.join('\n').trim();
|
|
120
|
+
if (code.length > 0) {
|
|
121
|
+
markdown += "```jsonnet\n";
|
|
122
|
+
markdown += code + "\n";
|
|
123
|
+
markdown += "```\n\n";
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
markdown += "---\n";
|
|
129
|
+
}
|
|
130
|
+
return markdown;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
parseCliDocs() {
|
|
134
|
+
const jsPath = path.join(this.baseDir, 'module.js');
|
|
135
|
+
if (!fs.existsSync(jsPath)) return '';
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
// Load the module (Bypass cache to ensure fresh read)
|
|
139
|
+
delete require.cache[require.resolve(jsPath)];
|
|
140
|
+
const moduleExports = require(jsPath);
|
|
141
|
+
const meta = moduleExports._spellcraft_metadata;
|
|
142
|
+
|
|
143
|
+
// Guard clauses
|
|
144
|
+
if (!meta || !meta.cliExtensions || typeof meta.cliExtensions !== 'function') {
|
|
145
|
+
return '';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const capturedCommands = [];
|
|
149
|
+
|
|
150
|
+
// Create a Proxy/Mock object to intercept yargs calls
|
|
151
|
+
const mockYargs = {
|
|
152
|
+
command: (command, description, ...args) => {
|
|
153
|
+
// Capture the essential info
|
|
154
|
+
capturedCommands.push({ command, description });
|
|
155
|
+
return mockYargs; // Return self to allow chaining .command().command()
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
// Stub out other common yargs methods so the script doesn't crash
|
|
159
|
+
// if the module uses .usage(), .option(), etc.
|
|
160
|
+
usage: () => mockYargs,
|
|
161
|
+
scriptName: () => mockYargs,
|
|
162
|
+
demandCommand: () => mockYargs,
|
|
163
|
+
recommendCommands: () => mockYargs,
|
|
164
|
+
strict: () => mockYargs,
|
|
165
|
+
showHelpOnFail: () => mockYargs,
|
|
166
|
+
help: () => mockYargs,
|
|
167
|
+
alias: () => mockYargs,
|
|
168
|
+
version: () => mockYargs,
|
|
169
|
+
epilogue: () => mockYargs,
|
|
170
|
+
option: () => mockYargs,
|
|
171
|
+
positional: () => mockYargs,
|
|
172
|
+
group: () => mockYargs,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Mock SpellFrame (The second argument passed to cliExtensions)
|
|
176
|
+
// We mock this just in case the extension tries to read properties from it immediately
|
|
177
|
+
const mockSpellFrame = {
|
|
178
|
+
init: async () => {},
|
|
179
|
+
render: async () => {},
|
|
180
|
+
write: () => {},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// Execute the function!
|
|
184
|
+
meta.cliExtensions(mockYargs, mockSpellFrame);
|
|
185
|
+
|
|
186
|
+
// Generate Markdown
|
|
187
|
+
if (capturedCommands.length === 0) return '';
|
|
188
|
+
|
|
189
|
+
let markdown = "## CLI Commands\n\n";
|
|
190
|
+
|
|
191
|
+
capturedCommands.forEach(c => {
|
|
192
|
+
// If description is explicitly false (hidden command), skip it
|
|
193
|
+
if (c.description === false) return;
|
|
194
|
+
|
|
195
|
+
markdown += `- **\`spellcraft ${c.command}\`**\n`;
|
|
196
|
+
markdown += ` ${c.description}\n`;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return markdown;
|
|
200
|
+
|
|
201
|
+
} catch (e) {
|
|
202
|
+
console.warn(`[!] Failed to parse CLI docs from module.js: ${e.message}`);
|
|
203
|
+
return '';
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = DocGenerator;
|
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,27 +122,29 @@ 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
|
-
|
|
124
|
-
|
|
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
|
-
/**
|
|
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
148
|
loadPluginsFromDependencies() {
|
|
142
149
|
const packageJsonPath = path.join(baseDir, 'package.json');
|
|
143
150
|
if (!fs.existsSync(packageJsonPath)) return;
|
|
@@ -175,11 +182,6 @@ exports.SpellFrame = class SpellFrame {
|
|
|
175
182
|
});
|
|
176
183
|
}
|
|
177
184
|
|
|
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
185
|
loadLocalMagicModules() {
|
|
184
186
|
const localModulesDir = path.join(baseDir, 'spellcraft_modules');
|
|
185
187
|
const generatedDir = path.join(baseDir, '.spellcraft');
|
|
@@ -251,14 +253,13 @@ exports.SpellFrame = class SpellFrame {
|
|
|
251
253
|
fs.writeFileSync(aggregateFile, finalContent, 'utf-8');
|
|
252
254
|
}
|
|
253
255
|
|
|
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
256
|
loadPlugin(packageName, jsMainPath) {
|
|
260
257
|
if (!jsMainPath || !fs.existsSync(jsMainPath)) return;
|
|
261
258
|
|
|
259
|
+
if (this.loadedPlugins.has(packageName)) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
262
263
|
let moduleExports;
|
|
263
264
|
try {
|
|
264
265
|
moduleExports = require(jsMainPath);
|
|
@@ -269,6 +270,11 @@ exports.SpellFrame = class SpellFrame {
|
|
|
269
270
|
|
|
270
271
|
if (moduleExports._spellcraft_metadata) {
|
|
271
272
|
this.extendWithModuleMetadata(moduleExports._spellcraft_metadata);
|
|
273
|
+
|
|
274
|
+
this.loadedPlugins.set(packageName, {
|
|
275
|
+
name: packageName,
|
|
276
|
+
requires: moduleExports._spellcraft_metadata.requires || []
|
|
277
|
+
});
|
|
272
278
|
}
|
|
273
279
|
|
|
274
280
|
Object.keys(moduleExports).forEach(key => {
|
|
@@ -294,7 +300,63 @@ exports.SpellFrame = class SpellFrame {
|
|
|
294
300
|
});
|
|
295
301
|
}
|
|
296
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
|
+
|
|
297
354
|
async render(file) {
|
|
355
|
+
|
|
356
|
+
if (!this.isInitialized) {
|
|
357
|
+
await this.init();
|
|
358
|
+
}
|
|
359
|
+
|
|
298
360
|
const absoluteFilePath = path.resolve(file);
|
|
299
361
|
if (!fs.existsSync(absoluteFilePath)) {
|
|
300
362
|
throw new Error(`SpellCraft Render Error: Input file ${absoluteFilePath} does not exist.`);
|
|
@@ -329,11 +391,22 @@ exports.SpellFrame = class SpellFrame {
|
|
|
329
391
|
return this.lastRender;
|
|
330
392
|
}
|
|
331
393
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
+
}
|
|
409
|
+
|
|
337
410
|
write(filesToWrite = this.lastRender) {
|
|
338
411
|
if (!filesToWrite || typeof filesToWrite !== 'object') return this;
|
|
339
412
|
|
package/jsdoc.json
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"tags": {
|
|
3
|
-
"allowUnknownTags": true
|
|
4
|
-
},
|
|
5
|
-
"plugins": ["plugins/markdown"],
|
|
6
|
-
"opts": {
|
|
7
|
-
"destination": "./docs"
|
|
8
|
-
},
|
|
9
|
-
"source": {
|
|
10
|
-
"include": [
|
|
11
|
-
"src/",
|
|
12
|
-
"bin/"
|
|
13
|
-
],
|
|
14
|
-
"includePattern": ".",
|
|
15
|
-
"excludePattern": "\\.bak$"
|
|
16
|
-
},
|
|
17
|
-
"opts": {
|
|
18
|
-
"destination": "./docs/",
|
|
19
|
-
"encoding": "utf8",
|
|
20
|
-
"private": true,
|
|
21
|
-
"recurse": false,
|
|
22
|
-
"readme": "README.md",
|
|
23
|
-
"template": "node_modules/clean-jsdoc-theme"
|
|
24
|
-
},
|
|
25
|
-
"markdown": {
|
|
26
|
-
"hardwrap": false,
|
|
27
|
-
"idInHeadings": true
|
|
28
|
-
},
|
|
29
|
-
"theme_opts": {
|
|
30
|
-
"default_theme": "dark"
|
|
31
|
-
}
|
|
32
|
-
}
|