@c6fc/spellcraft 0.0.1

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.
@@ -0,0 +1,403 @@
1
+ 'use strict';
2
+
3
+ const fs = require("fs");
4
+ const os = require("os");
5
+ const ini = require("ini");
6
+ const path = require("path");
7
+ const yaml = require('js-yaml');
8
+ const crypto = require('crypto');
9
+ const readline = require("readline");
10
+ const { spawnSync } = require('child_process');
11
+ const { Jsonnet } = require("@hanazuki/node-jsonnet");
12
+
13
+ const baseDir = process.cwd();
14
+
15
+ exports.SpellFrame = class {
16
+ renderPath; cliExtensions; cleanBeforeRender; fileTypeHandlers; initFn; jsonnet; lastRender; activePath; functionContext; useDefaultFileHandlers;
17
+
18
+ constructor(options) {
19
+
20
+ const defaults = {
21
+ renderPath: "./render",
22
+ cleanBeforeRender: true,
23
+ useDefaultFileHandlers: true
24
+ };
25
+
26
+ Object.keys(defaults).forEach(e => {
27
+ this[e] = options?.[e] ?? defaults[e];
28
+ });
29
+
30
+ // An array of functions aggregated from all plugins that must all succeed before files are processed
31
+ this.initFn = [];
32
+
33
+ // A cache of synchronous function execution results.
34
+ this.cache = {};
35
+
36
+ // An array of functions that extend the CLI 'yargs' argument.
37
+ this.cliExtensions = [];
38
+
39
+ this.fileTypeHandlers = (this.useDefaultFileHandlers) ? defaultFileTypeHandlers : {};
40
+
41
+ // An object to pass as `this` to all functions invoked via JSonnet.
42
+ this.functionContext = {};
43
+
44
+ this.jsonnet = new Jsonnet()
45
+ .addJpath(path.join(__dirname, '../lib'))
46
+ .addJpath(path.join(__dirname, '../modules'));
47
+
48
+ this.addFunction("envvar", (name) => {
49
+ return process.env?.[name] ?? false;
50
+ }, "name");
51
+
52
+ this.addFunction("path", () => {
53
+ return `${process.cwd()}`;
54
+ });
55
+
56
+ this.loadModulesFromPackageList();
57
+
58
+ return this;
59
+ }
60
+
61
+ _cacheKey(...args) {
62
+ return crypto.createHash('sha256').update(JSON.stringify(args)).digest('hex');
63
+ }
64
+
65
+ addFileTypeHander(pattern, handler) {
66
+ Object.defineProperty(this.fileTypeHandlers, pattern, {
67
+ value: handler,
68
+ writable: false
69
+ });
70
+
71
+ return this.fileTypeHandlers;
72
+ }
73
+
74
+ addFunction(name, fn, ...parameters) {
75
+ this.jsonnet.nativeCallback(name, (...args) => {
76
+
77
+ let key = this._cacheKey(name, args);
78
+ if (!!this.cache?.[key]) {
79
+ return this.cache[key];
80
+ }
81
+
82
+ this.cache[key] = fn.call(this.functionContext, ...args);
83
+
84
+ return this.cache[key];
85
+ }, ...parameters);
86
+ }
87
+
88
+ export(name, value) {
89
+ if (typeof value !== "string") {
90
+ value = JSON.stringify(value);
91
+ }
92
+
93
+ this.jsonnet = this.jsonnet.extCode(name, value);
94
+ return this;
95
+ }
96
+
97
+ extendWithModuleMetadata(metadata) {
98
+
99
+ }
100
+
101
+ import(path) {
102
+ this.jsonnet = this.jsonnet.addJpath(path);
103
+
104
+ return this;
105
+ }
106
+
107
+ async init() {
108
+ for (const step of this.initFn) {
109
+ await step();
110
+ }
111
+ }
112
+
113
+ async importSpellCraftModuleFromNpm(npmPackage, name = false) {
114
+ const npmPath = path.resolve(path.join(baseDir, 'node_modules', npmPackage));
115
+ if (!fs.existsSync(npmPath)) {
116
+ const install = spawnSync(`npm`, ['install', '-p', npmPackage], {
117
+ cwd: this.renderPath,
118
+ stdio: [process.stdin, process.stdout, process.stderr]
119
+ });
120
+ }
121
+
122
+ const configFile = path.join(npmPath, 'package.json');
123
+
124
+ if (!fs.existsSync(configFile)) {
125
+ console.log(`[!] Package ${npmPackage} is missing package.json`);
126
+ process.exit(1);
127
+ }
128
+
129
+ const { config } = JSON.parse(fs.readFileSync(path.join(configFile)));
130
+
131
+ if (!name && !config?.spellcraft_module_default_name) {
132
+ console.log(config);
133
+ console.log(`[!] No import name specified, and ${npmPackage} has no default import name`);
134
+ process.exit(1);
135
+ }
136
+
137
+ const packagesDirPath = path.join(baseDir, 'spellcraft_modules');
138
+ if (!fs.existsSync(packagesDirPath)) {
139
+ fs.mkdirSync(packagesDirPath);
140
+ }
141
+
142
+ let packages = {};
143
+ const packagesFilePath = path.join(baseDir, 'spellcraft_modules', 'packages.json')
144
+ if (fs.existsSync(packagesFilePath)) {
145
+ packages = JSON.parse(fs.readFileSync(packagesFilePath));
146
+ }
147
+
148
+ let npmPackageBaseName;
149
+
150
+ // If the package is namespaced,
151
+ if (npmPackage[0] == "@") {
152
+ [ , npmPackageBaseName ] = npmPackage.split('@');
153
+ npmPackageBaseName = `@${npmPackageBaseName}`;
154
+ } else {
155
+ [ npmPackageBaseName ] = npmPackage.split('@');
156
+ }
157
+
158
+ const packagesKey = name || config.spellcraft_module_default_name;
159
+ packages[packagesKey] = npmPackageBaseName;
160
+
161
+ fs.writeFileSync(packagesFilePath, JSON.stringify(packages, null, "\t"));
162
+
163
+ console.log(`[+] Linked ${npmPackage} as ${packagesKey}`);
164
+
165
+ }
166
+
167
+ async render(file) {
168
+ if (!fs.existsSync(file)) {
169
+ throw new Error(`Sonnetry Error: ${file} does not exist.`);
170
+ }
171
+
172
+ this.activePath = path.dirname(path.resolve(file));
173
+
174
+ const moduleFile = path.resolve(path.join(__dirname, '../modules/modules'));
175
+
176
+ if (fs.existsSync(moduleFile)) {
177
+ throw new Error(`[!] The module target file [${moduleFile}] already exists. Remove or rename it before continuing.`);
178
+ }
179
+
180
+ this.loadModulesFromModuleDirectory(moduleFile);
181
+
182
+ console.log(this.renderPath);
183
+
184
+ this.renderPath = (this.renderPath.split("").slice(-1)[0] == "/") ?
185
+ this.renderPath.split("").slice(0, -1).join("") :
186
+ this.renderPath;
187
+
188
+ try {
189
+ this.lastRender = JSON.parse(await this.jsonnet.evaluateFile(file));
190
+ } catch (e) {
191
+ throw new Error(`Error parsing Jsonnet file: ${e}`);
192
+ }
193
+
194
+ const modulePath = path.resolve(path.join(__dirname, '../modules/'));
195
+
196
+ fs.readdirSync(modulePath)
197
+ .map(e => path.join(modulePath, e))
198
+ .forEach(e => fs.unlinkSync(e));
199
+
200
+ return this.lastRender;
201
+ }
202
+
203
+ loadModulesFromModuleDirectory(moduleFile) {
204
+
205
+ const modulePath = path.join(baseDir, 'spellcraft_modules');
206
+
207
+ if (!fs.existsSync(modulePath)) {
208
+ return [];
209
+ }
210
+
211
+ const regex = /.*?\.js$/
212
+ const fileList = fs.readdirSync(modulePath)
213
+ .filter(f => regex.test(f))
214
+ .map(f => path.join(modulePath, f));
215
+
216
+ return this.loadModulesFromFileList(fileList, moduleFile);
217
+ }
218
+
219
+ loadModulesFromPackageList() {
220
+ const packagePath = path.join(baseDir, 'spellcraft_modules', 'packages.json');
221
+
222
+ if (!fs.existsSync(packagePath)) {
223
+ console.log('No spellcraft_modules/packages.json file found. Skip package-based module import');
224
+ return [];
225
+ }
226
+
227
+ const packages = JSON.parse(fs.readFileSync(packagePath));
228
+
229
+ return Object.keys(packages).map(k => {
230
+ const configFile = path.join(baseDir, 'node_modules', packages[k], 'package.json');
231
+ const config = JSON.parse(fs.readFileSync(configFile));
232
+
233
+ const jsMainFile = path.join(baseDir, 'node_modules', packages[k], config.main);
234
+ this.loadFunctionsFromFile(jsMainFile);
235
+
236
+ const moduleFile = path.resolve(path.join(__dirname, '..', 'modules', k));
237
+ const importFile = path.resolve(path.join(jsMainFile, '..', 'module.libsonnet'));
238
+
239
+ if (fs.existsSync(importFile)) {
240
+ fs.copyFileSync(importFile, moduleFile);
241
+ }
242
+
243
+ return k;
244
+ });
245
+ }
246
+
247
+ loadModulesFromFileList(fileList, moduleFile) {
248
+
249
+ let registeredFunctions = [];
250
+
251
+ if (fileList.length < 1) {
252
+ return [];
253
+ }
254
+
255
+ let magicContent = [];
256
+
257
+ fileList.map(file => {
258
+ const { functions, magic } = this.loadFunctionsFromFile(file);
259
+ registeredFunctions = registeredFunctions.concat(functions);
260
+ magicContent = magicContent.concat(magic);
261
+ });
262
+
263
+ fs.writeFileSync(moduleFile, `{\n${magicContent.join(",\n")}\n}`);
264
+
265
+ console.log(`[+] Registered ${fileList.length} module${(fileList.length > 1) ? 's' : ''} as '${path.basename(moduleFile)}' comprising ${registeredFunctions.length} function${(registeredFunctions.length > 1) ? 's' : ''}: [ ${registeredFunctions.sort().join(', ')} ]`)
266
+
267
+ return { registeredFunctions, magicContent };
268
+ }
269
+
270
+ loadFunctionsFromFile(file) {
271
+ const functions = require(file);
272
+
273
+ const magicContent = [];
274
+ if (functions._spellcraft_metadata) {
275
+ const metadata = functions._spellcraft_metadata;
276
+ ['fileTypeHandlers', 'functionContext'].forEach(e => Object.assign(this[e], metadata[e] ?? {}));
277
+
278
+ ['cliExtensions'].forEach(e => metadata[e] && this[e].push(metadata[e]));
279
+
280
+ metadata.initFn && this.init.push(metadata.initFn);
281
+ }
282
+
283
+ const registeredFunctions = Object.keys(functions).filter(e => e !== '_spellcraft_metadata').map(e => {
284
+
285
+ let fn, parameters;
286
+
287
+ if (typeof functions[e] == "object") {
288
+ [fn, ...parameters] = functions[e];
289
+ }
290
+
291
+ if (typeof functions[e] == "function") {
292
+ fn = functions[e];
293
+ parameters = getFunctionParameterList(fn);
294
+ }
295
+
296
+ magicContent.push(`\t${e}(${parameters.join(', ')}):: std.native('${e}')(${parameters.join(', ')})`);
297
+
298
+ this.addFunction(e, fn, ...parameters);
299
+ return e;
300
+ });
301
+
302
+ return { functions: registeredFunctions, magic: magicContent };
303
+ }
304
+
305
+ toString() {
306
+ if (this?.lastRender) {
307
+ return this.lastRender
308
+ }
309
+
310
+ return null;
311
+ }
312
+
313
+ write(files = this.lastRender) {
314
+ try {
315
+ if (!fs.existsSync(this.renderPath)) {
316
+ fs.mkdirSync(this.renderPath, { recursive: true });
317
+ }
318
+ } catch (e) {
319
+ throw new Error(`Spellcraft Error: renderPath could not be created. ${e}`);
320
+ }
321
+
322
+ if (this.cleanBeforeRender) {
323
+ try {
324
+ Object.keys(this.fileTypeHandlers).forEach(regex => {
325
+ // console.log(regex);
326
+ fs.readdirSync(this.renderPath)
327
+ .filter(f => new RegExp(regex, "i").test(f))
328
+ .map(f => fs.unlinkSync(`${this.renderPath}/${f}`));
329
+ });
330
+ } catch (e) {
331
+ throw new Error(`Failed to remove files from renderPath. ${e}`);
332
+ }
333
+ }
334
+
335
+ try {
336
+ for (const filename in files) {
337
+ const outputPath = `${this.renderPath}/${filename}`;
338
+
339
+ const [, handler] = Object.entries(this.fileTypeHandlers)
340
+ .find(([pattern, ]) => new RegExp(pattern).test(filename)) || [false, defaultFileHandler];
341
+
342
+ fs.writeFileSync(outputPath, handler(files[filename]));
343
+ console.log(' ' + path.basename(outputPath));
344
+ }
345
+ } catch (e) {
346
+ throw new Error(`Failed to write to renderPath. ${e}`);
347
+ }
348
+
349
+ return this;
350
+ }
351
+ };
352
+
353
+ const defaultFileTypeHandlers = {
354
+ // JSON files
355
+ '.*?\.json$': (e) => {
356
+ // console.log('Using JSON encoder');
357
+ return JSON.stringify(e, null, 4)
358
+ },
359
+
360
+ // YAML files
361
+ '.*?\.yaml$': (e) => {
362
+ // console.log('Using YAML encoder');
363
+ yaml.dump(e, {
364
+ indent: 4
365
+ })
366
+ },
367
+
368
+ '.*?\.yml$': (e) => {
369
+ // console.log('Using YAML encoder');
370
+ yaml.dump(e, {
371
+ indent: 4
372
+ })
373
+ },
374
+ };
375
+
376
+ const defaultFileHandler = (e) => JSON.stringify(e, null, 4);
377
+
378
+ function getFunctionParameterList(fn) {
379
+
380
+ let str = fn.toString();
381
+
382
+ str = str.replace(/\/\*[\s\S]*?\*\//g, '')
383
+ .replace(/\/\/(.)*/g, '')
384
+ .replace(/{[\s\S]*}/, '')
385
+ .replace(/=>/g, '')
386
+ .trim();
387
+
388
+ const start = str.indexOf("(") + 1;
389
+ const end = str.length - 1;
390
+
391
+ const result = str.substring(start, end).split(", ");
392
+
393
+ const params = [];
394
+ result.forEach(element => {
395
+ element = element.replace(/=[\s\S]*/g, '').trim();
396
+
397
+ if(element.length > 0) {
398
+ params.push(element);
399
+ }
400
+ });
401
+
402
+ return params;
403
+ }
package/src/test.js ADDED
@@ -0,0 +1,16 @@
1
+ #! /usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const { SpellFrame } = require('./index.js');
5
+
6
+ const sonnetry = new SpellFrame({
7
+ renderPath: './render',
8
+ cleanBeforeRender: true
9
+ });
10
+
11
+ (async () => {
12
+
13
+ testBootstrap = sonnetry.render(`local spellcraft = import 'spellcraft'; { test: spellcraft.path() }`);
14
+ console.log(testBootstrap);
15
+
16
+ })();