Skip to content

Commit

Permalink
Merge pull request #322 from friggframework/FRI-418_Explore-ability-t…
Browse files Browse the repository at this point in the history
…o-have-a-CLI-as-part-of-devtools-package

CLI for Frigg - Install command for now
  • Loading branch information
seanspeaks authored Aug 6, 2024
2 parents 5e921ed + deef4e0 commit b0f53c5
Show file tree
Hide file tree
Showing 15 changed files with 1,095 additions and 79 deletions.
463 changes: 385 additions & 78 deletions package-lock.json

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions packages/devtools/frigg-cli/backendJs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const fs = require('fs-extra');
const path = require('path');
const { logInfo } = require('./logger');
const INTEGRATIONS_DIR = 'src/integrations';
const BACKEND_JS = 'backend.js';

function updateBackendJsFile(backendPath, apiModuleName) {
const backendJsPath = path.join(path.dirname(backendPath), BACKEND_JS);
logInfo(`Updating backend.js: ${backendJsPath}`);
updateBackendJs(backendJsPath, apiModuleName);
}

function updateBackendJs(backendJsPath, apiModuleName) {
const backendJsContent = fs.readFileSync(backendJsPath, 'utf-8');
const importStatement = `const ${apiModuleName}Integration = require('./${INTEGRATIONS_DIR}/${apiModuleName}Integration');\n`;

if (!backendJsContent.includes(importStatement)) {
const updatedContent = backendJsContent.replace(
/(integrations\s*:\s*\[)([\s\S]*?)(\])/,
`$1\n ${apiModuleName}Integration,$2$3`
);
fs.writeFileSync(backendJsPath, importStatement + updatedContent);
} else {
logInfo(
`Import statement for ${apiModuleName}Integration already exists in backend.js`
);
}
}

module.exports = {
updateBackendJsFile,
updateBackendJs,
};
26 changes: 26 additions & 0 deletions packages/devtools/frigg-cli/backendPath.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const fs = require('fs-extra');
const path = require('path');
const PACKAGE_JSON = 'package.json';

function findNearestBackendPackageJson() {
let currentDir = process.cwd();
while (currentDir !== path.parse(currentDir).root) {
const packageJsonPath = path.join(currentDir, 'backend', PACKAGE_JSON);
if (fs.existsSync(packageJsonPath)) {
return packageJsonPath;
}
currentDir = path.dirname(currentDir);
}
return null;
}

function validateBackendPath(backendPath) {
if (!backendPath) {
throw new Error('Could not find a backend package.json file.');
}
}

module.exports = {
findNearestBackendPackageJson,
validateBackendPath,
};
16 changes: 16 additions & 0 deletions packages/devtools/frigg-cli/commitChanges.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const { execSync } = require('child_process');
const path = require('path');

function commitChanges(backendPath, apiModuleName) {
const apiModulePath = path.join(path.dirname(backendPath), 'src', 'integrations', `${apiModuleName}Integration.js`);
try {
execSync(`git add ${apiModulePath}`);
execSync(`git commit -m "Add ${apiModuleName}Integration to ${apiModuleName}Integration.js"`);
} catch (error) {
throw new Error('Failed to commit changes:', error);
}
}

module.exports = {
commitChanges,
};
134 changes: 134 additions & 0 deletions packages/devtools/frigg-cli/environmentVariables.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
const fs = require('fs');
const dotenv = require('dotenv');
const { readFileSync, writeFileSync, existsSync } = require('fs');
const { logInfo } = require('./logger');
const { resolve } = require('node:path');
const inquirer = require('inquirer');

const { parse } = require('@babel/parser');
const traverse = require('@babel/traverse').default;

