@c6fc/spellcraft 0.1.1 → 0.1.3
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 -66
- package/lib/spellcraft +0 -2
- package/package.json +4 -4
- package/src/doc-generator.js +208 -0
- package/src/index.js +93 -23
- package/jsdoc.json +0 -33
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,67 +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
|
-
/**
|
|
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
|
-
// --- End of JSDoc Blocks for CLI Commands ---
|
|
62
|
-
|
|
63
13
|
(async () => {
|
|
64
|
-
// No JSDoc for setupCli as it's an internal helper
|
|
65
14
|
function setupCli(sfInstance) {
|
|
66
15
|
let cli = yargs(hideBin(process.argv))
|
|
67
16
|
.usage("Syntax: $0 <command> [options]")
|
|
@@ -73,6 +22,12 @@ const spellframe = new SpellFrame();
|
|
|
73
22
|
console.log("[~] That's too arcane. (Unrecognized command)");
|
|
74
23
|
})
|
|
75
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
|
+
|
|
76
31
|
.command("generate <filename>", "Generates files from a configuration", (yargsInstance) => {
|
|
77
32
|
return yargsInstance.positional('filename', {
|
|
78
33
|
describe: 'Jsonnet configuration file to consume',
|
|
@@ -84,23 +39,17 @@ const spellframe = new SpellFrame();
|
|
|
84
39
|
description: 'Leave temporary modules intact after rendering'
|
|
85
40
|
});
|
|
86
41
|
},
|
|
87
|
-
async (argv) => {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
42
|
+
async (argv) => {
|
|
43
|
+
if (argv['s']) {
|
|
44
|
+
sfInstance.cleanModulesAfterRender = false;
|
|
45
|
+
}
|
|
92
46
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
/*} catch (error) {
|
|
98
|
-
console.error(`[!] Error during generation: ${error.message.red}`);
|
|
99
|
-
process.exit(1);
|
|
100
|
-
}*/
|
|
47
|
+
await sfInstance.init();
|
|
48
|
+
await sfInstance.render(argv.filename);
|
|
49
|
+
await sfInstance.write();
|
|
50
|
+
console.log("[+] Generation complete.");
|
|
101
51
|
})
|
|
102
52
|
|
|
103
|
-
// No JSDoc for CLI extensions loop if considered internal detail
|
|
104
53
|
if (sfInstance.cliExtensions && sfInstance.cliExtensions.length > 0) {
|
|
105
54
|
sfInstance.cliExtensions.forEach((extensionFn) => {
|
|
106
55
|
if (typeof extensionFn === 'function') {
|
package/lib/spellcraft
CHANGED
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.
|
|
10
|
+
"version": "0.1.3",
|
|
10
11
|
"main": "src/index.js",
|
|
11
12
|
"directories": {
|
|
12
13
|
"lib": "lib"
|
|
@@ -16,7 +17,7 @@
|
|
|
16
17
|
},
|
|
17
18
|
"scripts": {
|
|
18
19
|
"test": "node src/test.js",
|
|
19
|
-
"doc": "
|
|
20
|
+
"doc": "node src/doc-generator.js"
|
|
20
21
|
},
|
|
21
22
|
"repository": {
|
|
22
23
|
"type": "git",
|
|
@@ -32,7 +33,6 @@
|
|
|
32
33
|
},
|
|
33
34
|
"homepage": "https://github.com/c6fc/spellcraft#readme",
|
|
34
35
|
"devDependencies": {
|
|
35
|
-
"clean-jsdoc-theme": "^4.3.0"
|
|
36
|
-
"jsdoc": "^4.0.4"
|
|
36
|
+
"clean-jsdoc-theme": "^4.3.0"
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -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
|
@@ -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) {
|
|
@@ -129,11 +133,6 @@ exports.SpellFrame = class SpellFrame {
|
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
|
|
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
136
|
loadPluginsFromDependencies() {
|
|
138
137
|
const packageJsonPath = path.join(baseDir, 'package.json');
|
|
139
138
|
if (!fs.existsSync(packageJsonPath)) return;
|
|
@@ -144,28 +143,104 @@ exports.SpellFrame = class SpellFrame {
|
|
|
144
143
|
} catch (e) { return; }
|
|
145
144
|
|
|
146
145
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
146
|
+
|
|
147
|
+
// Create a require function that operates as if it's inside the user's project
|
|
148
|
+
const userProjectRequire = require('module').createRequire(packageJsonPath);
|
|
147
149
|
|
|
148
150
|
Object.keys(deps).forEach(depName => {
|
|
149
151
|
try {
|
|
150
|
-
//
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
// 1. Find the path to the dependency's package.json using the USER'S context
|
|
153
|
+
const depPackageJsonPath = userProjectRequire.resolve(`${depName}/package.json`);
|
|
154
|
+
|
|
155
|
+
// 2. Load that package.json using the absolute path
|
|
156
|
+
const depPkg = require(depPackageJsonPath);
|
|
157
|
+
const depDir = path.dirname(depPackageJsonPath);
|
|
158
|
+
|
|
159
|
+
// 3. Check for SpellCraft metadata
|
|
155
160
|
if (depPkg.spellcraft || depPkg.keywords?.includes("spellcraft-module")) {
|
|
156
|
-
|
|
161
|
+
const jsMainPath = path.join(depDir, depPkg.main || 'index.js');
|
|
162
|
+
|
|
163
|
+
// 4. Load the plugin using the calculated absolute path
|
|
164
|
+
this.loadPlugin(depName, jsMainPath);
|
|
157
165
|
}
|
|
158
166
|
} catch (e) {
|
|
159
167
|
// Dependency might not be installed or resolvable, skip quietly
|
|
168
|
+
console.warn(`Debug: Could not load potential plugin ${depName}: ${e.message}`);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
loadLocalMagicModules() {
|
|
174
|
+
const localModulesDir = path.join(baseDir, 'spellcraft_modules');
|
|
175
|
+
const generatedDir = path.join(baseDir, '.spellcraft');
|
|
176
|
+
const aggregateFile = path.join(generatedDir, 'modules');
|
|
177
|
+
|
|
178
|
+
if (!fs.existsSync(localModulesDir)) {
|
|
179
|
+
// Clean up if it exists so imports fail gracefully if folder is deleted
|
|
180
|
+
if(fs.existsSync(aggregateFile)) fs.unlinkSync(aggregateFile);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Ensure hidden directory exists
|
|
185
|
+
if (!fs.existsSync(generatedDir)) fs.mkdirSync(generatedDir, { recursive: true });
|
|
186
|
+
|
|
187
|
+
const jsFiles = fs.readdirSync(localModulesDir).filter(f => f.endsWith('.js'));
|
|
188
|
+
|
|
189
|
+
let jsonnetContentParts = [];
|
|
190
|
+
|
|
191
|
+
jsFiles.forEach(file => {
|
|
192
|
+
const moduleName = path.basename(file, '.js');
|
|
193
|
+
const fullPath = path.join(localModulesDir, file);
|
|
194
|
+
|
|
195
|
+
let moduleExports;
|
|
196
|
+
try {
|
|
197
|
+
// Cache busting for dev speed
|
|
198
|
+
delete require.cache[require.resolve(fullPath)];
|
|
199
|
+
moduleExports = require(fullPath);
|
|
200
|
+
} catch (e) {
|
|
201
|
+
console.warn(`[!] Error loading local module ${file}: ${e.message}`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let fileMethods = [];
|
|
206
|
+
|
|
207
|
+
Object.keys(moduleExports).forEach(funcName => {
|
|
208
|
+
if (funcName === '_spellcraft_metadata') return; // Skip metadata
|
|
209
|
+
|
|
210
|
+
let func, params;
|
|
211
|
+
// Handle [func, "arg1", "arg2"] syntax or plain function
|
|
212
|
+
if (Array.isArray(moduleExports[funcName])) {
|
|
213
|
+
[func, ...params] = moduleExports[funcName];
|
|
214
|
+
} else if (typeof moduleExports[funcName] === 'function') {
|
|
215
|
+
func = moduleExports[funcName];
|
|
216
|
+
// You'll need the getFunctionParameterList helper from before
|
|
217
|
+
params = getFunctionParameterList(func);
|
|
218
|
+
} else {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Register with a unique local prefix
|
|
223
|
+
const uniqueId = `local_${moduleName}_${funcName}`;
|
|
224
|
+
this.addNativeFunction(uniqueId, func, ...params);
|
|
225
|
+
|
|
226
|
+
// Create the Jsonnet wrapper string
|
|
227
|
+
// e.g. myFunc(a, b):: std.native("local_utils_myFunc")(a, b)
|
|
228
|
+
const paramStr = params.join(", ");
|
|
229
|
+
fileMethods.push(` ${funcName}(${paramStr}):: std.native("${uniqueId}")(${paramStr})`);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
console.log(`[+] Loaded [${Object.keys(moduleExports).join(", ")}] from [${file}].`);
|
|
233
|
+
|
|
234
|
+
if (fileMethods.length > 0) {
|
|
235
|
+
jsonnetContentParts.push(` ${moduleName}: {\n${fileMethods.join(",\n")}\n }`);
|
|
160
236
|
}
|
|
161
237
|
});
|
|
238
|
+
|
|
239
|
+
// Generate the file
|
|
240
|
+
const finalContent = "{\n" + jsonnetContentParts.join(",\n") + "\n}";
|
|
241
|
+
fs.writeFileSync(aggregateFile, finalContent, 'utf-8');
|
|
162
242
|
}
|
|
163
243
|
|
|
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
244
|
loadPlugin(packageName, jsMainPath) {
|
|
170
245
|
if (!jsMainPath || !fs.existsSync(jsMainPath)) return;
|
|
171
246
|
|
|
@@ -238,12 +313,7 @@ exports.SpellFrame = class SpellFrame {
|
|
|
238
313
|
|
|
239
314
|
return this.lastRender;
|
|
240
315
|
}
|
|
241
|
-
|
|
242
|
-
// Removed: importSpellCraftModuleFromNpm
|
|
243
|
-
// Removed: loadModulesFromModuleDirectory
|
|
244
|
-
// Removed: loadModulesFromPackageList
|
|
245
|
-
// Removed: loadModuleByName (file copier)
|
|
246
|
-
|
|
316
|
+
|
|
247
317
|
write(filesToWrite = this.lastRender) {
|
|
248
318
|
if (!filesToWrite || typeof filesToWrite !== 'object') return this;
|
|
249
319
|
|
package/jsdoc.json
DELETED
|
@@ -1,33 +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
|
-
"lib/"
|
|
14
|
-
],
|
|
15
|
-
"includePattern": ".",
|
|
16
|
-
"excludePattern": "\\.bak$"
|
|
17
|
-
},
|
|
18
|
-
"opts": {
|
|
19
|
-
"destination": "./docs/",
|
|
20
|
-
"encoding": "utf8",
|
|
21
|
-
"private": true,
|
|
22
|
-
"recurse": false,
|
|
23
|
-
"readme": "README.md",
|
|
24
|
-
"template": "node_modules/clean-jsdoc-theme"
|
|
25
|
-
},
|
|
26
|
-
"markdown": {
|
|
27
|
-
"hardwrap": false,
|
|
28
|
-
"idInHeadings": true
|
|
29
|
-
},
|
|
30
|
-
"theme_opts": {
|
|
31
|
-
"default_theme": "dark"
|
|
32
|
-
}
|
|
33
|
-
}
|