mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-19 00:01:14 +00:00
138 lines
5.2 KiB
JavaScript
138 lines
5.2 KiB
JavaScript
#!/usr/bin/env node
|
|
// Structural validator for the Warmbly Make.com Custom App.
|
|
// IML is interpreted by Make (no compiler), so this is the CI gate: it checks
|
|
// the manifest, JSON validity, code-file references, codeFiles keys, component
|
|
// enums, and that nothing is orphaned. Exits non-zero on any problem.
|
|
|
|
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
|
import { join, dirname, relative } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
const errors = [];
|
|
const err = (m) => errors.push(m);
|
|
|
|
// Allowed codeFiles keys per component type (Make Apps SDK ComponentCodeType).
|
|
const ALLOWED_KEYS = {
|
|
connection: ['communication', 'params', 'common', 'scopeList', 'defaultScope', 'installSpec', 'installDirectives'],
|
|
module: ['communication', 'epoch', 'staticParams', 'mappableParams', 'interface', 'samples', 'scope'],
|
|
rpc: ['communication', 'params'],
|
|
webhook: ['communication', 'params', 'attach', 'detach', 'update', 'requiredScope'],
|
|
function: ['code', 'test'],
|
|
};
|
|
const GENERAL_KEYS = ['base', 'common', 'readme', 'groups'];
|
|
const MODULE_TYPES = ['trigger', 'action', 'search', 'instant_trigger', 'responder', 'universal'];
|
|
const CRUD = ['create', 'read', 'update', 'delete'];
|
|
const CONNECTION_TYPES = ['basic', 'oauth', 'apikey', 'digest'];
|
|
|
|
const readJson = (rel) => {
|
|
const abs = join(ROOT, rel);
|
|
const text = readFileSync(abs, 'utf8');
|
|
return JSON.parse(text);
|
|
};
|
|
|
|
// 1. Manifest parses.
|
|
let manifest;
|
|
try {
|
|
manifest = readJson('makecomapp.json');
|
|
} catch (e) {
|
|
console.error('FATAL: makecomapp.json is missing or invalid JSON:', e.message);
|
|
process.exit(1);
|
|
}
|
|
|
|
const referenced = new Set(); // every code-file path the manifest points at
|
|
|
|
// 2. General code files.
|
|
for (const [key, path] of Object.entries(manifest.generalCodeFiles || {})) {
|
|
if (!GENERAL_KEYS.includes(key)) err(`generalCodeFiles: unknown key "${key}"`);
|
|
referenced.add(path);
|
|
if (!existsSync(join(ROOT, path))) err(`generalCodeFiles.${key}: missing file ${path}`);
|
|
}
|
|
|
|
// 3. Components.
|
|
const components = manifest.components || {};
|
|
for (const type of Object.keys(components)) {
|
|
if (!ALLOWED_KEYS[type]) {
|
|
err(`components: unknown component type "${type}"`);
|
|
continue;
|
|
}
|
|
for (const [name, def] of Object.entries(components[type] || {})) {
|
|
// enum checks
|
|
if (type === 'module') {
|
|
if (!MODULE_TYPES.includes(def.moduleType)) err(`module ${name}: invalid moduleType "${def.moduleType}"`);
|
|
if (def.moduleType === 'action' && def.actionCrud && !CRUD.includes(def.actionCrud)) {
|
|
err(`module ${name}: invalid actionCrud "${def.actionCrud}"`);
|
|
}
|
|
}
|
|
if (type === 'connection' && !CONNECTION_TYPES.includes(def.connectionType)) {
|
|
err(`connection ${name}: invalid connectionType "${def.connectionType}"`);
|
|
}
|
|
// codeFiles
|
|
const codeFiles = def.codeFiles || {};
|
|
if (!codeFiles.communication && type !== 'function') {
|
|
err(`${type} ${name}: missing required "communication" code file`);
|
|
}
|
|
if (type === 'function' && !codeFiles.code) {
|
|
err(`function ${name}: missing required "code" file`);
|
|
}
|
|
for (const [key, path] of Object.entries(codeFiles)) {
|
|
if (!ALLOWED_KEYS[type].includes(key)) err(`${type} ${name}: invalid codeFiles key "${key}"`);
|
|
referenced.add(path);
|
|
if (!existsSync(join(ROOT, path))) err(`${type} ${name}: missing file ${path} (key "${key}")`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. Every referenced .iml.json / .json parses; every .js parses.
|
|
for (const path of referenced) {
|
|
const abs = join(ROOT, path);
|
|
if (!existsSync(abs)) continue; // already reported
|
|
if (path.endsWith('.json')) {
|
|
try {
|
|
JSON.parse(readFileSync(abs, 'utf8'));
|
|
} catch (e) {
|
|
err(`invalid JSON: ${path} (${e.message})`);
|
|
}
|
|
} else if (path.endsWith('.js')) {
|
|
try {
|
|
// Function-body parse check; Make runs these in its own sandbox.
|
|
new Function(readFileSync(abs, 'utf8'));
|
|
} catch (e) {
|
|
err(`invalid JS: ${path} (${e.message})`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 5. No orphaned files in component directories.
|
|
const COMPONENT_DIRS = ['general', 'connections', 'rpcs', 'modules', 'functions'];
|
|
const walk = (dir) => {
|
|
const abs = join(ROOT, dir);
|
|
if (!existsSync(abs)) return [];
|
|
return readdirSync(abs).flatMap((entry) => {
|
|
const full = join(abs, entry);
|
|
return statSync(full).isDirectory() ? walk(relative(ROOT, full)) : [relative(ROOT, full)];
|
|
});
|
|
};
|
|
for (const dir of COMPONENT_DIRS) {
|
|
for (const file of walk(dir)) {
|
|
if (!referenced.has(file)) err(`orphaned file (not referenced by makecomapp.json): ${file}`);
|
|
}
|
|
}
|
|
|
|
// Report.
|
|
if (errors.length) {
|
|
console.error(`Make app validation FAILED with ${errors.length} problem(s):`);
|
|
for (const e of errors) console.error(' - ' + e);
|
|
process.exit(1);
|
|
}
|
|
const counts = {
|
|
modules: Object.keys(components.module || {}).length,
|
|
rpcs: Object.keys(components.rpc || {}).length,
|
|
connections: Object.keys(components.connection || {}).length,
|
|
functions: Object.keys(components.function || {}).length,
|
|
};
|
|
console.log(
|
|
`Make app OK: ${counts.modules} modules, ${counts.rpcs} RPCs, ${counts.connections} connection(s), ` +
|
|
`${counts.functions} function(s); ${referenced.size} code files referenced and present.`
|
|
);
|