@triophore/falcon-cli 1.0.0

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 (56) hide show
  1. package/README.md +62 -0
  2. package/auth/basic.js +8 -0
  3. package/auth/cookie.js +10 -0
  4. package/auth/jwks.js +6 -0
  5. package/auth/jwt.js +9 -0
  6. package/auth/openid.js +5 -0
  7. package/auth/webscoket.js +6 -0
  8. package/builder/EnvBuilder.js +0 -0
  9. package/builder/createCjsModule.js +95 -0
  10. package/builder/editModelInteractive.js +159 -0
  11. package/builder/interactiveModelBuilder.js +215 -0
  12. package/builder/interactiveMongobuilder.js +189 -0
  13. package/builder/interactiveUnifiedBuilder.js +277 -0
  14. package/builder/joiValidatorBuilder.js +218 -0
  15. package/builder/mongooseModelBuilder.js +290 -0
  16. package/builder/mongooseModelBuilder2.js +313 -0
  17. package/builder/runMigrations.js +106 -0
  18. package/builder/sequelizeModelBuilder.js +180 -0
  19. package/cli.js +60 -0
  20. package/commands/create.js +57 -0
  21. package/commands/generate.js +74 -0
  22. package/dev/Uset.schema.json +18 -0
  23. package/dev/buildSchemaInteractive.js +189 -0
  24. package/dev/buildSequelizeSchemaInteractive.js +128 -0
  25. package/dev/createJoiSchemaFromJson.js +137 -0
  26. package/dev/createModelFromJson.js +280 -0
  27. package/dev/generateAllFiles.js +45 -0
  28. package/dev/generateJoiFile.js +95 -0
  29. package/dev/generateSequelizeFiles.js +167 -0
  30. package/dev/interactiveJoiBuilder.js +177 -0
  31. package/dev/ra.js +22 -0
  32. package/dev/rj.js +18 -0
  33. package/dev/run.js +16 -0
  34. package/dev/run_seq.js +18 -0
  35. package/dev/tracker.js +23 -0
  36. package/editJsConfig.js +188 -0
  37. package/index.js +548 -0
  38. package/lib/ModelGenerator.js +203 -0
  39. package/lib/ProjectGenerator.js +246 -0
  40. package/lib/utils.js +100 -0
  41. package/logo.js +3 -0
  42. package/package.json +35 -0
  43. package/readme.md +2 -0
  44. package/schema.json +42 -0
  45. package/templates/auth_vals.json +3 -0
  46. package/templates/config.js +0 -0
  47. package/templates/example-route.js +94 -0
  48. package/templates/example-service.js +63 -0
  49. package/templates/example-validator.js +15 -0
  50. package/templates/example-worker.js +83 -0
  51. package/templates/index.txt +41 -0
  52. package/templates/post-init.js +78 -0
  53. package/templates/settings.js +192 -0
  54. package/templates/template1.settings.txt +15 -0
  55. package/templates/templatev1.json +38 -0
  56. package/validateJsConfig.js +125 -0