const extractRawEnvVariables = (modulePath) => {
const filePath = resolve(modulePath, 'definition.js');

const fileContent = fs.readFileSync(filePath, 'utf-8');
const ast = parse(fileContent, {
sourceType: 'module',
plugins: ['jsx', 'typescript'], // Add more plugins if needed
});

const envVariables = {};

traverse(ast, {
ObjectProperty(path) {
if (path.node.key.name === 'env') {
path.node.value.properties.forEach((prop) => {
const key = prop.key.name;
if (prop.value.type === 'MemberExpression') {
const property = prop.value.property.name;
envVariables[key] = `${property}`;
} else if (prop.value.type === 'TemplateLiteral') {
// Handle template literals
const expressions = prop.value.expressions.map((exp) =>
exp.type === 'MemberExpression'
? `${exp.property.name}`
: exp.name
);
envVariables[key] = expressions.join('');
}
});
}
},
});

return envVariables;
};
const handleEnvVariables = async (backendPath, modulePath) => {
logInfo('Searching for missing environment variables...');
const Definition = { env: extractRawEnvVariables(modulePath) };
if (Definition && Definition.env) {
console.log('Here is Definition.env:', Definition.env);
const envVars = Object.values(Definition.env);

console.log(
'Found the following environment variables in the API module:',
envVars
);

const localEnvPath = resolve(backendPath, '../.env');
const localDevConfigPath = resolve(
backendPath,
'../src/configs/dev.json'
);

// Load local .env variables
let localEnvVars = {};
if (existsSync(localEnvPath)) {
localEnvVars = dotenv.parse(readFileSync(localEnvPath, 'utf8'));
}

// Load local dev.json variables
let localDevConfig = {};
if (existsSync(localDevConfigPath)) {
localDevConfig = JSON.parse(
readFileSync(localDevConfigPath, 'utf8')
);
}

const missingEnvVars = envVars.filter(
(envVar) => !localEnvVars[envVar] && !localDevConfig[envVar]
);

logInfo(`Missing environment variables: ${missingEnvVars.join(', ')}`);

if (missingEnvVars.length > 0) {
const { addEnvVars } = await inquirer.prompt([
{
type: 'confirm',
name: 'addEnvVars',
message: `The following environment variables are required: ${missingEnvVars.join(
', '
)}. Do you want to add them now?`,
},
]);

if (addEnvVars) {
const envValues = {};
for (const envVar of missingEnvVars) {
const { value } = await inquirer.prompt([
{
type: 'input',
name: 'value',
message: `Enter value for ${envVar}:`,
},
]);
envValues[envVar] = value;
}

// Add the envValues to the local .env file if it exists
if (existsSync(localEnvPath)) {
const envContent = Object.entries(envValues)
.map(([key, value]) => `${key}=${value}`)
.join('\n');
fs.appendFileSync(localEnvPath, `\n${envContent}`);
}

// Add the envValues to the local dev.json file if it exists
if (existsSync(localDevConfigPath)) {
const updatedDevConfig = {
...localDevConfig,
...envValues,
};
writeFileSync(
localDevConfigPath,
JSON.stringify(updatedDevConfig, null, 2)
);
}
} else {
logInfo("Edit whenever you're able, safe travels friend!");
}
}
}
};

module.exports = { handleEnvVariables };
86 changes: 86 additions & 0 deletions packages/devtools/frigg-cli/environmentVariables.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const { handleEnvVariables } = require('./environmentVariables');
const { logInfo } = require('./logger');
const inquirer = require('inquirer');
const fs = require('fs');
const dotenv = require('dotenv');
const { resolve } = require('node:path');
const { parse } = require('@babel/parser');
const traverse = require('@babel/traverse');

jest.mock('inquirer');
jest.mock('fs');
jest.mock('dotenv');
jest.mock('./logger');
jest.mock('@babel/parser');
jest.mock('@babel/traverse');

