@cloudquery/plugin-config-ui-lib
Version:
Plugin configuration UI library for CloudQuery Cloud App
485 lines (420 loc) • 15.9 kB
JavaScript
#!/usr/bin/env node
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import Handlebars from 'handlebars';
import humanizeString from 'humanize-string';
import inquirer from 'inquirer';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
let pluginLogoPathNotProvided = false;
async function main() {
const outputDir = path.join(process.cwd(), 'cloud-config-ui');
try {
const { pluginKind, pluginLabel, pluginLogoPath, pluginName, pluginTeam, pluginVersion } =
await inquirer.prompt([
{
type: 'input',
name: 'pluginLabel',
message: 'What is the label of the plugin (e.g. "AWS", "GitHub", "Zendesk")?',
required: true,
},
{
type: 'input',
name: 'pluginName',
message: 'What is the name of the plugin (e.g. "aws", "github", "zendesk")?',
required: true,
validate: (input) => /^[a-z](-?[\da-z]+)+$/.test(input),
},
{
type: 'select',
name: 'pluginKind',
message: 'What is the kind of the plugin?',
choices: ['source', 'destination'],
required: true,
},
{
type: 'input',
name: 'pluginTeam',
message: 'What is the team name of the plugin (e.g. "cloudquery")?',
required: true,
default: 'cloudquery',
},
{
type: 'input',
name: 'pluginVersion',
message: 'What is the latest version of the plugin (e.g. "v1.0.0")?',
required: true,
validate: (input) =>
/^v\d+\.\d+\.\d+(-[\dA-Za-z-]+(\.[\dA-Za-z-]+)*)?(\+[\dA-Za-z-]+(\.[\dA-Za-z-]+)*)?$/.test(
input,
),
},
{
type: 'input',
name: 'pluginLogoPath',
message: 'Provide the path to the plugin logo (optional):',
validate: (input) => {
if (!input) {
return true;
}
let logoSrcPath = input;
if (!path.isAbsolute(logoSrcPath)) {
logoSrcPath = path.resolve(process.cwd(), logoSrcPath);
}
if (!fs.existsSync(logoSrcPath)) {
return 'Logo file was not found. Please provide a valid path.';
}
return true;
},
transformer: (input, { isFinal }) => {
if (!isFinal) {
return input;
} else if (!input) {
return '';
}
let logoSrcPath = input;
if (!path.isAbsolute(logoSrcPath)) {
logoSrcPath = path.resolve(process.cwd(), logoSrcPath);
}
return logoSrcPath;
},
},
]);
pluginLogoPathNotProvided = !pluginLogoPath;
let createTablesSelector = false;
if (pluginKind === 'source') {
({ createTablesSelector } = await inquirer.prompt({
type: 'confirm',
name: 'createTablesSelector',
message: 'Does the plugin support table selection?',
required: true,
}));
}
let createServicesSelector = false;
let topServices = '';
let slowTables = '';
let expensiveTables = '';
if (pluginKind === 'source') {
({ createServicesSelector } = await inquirer.prompt({
type: 'confirm',
name: 'createServicesSelector',
message: 'Does the plugin support service selection?',
required: true,
}));
if (createServicesSelector) {
({ topServices } = await inquirer.prompt({
type: 'input',
name: 'topServices',
message: 'Provide the list of top services (comma separated, e.g. "service1,service2"):',
}));
({ slowTables } = await inquirer.prompt({
type: 'input',
name: 'slowTables',
message: 'Provide the list of slow tables (comma separated, e.g. "table1,table2"):',
}));
({ expensiveTables } = await inquirer.prompt({
type: 'input',
name: 'expensiveTables',
message: 'Provide the list of expensive tables (comma separated, e.g. "table1,table2"):',
}));
}
}
const { authentication } = await inquirer.prompt({
type: 'select',
name: 'authentication',
message: 'What is the authentication type of the plugin?',
choices: ['oauth', 'token', 'both'],
required: true,
});
let authTokenSpecProperties = [];
if (['token', 'both'].includes(authentication)) {
const { specProperties } = await inquirer.prompt([
{
type: 'input',
name: 'specProperties',
message:
'Provide the list of spec properties to be used for the authentication (comma separated, e.g. "access_key,secret_key"):',
required: true,
},
]);
authTokenSpecProperties = specProperties.split(',').map((property) => {
const name = property.trim();
return {
name,
label: humanizeString(name),
};
});
}
// Advanced options collection
const advancedOptions = [];
const { includeAdvancedOptions } = await inquirer.prompt({
type: 'confirm',
name: 'includeAdvancedOptions',
message: 'Would you like to include any advanced options?',
default: false,
});
let addMoreOptions = includeAdvancedOptions;
while (addMoreOptions) {
const { name } = await inquirer.prompt({
type: 'input',
name: 'name',
message: 'Enter the name of the advanced option (e.g. max_requests_per_second):',
required: true,
validate: (input) => input.trim().length > 0 || 'Name is required',
});
const { label } = await inquirer.prompt({
type: 'input',
name: 'label',
message: 'Enter the label for this option:',
default: humanizeString(name),
});
const { type } = await inquirer.prompt({
type: 'list',
name: 'type',
message: 'Select the data type for this option:',
choices: ['string', 'number', 'boolean'],
required: true,
});
let numberConstraints = {};
if (type === 'number') {
const { isPositive } = await inquirer.prompt({
type: 'confirm',
name: 'isPositive',
message: 'Should this number be positive only?',
default: false,
});
const { isInteger } = await inquirer.prompt({
type: 'confirm',
name: 'isInteger',
message: 'Should this number be an integer?',
default: false,
});
numberConstraints = { isPositive, isInteger };
}
const { required } = await inquirer.prompt({
type: 'confirm',
name: 'required',
message: 'Is this option required?',
default: false,
});
const { value } = await inquirer.prompt({
type: type === 'boolean' ? 'confirm' : 'input',
name: 'value',
message: 'Enter the default value:',
validate: (input) => {
if (type === 'number') {
const num = Number(input);
if (isNaN(num)) return 'Must be a valid number';
if (numberConstraints.isPositive && num <= 0) return 'Must be positive';
if (numberConstraints.isInteger && !Number.isInteger(num)) return 'Must be an integer';
}
return true;
},
transformer: (input) => {
if (type === 'number') return Number(input);
if (type === 'boolean') return input === 'true';
return input;
},
});
advancedOptions.push({
name,
label,
type,
required,
default: type === 'number' ? Number(value) : type === 'boolean' ? value === 'true' : `'${value}'`,
...(type === 'number' ? numberConstraints : {}),
});
const { addAnother } = await inquirer.prompt({
type: 'confirm',
name: 'addAnother',
message: 'Would you like to add another advanced option?',
default: false,
});
addMoreOptions = addAnother;
}
const packageJson = JSON.parse(
fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf8'),
);
const payload = {
pluginName,
pluginKind,
pluginTeam,
pluginLabel,
pluginVersion,
pluginKindLabel: pluginKind === 'source' ? 'integrations' : 'destinations',
authenticationOAuth: authentication === 'oauth' || authentication === 'both',
authenticationToken: authentication === 'token' || authentication === 'both',
authenticationBoth: authentication === 'both',
authentication:
authentication === 'both'
? '[AuthType.OAUTH, AuthType.OTHER]'
: authentication === 'oauth'
? '[AuthType.OAUTH]'
: '[AuthType.OTHER]',
createTablesSelector,
createServicesSelector,
topServices: topServices.split(',').map((service) => `'${service.trim()}'`).join(', '),
slowTables: slowTables.split(',').map((table) => `'${table.trim()}'`).join(', '),
expensiveTables: expensiveTables.split(',').map((table) => `'${table.trim()}'`).join(', '),
advancedOptions: advancedOptions.length > 0 ? advancedOptions : undefined,
authTokenSpecProperties,
cloudQueryPluginConfigUiLibVersion: packageJson.version,
yup:
authentication === 'both' ||
authentication === 'token' ||
advancedOptions.length > 0 ||
advancedOptions.length > 0,
};
if (fs.existsSync(outputDir)) {
throw new Error('cloud-config-ui directory already exists.');
}
fs.mkdirSync(outputDir);
const templateDir = path.join(__dirname, '..', 'template');
// Copy and compile index.html
createAndCompileTemplate(
path.join(templateDir, 'index.html.hbs'),
path.join(outputDir, 'index.html'),
payload,
);
// Copy template/public folder
const publicSrcDir = path.join(templateDir, 'public');
const publicDestDir = path.join(outputDir, 'public');
fs.cpSync(publicSrcDir, publicDestDir, { recursive: true });
// Copy and compile public/manifest.json
createAndCompileTemplate(
path.join(templateDir, 'public', 'manifest.json.hbs'),
path.join(outputDir, 'public', 'manifest.json'),
payload,
);
// Copy logo
if (pluginLogoPath) {
let logoSrcPath = pluginLogoPath;
if (!path.isAbsolute(logoSrcPath)) {
logoSrcPath = path.resolve(process.cwd(), logoSrcPath);
}
const logoDestPath = path.join(outputDir, 'public', 'images', 'logo.png');
fs.mkdirSync(path.dirname(logoDestPath), { recursive: true });
fs.copyFileSync(logoSrcPath, logoDestPath);
}
// Copy scripts if plugin is a source
if (pluginKind === 'source') {
createAndCompileTemplate(
path.join(templateDir, 'scripts', 'initialize.js.hbs'),
path.join(outputDir, 'scripts', 'initialize.js'),
payload,
);
}
// Copy and compile src/hooks/useConfig.tsx
createAndCompileTemplate(
path.join(templateDir, 'src', 'hooks', 'useConfig.tsx.hbs'),
path.join(outputDir, 'src', 'hooks', 'useConfig.tsx'),
payload,
);
// Copy src/utils
const utilsSrcDir = path.join(templateDir, 'src', 'utils');
const utilsDestDir = path.join(outputDir, 'src', 'utils');
fs.cpSync(utilsSrcDir, utilsDestDir, { recursive: true });
// Copy and compile src/tests
createAndCompileTemplate(
path.join(templateDir, 'src', 'tests', 'setupTests.ts.hbs'),
path.join(outputDir, 'src', 'tests', 'setupTests.ts'),
payload,
);
const testsSrcPath = path.join(templateDir, 'src', 'tests', 'create.test.tsx');
const testsDestPath = path.join(outputDir, 'src', 'tests', 'create.test.tsx');
fs.copyFileSync(testsSrcPath, testsDestPath);
// Copy and compile src/App.tsx
createAndCompileTemplate(
path.join(templateDir, 'src', 'App.tsx.hbs'),
path.join(outputDir, 'src', 'App.tsx'),
payload,
);
// Copy src/index.tsx
const indexSrcPath = path.join(templateDir, 'src', 'index.tsx');
const indexDestPath = path.join(outputDir, 'src', 'index.tsx');
fs.copyFileSync(indexSrcPath, indexDestPath);
// Copy and compile .env
createAndCompileTemplate(
path.join(templateDir, '.env.example.hbs'),
path.join(outputDir, '.env.example'),
payload,
);
fs.copyFileSync(path.join(outputDir, '.env.example'), path.join(outputDir, '.env'));
// Copy .eslintrc.cjs
const eslintSrcPath = path.join(templateDir, '.eslintrc.cjs');
const eslintDestPath = path.join(outputDir, '.eslintrc.cjs');
fs.copyFileSync(eslintSrcPath, eslintDestPath);
// Copy .gitignore
const gitignoreSrcPath = path.join(templateDir, '_gitignore');
const gitignoreDestPath = path.join(outputDir, '.gitignore');
fs.copyFileSync(gitignoreSrcPath, gitignoreDestPath);
// Copy .nvmrc
const nvmrcSrcPath = path.join(templateDir, '.nvmrc');
const nvmrcDestPath = path.join(outputDir, '.nvmrc');
fs.copyFileSync(nvmrcSrcPath, nvmrcDestPath);
// Copy .prettierrc.cjs
const prettierrcSrcPath = path.join(templateDir, '.prettierrc.cjs');
const prettierrcDestPath = path.join(outputDir, '.prettierrc.cjs');
fs.copyFileSync(prettierrcSrcPath, prettierrcDestPath);
// Copy and compile package.json
createAndCompileTemplate(
path.join(templateDir, 'package.json.hbs'),
path.join(outputDir, 'package.json'),
payload,
);
// Copy and compile README.md
createAndCompileTemplate(
path.join(templateDir, 'README.md.hbs'),
path.join(outputDir, 'README.md'),
payload,
);
// Copy tsconfig.json
const tsconfigSrcPath = path.join(templateDir, 'tsconfig.json.hbs');
const tsconfigDestPath = path.join(outputDir, 'tsconfig.json');
fs.copyFileSync(tsconfigSrcPath, tsconfigDestPath);
// Copy vite.config.js
const viteConfigSrcPath = path.join(templateDir, 'vite.config.js');
const viteConfigDestPath = path.join(outputDir, 'vite.config.js');
fs.copyFileSync(viteConfigSrcPath, viteConfigDestPath);
} catch (error) {
if (fs.existsSync(outputDir)) {
fs.rmdirSync(outputDir, { recursive: true });
}
if (error.name === 'ExitPromptError') {
return;
}
throw error;
}
}
function createAndCompileTemplate(templatePath, outputPath, data) {
const template = fs.readFileSync(templatePath, 'utf8');
const compiledTemplate = Handlebars.compile(template);
const outputDir = path.dirname(outputPath);
fs.mkdirSync(outputDir, { recursive: true });
fs.writeFileSync(outputPath, compiledTemplate(data));
}
await main();
// eslint-disable-next-line no-console
console.log('Generating cloud config UI completed. Installing dependencies...');
try {
const cloudConfigUiDir = path.join(process.cwd(), 'cloud-config-ui');
process.chdir(cloudConfigUiDir);
execSync('npm install', { stdio: 'inherit' });
// eslint-disable-next-line no-console
console.log('\n\nDependencies installed successfully.');
if (pluginLogoPathNotProvided) {
// eslint-disable-next-line no-console
console.warn(
'No logo path provided. Please remember to add a logo to your plugin to the public/images/logo.png file.',
);
}
process.chdir('..');
// eslint-disable-next-line no-console
console.log('You can now navigate into the cloud-config-ui directory and start developing.');
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error installing dependencies:', error.message);
process.exit(1);
}