@@ -0,0 +1,167 @@
1
+ // generateSequelizeFiles.js
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ function generateModelFile(config) {
6
+ const { name, schema, associations, options } = config;
7
+ const modelName = name;
8
+ const tableName = options.tableName;
9
+
10
+ let code = `const { DataTypes } = require('sequelize');\n\n`;
11
+ code += `module.exports = (sequelize) => {\n`;
12
+ code += ` const ${modelName} = sequelize.define('${modelName}', {\n`;
13
+
14
+ for (const [field, def] of Object.entries(schema)) {
15
+ code += ` ${field}: {\n`;
16
+ code += ` type: ${formatType(def.type, def.values)},\n`;
17
+ if (def.allowNull === false) code += ` allowNull: false,\n`;
18
+ if (def.unique) code += ` unique: true,\n`;
19
+ if (def.defaultValue !== undefined) {
20
+ code += ` defaultValue: ${formatDefault(def.defaultValue)},\n`;
21
+ }
22
+ code = code.replace(/,\n$/, '\n'); // remove last comma
23
+ code += ` },\n`;
24
+ }
25
+
26
+ code += ` }, {\n`;
27
+ code += ` tableName: '${tableName}',\n`;
28
+ code += ` timestamps: ${options.timestamps},\n`;
29
+ code += ` paranoid: ${options.paranoid},\n`;
30
+ code += ` freezeTableName: true\n`;
31
+ code += ` });\n\n`;
32
+
33
+ // Associations
34
+ if (associations.length > 0) {
35
+ code += ` // Associations\n`;
36
+ associations.forEach(assoc => {
37
+ if (assoc.type === 'belongsTo') {
38
+ code += ` ${modelName}.belongsTo(sequelize.models.${assoc.targetModel}, { foreignKey: '${assoc.foreignKey}', as: '${assoc.as}' });\n`;
39
+ } else if (assoc.type === 'hasMany') {
40
+ code += ` ${modelName}.hasMany(sequelize.models.${assoc.targetModel}, { foreignKey: '${assoc.foreignKey}', as: '${assoc.as}' });\n`;
41
+ }
42
+ });
43
+ }
44
+
45
+ code += `\n return ${modelName};\n};\n`;
46
+
47
+ return code;
48
+ }
49
+
50
+ function generateMigrationFile(config) {
51
+ const timestamp = new Date().toISOString().replace(/[-:T.]/g, '').slice(0, 14);
52
+ const migrationName = `create-${config.tableName}`;
53
+ const fileName = `${timestamp}-${migrationName}.js`;
54
+
55
+ let code = `'use strict';\n\n`;
56
+ code += `module.exports = {\n`;
57
+ code += ` up: async (queryInterface, Sequelize) => {\n`;
58
+ code += ` await queryInterface.createTable('${config.tableName}', {\n`;
59
+
60
+ // Add id
61
+ code += ` id: {\n`;
62
+ code += ` allowNull: false,\n`;
63
+ code += ` autoIncrement: true,\n`;
64
+ code += ` primaryKey: true,\n`;
65
+ code += ` type: Sequelize.INTEGER\n`;
66
+ code += ` },\n`;
67
+
68
+ // Add fields
69
+ for (const [field, def] of Object.entries(config.schema)) {
70
+ code += ` ${field}: {\n`;
71
+ code += ` type: ${formatType(def.type, def.values)},\n`;
72
+ if (def.allowNull === false) code += ` allowNull: false,\n`;
73
+ if (def.unique) code += ` unique: true,\n`;
74
+ if (def.defaultValue !== undefined) {
75
+ code += ` defaultValue: ${formatDefault(def.defaultValue)},\n`;
76
+ }
77
+ code = code.replace(/,\n$/, '\n');
78
+ code += ` },\n`;
79
+ }
80
+
81
+ // Timestamps
82
+ if (config.options.timestamps) {
83
+ code += ` createdAt: { allowNull: false, type: Sequelize.DATE, defaultValue: Sequelize.fn('NOW') },\n`;
84
+ code += ` updatedAt: { allowNull: false, type: Sequelize.DATE, defaultValue: Sequelize.fn('NOW') },\n`;
85
+ }
86
+ if (config.options.paranoid) {
87
+ code += ` deletedAt: { type: Sequelize.DATE },\n`;
88
+ }
89
+
90
+ code += ` });\n`;
91
+
92
+ // Add indexes
93
+ code += `\n // Indexes\n`;
94
+ for (const [field, def] of Object.entries(config.schema)) {
95
+ if (def.unique) {
96
+ code += ` await queryInterface.addIndex('${config.tableName}', ['${field}'], { unique: true });\n`;
97
+ }
98
+ }
99
+
100
+ // Foreign keys
101
+ const fks = config.associations.filter(a => a.type === 'belongsTo');
102
+ if (fks.length > 0) {
103
+ code += `\n // Foreign Keys\n`;
104
+ for (const fk of fks) {
105
+ code += ` await queryInterface.addConstraint('${config.tableName}', {\n`;
106
+ code += ` fields: ['${fk.foreignKey}'],\n`;
107
+ code += ` type: 'foreign key',\n`;
108
+ code += ` name: 'fk_${config.tableName}_${fk.foreignKey}',\n`;
109
+ code += ` references: { table: '${fk.targetModel.toLowerCase()}s', field: 'id' },\n`;
110
+ code += ` onDelete: 'SET NULL',\n`;
111
+ code += ` onUpdate: 'CASCADE'\n`;
112
+ code += ` });\n`;
113
+ }
114
+ }
115
+
116
+ code += ` },\n\n`;
117
+
118
+ code += ` down: async (queryInterface) => {\n`;
119
+ code += ` await queryInterface.dropTable('${config.tableName}');\n`;
120
+ code += ` }\n};\n`;
121
+
122
+ return { fileName, code };
123
+ }
124
+
125
+ function formatType(type, enumValues) {
126
+ if (type.includes('[]')) {
127
+ const base = type.replace('[]', '');
128
+ return `Sequelize.ARRAY(Sequelize.${base})`;
129
+ }
130
+ if (type.includes('(')) {
131
+ const [base, len] = type.split('(');
132
+ return `Sequelize.${base}(${len}`;
133
+ }
134
+ if (type === 'ENUM') {
135
+ return `Sequelize.ENUM(${enumValues.map(v => `'${v}'`).join(', ')})`;
136
+ }
137
+ return `Sequelize.${type}`;
138
+ }
139
+
140
+ function formatDefault(val) {
141
+ if (val === 'sequelize.fn("NOW")') return `Sequelize.fn('NOW')`;
142
+ if (val === true) return 'true';
143
+ if (val === false) return 'false';
144
+ if (!isNaN(val)) return val;
145
+ return `'${val}'`;
146
+ }
147
+
148
+ function exportFiles(config) {
149
+ const modelDir = 'models';
150
+ const migrationDir = 'migrations';
151
+
152
+ if (!fs.existsSync(modelDir)) fs.mkdirSync(modelDir);
153
+ if (!fs.existsSync(migrationDir)) fs.mkdirSync(migrationDir);
154
+
155
+ // Model
156
+ const modelCode = generateModelFile(config);
157
+ fs.writeFileSync(path.join(modelDir, `${config.name}.js`), modelCode);
158
+
159
+ // Migration
160
+ const { fileName, code } = generateMigrationFile(config);
161
+ fs.writeFileSync(path.join(migrationDir, fileName), code);
162
+
163
+ console.log(`Model: ${modelDir}/${config.name}.js`);
164
+ console.log(`Migration: ${migrationDir}/${fileName}`);
165
+ }
166
+
167
+ module.exports = { exportFiles };
@@ -0,0 +1,177 @@
1
+ // interactiveJoiBuilder.js
2
+ const {
3
+ input,
4
+ confirm,
5
+ select,
6
+ checkbox,
7
+ } = require('@inquirer/prompts');
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ async function interactiveJoiBuilder() {
12
+ console.log('\nJoi Schema Builder\n');
13
+
14
+ const schemaName = await input({
15
+ message: 'Schema name (e.g., UserSchema):',
16
+ default: 'MySchema',
17
+ });
18
+
19
+ const schema = {};
20
+ console.log('\nAdd fields:\n');
21
+
22
+ while (true) {
23
+ const addField = await confirm({
24
+ message: 'Add a field?',
25
+ default: true,
26
+ });
27
+ if (!addField) break;
28
+
29
+ const fieldName = await input({
30
+ message: 'Field name:',
31
+ validate: v => v.trim() && !schema[v] ? true : 'Invalid or duplicate name',
32
+ });
33
+
34
+ const type = await select({
35
+ message: `Type for "${fieldName}":`,
36
+ choices: [
37
+ { name: 'string', value: 'string' },
38
+ { name: 'number', value: 'number' },
39
+ { name: 'boolean', value: 'boolean' },
40
+ { name: 'date', value: 'date' },
41
+ { name: 'array', value: 'array' },
42
+ { name: 'object', value: 'object' },
43
+ { name: 'any', value: 'any' },
44
+ ],
45
+ });
46
+
47
+ const field = { type };
48
+
49
+ // Required
50
+ const required = await confirm({ message: 'Required?', default: false });
51
+ if (required) field.required = true;
52
+
53
+ // Default
54
+ const hasDefault = await confirm({ message: 'Set default value?', default: false });
55
+ if (hasDefault) {
56
+ if (type === 'boolean') {
57
+ field.default = await confirm({ message: 'Default TRUE?', default: true });
58
+ } else if (type === 'date') {
59
+ const now = await confirm({ message: 'Default to now?', default: true });
60
+ field.default = now ? 'Date.now()' : await input({ message: 'Default value (JS):' });
61
+ } else {
62
+ field.default = await input({ message: 'Default value (JS literal):' });
63
+ }
64
+ }
65
+
66
+ // Type-specific rules
67
+ if (type === 'string') {
68
+ const stringRules = await checkbox({
69
+ message: 'String rules:',
70
+ choices: [
71
+ { name: 'email', value: 'email' },
72
+ { name: 'trim', value: 'trim' },
73
+ { name: 'lowercase', value: 'lowercase' },
74
+ { name: 'uppercase', value: 'uppercase' },
75
+ { name: 'alphanum', value: 'alphanum' },
76
+ { name: 'uuid', value: 'uuid' },
77
+ ],
78
+ });
79
+ stringRules.forEach(r => field[r] = true);
80
+
81
+ const min = await input({ message: 'Min length (or leave empty):' });
82
+ const max = await input({ message: 'Max length (or leave empty):' });
83
+ if (min) field.min = +min;
84
+ if (max) field.max = +max;
85
+
86
+ const hasRegex = await confirm({ message: 'Add regex pattern?', default: false });
87
+ if (hasRegex) {
88
+ const pattern = await input({ message: 'Regex (e.g., ^[a-z]+$):' });
89
+ field.regex = pattern;
90
+ }
91
+
92
+ const hasEnum = await confirm({ message: 'Use allowed values (enum)?', default: false });
93
+ if (hasEnum) {
94
+ const values = await input({ message: 'Comma-separated values:' });
95
+ field.enum = values.split(',').map(v => v.trim()).filter(Boolean);
96
+ }
97
+ }
98
+
99
+ if (type === 'number') {
100
+ const numberRules = await checkbox({
101
+ message: 'Number rules:',
102
+ choices: [
103
+ { name: 'integer', value: 'integer' },
104
+ { name: 'positive', value: 'positive' },
105
+ { name: 'negative', value: 'negative' },
106
+ ],
107
+ });
108
+ numberRules.forEach(r => field[r] = true);
109
+
110
+ const min = await input({ message: 'Min value:' });
111
+ const max = await input({ message: 'Max value:' });
112
+ if (min) field.min = +min;
113
+ if (max) field.max = +max;
114
+ }
115
+
116
+ if (type === 'array') {
117
+ const itemType = await select({
118
+ message: 'Array item type:',
119
+ choices: ['string', 'number', 'object', 'any'].map(t => ({ name: t, value: t })),
120
+ });
121
+ field.items = { type: itemType };
122
+
123
+ const min = await input({ message: 'Min items:' });
124
+ const max = await input({ message: 'Max items:' });
125
+ if (min) field.min = +min;
126
+ if (max) field.max = +max;
127
+
128
+ const unique = await confirm({ message: 'Unique items?', default: false });
129
+ if (unique) field.unique = true;
130
+ }
131
+
132
+ if (type === 'object') {
133
+ console.log(`\nDefine nested object for "${fieldName}":\n`);
134
+ field.properties = await buildNestedObject();
135
+ }
136
+
137
+ schema[fieldName] = field;
138
+ }
139
+
140
+ return { name: schemaName, schema };
141
+ }
142
+
143
+ // Recursive nested object builder
144
+ async function buildNestedObject() {
145
+ const props = {};
146
+ while (true) {
147
+ const add = await confirm({ message: 'Add nested field?', default: true });
148
+ if (!add) break;
149
+
150
+ const name = await input({ message: 'Field name:' });
151
+ const type = await select({
152
+ message: 'Type:',
153
+ choices: ['string', 'number', 'boolean', 'date', 'array', 'object', 'any'].map(t => ({ name: t, value: t })),
154
+ });
155
+
156
+ const field = { type };
157
+ const required = await confirm({ message: 'Required?', default: false });
158
+ if (required) field.required = true;
159
+
160
+ if (type === 'string') {
161
+ const rules = await checkbox({
162
+ message: 'Rules:',
163
+ choices: ['email', 'trim', 'lowercase'],
164
+ });
165
+ rules.forEach(r => field[r] = true);
166
+ }
167
+
168
+ if (type === 'object') {
169
+ field.properties = await buildNestedObject();
170
+ }
171
+
172
+ props[name] = field;
173
+ }
174
+ return props;
175
+ }
176
+
177
+ module.exports = { interactiveJoiBuilder };
package/dev/ra.js ADDED
@@ -0,0 +1,22 @@
1
+ // run.js
2
+ const { interactiveModelBuilder } = require('./interactiveModelBuilder');
3
+ const { generateAllFiles } = require('./generateAllFiles');
4
+ const { createModelFromJson } = require('./createModelFromJson');
5
+ const { createJoiSchemaFromJson } = require('./createJoiSchemaFromJson');
6
+
7
+ (async () => {
8
+ try {
9
+ const config = await interactiveModelBuilder();
10
+ console.log('\nConfig:'.green, JSON.stringify(config, null, 2));
11
+
12
+ generateAllFiles(config);
13
+
14
+ // Optional: test
15
+ const result = createModelFromJson(config);
16
+ const joi = createJoiSchemaFromJson({ schema: config.joi });
17
+ console.log('\nJoi schema ready! Use:'.cyan, `joi.validate(data)`);
18
+
19
+ } catch (err) {
20
+ console.error('Cancelled:', err.message);
21
+ }
22
+ })();
package/dev/rj.js ADDED
@@ -0,0 +1,18 @@
1
+ // run.js
2
+ const { interactiveJoiBuilder } = require('./interactiveJoiBuilder');
3
+ const { generateJoiFile } = require('./generateJoiFile');
4
+
5
+ (async () => {
6
+ try {
7
+ console.clear();
8
+ const config = await interactiveJoiBuilder();
9
+ console.log('\nGenerated Joi Config:'.green);
10
+ console.log(JSON.stringify(config, null, 2));
11
+
12
+ generateJoiFile(config, './joi-schemas');
13
+
14
+ console.log('\nJoi schema generated and saved!'.green);
15
+ } catch (err) {
16
+ console.error('Cancelled or error:'.red, err.message);
17
+ }
18
+ })();
package/dev/run.js ADDED
@@ -0,0 +1,16 @@
1
+ const { buildMongooseSchemaInteractive } = require('./buildSchemaInteractive');
2
+
3
+ (async () => {
4
+ try {
5
+ const schemaJson = await buildMongooseSchemaInteractive();
6
+ console.log('\nGenerated Schema JSON:'.green);
7
+ console.log(JSON.stringify(schemaJson, null, 2));
8
+
9
+ // Optional: Save to file
10
+ const fs = require('fs');
11
+ fs.writeFileSync(`${schemaJson.name}.schema.json`, JSON.stringify(schemaJson, null, 2));
12
+ console.log(`\nSaved to ${schemaJson.name}.schema.json`.green);
13
+ } catch (err) {
14
+ console.error('Cancelled or error:', err.message);
15
+ }
16
+ })();
package/dev/run_seq.js ADDED
@@ -0,0 +1,18 @@
1
+ // run.js
2
+ const { buildSequelizeSchemaInteractive } = require('./buildSequelizeSchemaInteractive');
3
+ const { exportFiles } = require('./generateSequelizeFiles');
4
+
5
+ (async () => {
6
+ try {
7
+ console.clear();
8
+ const config = await buildSequelizeSchemaInteractive();
9
+ console.log('\nGenerated Config:'.green);
10
+ console.log(JSON.stringify(config, null, 2));
11
+
12
+ exportFiles(config);
13
+
14
+ console.log('\nAll files generated successfully!'.green);
15
+ } catch (err) {
16
+ console.error('Cancelled or error:'.red, err.message);
17
+ }
18
+ })();
package/dev/tracker.js ADDED
@@ -0,0 +1,23 @@
1
+ class Tracker {
2
+ constructor(fpath) {
3
+ this.fpath = fpath;
4
+ }
5
+
6
+ async check_tracker_exists() {
7
+ return require("fs").existsSync(fpath);
8
+ }
9
+
10
+ async check_tracker_valid() {
11
+ try {
12
+ JSON.parse(fstring);
13
+ return true;
14
+ } catch (error) {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ async check_tracker_read() {
20
+ return require("fs").readFileSync(fpath);
21
+ }
22
+ }
23
+ module.exports.Tracker = Tracker;
@@ -0,0 +1,188 @@
1
+ // editJsConfig.js
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const acorn = require('acorn');
5
+ const estraverse = require('estraverse');
6
+ const escodegen = require('escodegen');
7
+
8
+ /**
9
+ * READ: Get the current settings object from JS file
10
+ * @param {string} filePath
11
+ * @returns {Object} settings object
12
+ */
13
+ function getJsConfigProperties(filePath) {
14
+ const fullPath = path.resolve(filePath);
15
+ if (!fs.existsSync(fullPath)) {
16
+ throw new Error(`File not found: ${fullPath}`);
17
+ }
18
+
19
+ const code = fs.readFileSync(fullPath, 'utf8');
20
+ const ast = acorn.parse(code, {
21
+ ecmaVersion: 'latest',
22
+ sourceType: 'module',
23
+ locations: true,
24
+ });
25
+
26
+ let settingsValue = null;
27
+
28
+ estraverse.traverse(ast, {
29
+ enter(node) {
30
+ if (
31
+ node.type === 'AssignmentExpression' &&
32
+ node.left.type === 'MemberExpression' &&
33
+ node.left.object.name === 'module' &&
34
+ node.left.property.name === 'exports' &&
35
+ node.right.type === 'MemberExpression' &&
36
+ node.right.property.name === 'settings'
37
+ ) {
38
+ settingsValue = evaluateObjectExpression(node.right.object);
39
+ this.break(); // Stop traversal
40
+ }
41
+ }
42
+ });
43
+
44
+ if (settingsValue === null) {
45
+ throw new Error('Could not find module.exports.settings');
46
+ }
47
+
48
+ return settingsValue;
49
+ }
50
+
51
+ /**
52
+ * EDIT: Edit a nested property and save file
53
+ * @param {string} filePath
54
+ * @param {string} propPath - e.g., 'crud.exclude', 'routes'
55
+ * @param {any} newValue
56
+ * @returns {boolean}
57
+ */
58
+ function editJsConfig(filePath, propPath, newValue) {
59
+ try {
60
+ const fullPath = path.resolve(filePath);
61
+ if (!fs.existsSync(fullPath)) {
62
+ throw new Error(`File not found: ${fullPath}`);
63
+ }
64
+
65
+ const code = fs.readFileSync(fullPath, 'utf8');
66
+ const ast = acorn.parse(code, {
67
+ ecmaVersion: 'latest',
68
+ sourceType: 'module',
69
+ locations: true,
70
+ });
71
+
72
+ let modified = false;
73
+ const pathParts = propPath.split('.');
74
+
75
+ estraverse.traverse(ast, {
76
+ enter(node) {
77
+ if (
78
+ node.type === 'AssignmentExpression' &&
79
+ node.left.type === 'MemberExpression' &&
80
+ node.left.object.name === 'module' &&
81
+ node.left.property.name === 'exports' &&
82
+ node.right.type === 'MemberExpression' &&
83
+ node.right.property.name === 'settings'
84
+ ) {
85
+ modified = editNestedProperty(node.right.object, pathParts, newValue);
86
+ }
87
+ }
88
+ });
89
+
90
+ if (!modified) {
91
+ throw new Error(`Property "${propPath}" not found`);
92
+ }
93
+
94
+ const newCode = escodegen.generate(ast, {
95
+ format: {
96
+ indent: { style: ' ' },
97
+ newline: '\n',
98
+ quotes: 'double',
99
+ semicolons: true,
100
+ },
101
+ });
102
+
103
+ fs.writeFileSync(fullPath, newCode, 'utf8');
104
+ console.log(`Updated: ${propPath}`);
105
+ return true;
106
+ } catch (err) {
107
+ console.error('Edit error:', err.message);
108
+ return false;
109
+ }
110
+ }
111
+
112
+ // ── Helper: Evaluate ObjectExpression → JS Object
113
+ function evaluateObjectExpression(node) {
114
+ if (node.type !== 'ObjectExpression') return null;
115
+
116
+ const obj = {};
117
+ for (const prop of node.properties) {
118
+ if (prop.type !== 'Property' || prop.key.type !== 'Identifier') continue;
119
+
120
+ const key = prop.key.name;
121
+ const valueNode = prop.value;
122
+
123
+ switch (valueNode.type) {
124
+ case 'Literal':
125
+ obj[key] = valueNode.value;
126
+ break;
127
+ case 'ArrayExpression':
128
+ obj[key] = valueNode.elements.map(el => el.type === 'Literal' ? el.value : null);
129
+ break;
130
+ case 'ObjectExpression':
131
+ obj[key] = evaluateObjectExpression(valueNode);
132
+ break;
133
+ default:
134
+ obj[key] = null; // Unsupported
135
+ }
136
+ }
137
+ return obj;
138
+ }
139
+
140
+ // ── Helper: Edit nested property in AST
141
+ function editNestedProperty(node, pathParts, newValue) {
142
+ if (pathParts.length === 0 || node.type !== 'ObjectExpression') return false;
143
+
144
+ const currentKey = pathParts[0];
145
+
146
+ for (const prop of node.properties) {
147
+ if (
148
+ prop.type === 'Property' &&
149
+ prop.key.type === 'Identifier' &&
150
+ prop.key.name === currentKey
151
+ ) {
152
+ if (pathParts.length === 1) {
153
+ prop.value = createLiteralNode(newValue);
154
+ return true;
155
+ } else {
156
+ return editNestedProperty(prop.value, pathParts.slice(1), newValue);
157
+ }
158
+ }
159
+ }
160
+ return false;
161
+ }
162
+
163
+ // ── Helper: Convert JS value → AST node
164
+ function createLiteralNode(value) {
165
+ if (Array.isArray(value)) {
166
+ return {
167
+ type: 'ArrayExpression',
168
+ elements: value.map(v => createLiteralNode(v)),
169
+ };
170
+ }
171
+ if (value === null || typeof value !== 'object') {
172
+ return { type: 'Literal', value };
173
+ }
174
+ return {
175
+ type: 'ObjectExpression',
176
+ properties: Object.entries(value).map(([k, v]) => ({
177
+ type: 'Property',
178
+ key: { type: 'Identifier', name: k },
179
+ value: createLiteralNode(v),
180
+ kind: 'init',
181
+ })),
182
+ };
183
+ }
184
+
185
+ module.exports = {
186
+ getJsConfigProperties,
187
+ editJsConfig,
188
+ };