describe('handleEnvVariables', () => {
const backendPath = '/mock/backend/path';
const modulePath = '/mock/module/path';

beforeEach(() => {
jest.clearAllMocks();
fs.readFileSync.mockReturnValue(`
const Definition = {
env: {
client_id: process.env.GOOGLE_CALENDAR_CLIENT_ID,
client_secret: process.env.GOOGLE_CALENDAR_CLIENT_SECRET,
redirect_uri: \`\${process.env.REDIRECT_URI}/google-calendar\`,
scope: process.env.GOOGLE_CALENDAR_SCOPE,
}
};
`);
parse.mockReturnValue({});
traverse.default.mockImplementation((ast, visitor) => {
visitor.ObjectProperty({
node: {
key: { name: 'env' },
value: {
properties: [
{ key: { name: 'client_id' }, value: { type: 'MemberExpression', object: { name: 'process' }, property: { name: 'GOOGLE_CALENDAR_CLIENT_ID' } } },
{ key: { name: 'client_secret' }, value: { type: 'MemberExpression', object: { name: 'process' }, property: { name: 'GOOGLE_CALENDAR_CLIENT_SECRET' } } },
{ key: { name: 'redirect_uri' }, value: { type: 'MemberExpression', object: { name: 'process' }, property: { name: 'REDIRECT_URI' } } },
{ key: { name: 'scope' }, value: { type: 'MemberExpression', object: { name: 'process' }, property: { name: 'GOOGLE_CALENDAR_SCOPE' } } },
]
}
}
});
});
});

it('should identify and handle missing environment variables', async () => {
const localEnvPath = resolve(backendPath, '../.env');
const localDevConfigPath = resolve(backendPath, '../src/configs/dev.json');

fs.existsSync.mockImplementation((path) => path === localEnvPath || path === localDevConfigPath);
dotenv.parse.mockReturnValue({});
fs.readFileSync.mockImplementation((path) => {
if (path === resolve(modulePath, 'index.js')) return 'mock module content';
if (path === localEnvPath) return '';
if (path === localDevConfigPath) return '{}';
return '';
});

inquirer.prompt.mockResolvedValueOnce({ addEnvVars: true })
.mockResolvedValueOnce({ value: 'client_id_value' })
.mockResolvedValueOnce({ value: 'client_secret_value' })
.mockResolvedValueOnce({ value: 'redirect_uri_value' })
.mockResolvedValueOnce({ value: 'scope_value' });

await handleEnvVariables(backendPath, modulePath);

expect(logInfo).toHaveBeenCalledWith('Searching for missing environment variables...');
expect(logInfo).toHaveBeenCalledWith('Missing environment variables: GOOGLE_CALENDAR_CLIENT_ID, GOOGLE_CALENDAR_CLIENT_SECRET, REDIRECT_URI, GOOGLE_CALENDAR_SCOPE');
expect(inquirer.prompt).toHaveBeenCalledTimes(5);
expect(fs.appendFileSync).toHaveBeenCalledWith(localEnvPath, '\nGOOGLE_CALENDAR_CLIENT_ID=client_id_value\nGOOGLE_CALENDAR_CLIENT_SECRET=client_secret_value\nREDIRECT_URI=redirect_uri_value\nGOOGLE_CALENDAR_SCOPE=scope_value');
expect(fs.writeFileSync).toHaveBeenCalledWith(
localDevConfigPath,
JSON.stringify({
GOOGLE_CALENDAR_CLIENT_ID: 'client_id_value',
GOOGLE_CALENDAR_CLIENT_SECRET: 'client_secret_value',
REDIRECT_URI: 'redirect_uri_value',
GOOGLE_CALENDAR_SCOPE: 'scope_value'
}, null, 2)
);
});
});
14 changes: 14 additions & 0 deletions packages/devtools/frigg-cli/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env node

const { Command } = require('commander');
const { installCommand } = require('./installCommand');

const program = new Command();
program
.command('install [apiModuleName]')
.description('Install an API module')
.action(installCommand);

program.parse(process.argv);

module.exports = { installCommand };
Loading

0 comments on commit b0f53c5

Please sign in to comment.