120 lines
5.4 KiB
JavaScript
120 lines
5.4 KiB
JavaScript
import { mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
import { join } from 'node:path';
|
|
|
|
const componentsDir = process.env.HA_CORE_COMPONENTS_DIR || '/tmp/opencode/homeassistant-core/homeassistant/components';
|
|
const integrationsRoot = new URL('../ts/integrations/', import.meta.url);
|
|
const generatedRoot = new URL('../ts/integrations/generated/', import.meta.url);
|
|
const markerName = '.generated-by-smarthome-exchange';
|
|
|
|
const toClassName = (domain) => {
|
|
const parts = domain.split(/[^a-zA-Z0-9]+/).filter(Boolean);
|
|
const pascal = parts.map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`).join('') || 'Unknown';
|
|
return `HomeAssistant${pascal}Integration`;
|
|
};
|
|
|
|
const readManifest = async (componentDir, fallbackDomain) => {
|
|
try {
|
|
return JSON.parse(await readFile(join(componentDir, 'manifest.json'), 'utf8'));
|
|
} catch {
|
|
return { domain: fallbackDomain, name: fallbackDomain };
|
|
}
|
|
};
|
|
|
|
const isGeneratedFolder = async (folderUrl) => {
|
|
try {
|
|
await stat(new URL(`${markerName}`, folderUrl));
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const json = (value) => JSON.stringify(value, null, 2);
|
|
|
|
await mkdir(integrationsRoot, { recursive: true });
|
|
await mkdir(generatedRoot, { recursive: true });
|
|
|
|
for (const entry of await readdir(integrationsRoot, { withFileTypes: true })) {
|
|
if (!entry.isDirectory()) continue;
|
|
if (entry.name === 'generated') continue;
|
|
const folderUrl = new URL(`./${entry.name}/`, integrationsRoot);
|
|
if (await isGeneratedFolder(folderUrl)) {
|
|
await rm(folderUrl, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
const ports = [];
|
|
|
|
for (const entry of await readdir(componentsDir, { withFileTypes: true })) {
|
|
if (!entry.isDirectory()) continue;
|
|
if (entry.name.startsWith('__')) continue;
|
|
|
|
const componentDir = join(componentsDir, entry.name);
|
|
const manifest = await readManifest(componentDir, entry.name);
|
|
const domain = String(manifest.domain || entry.name);
|
|
const folderName = domain.replace(/[^a-z0-9_]/gi, '_').toLowerCase();
|
|
const folderUrl = new URL(`./${folderName}/`, integrationsRoot);
|
|
const className = toClassName(domain);
|
|
const metadata = {
|
|
source: 'home-assistant/core',
|
|
upstreamPath: `homeassistant/components/${entry.name}`,
|
|
upstreamDomain: domain,
|
|
integrationType: manifest.integration_type ? String(manifest.integration_type) : undefined,
|
|
iotClass: manifest.iot_class ? String(manifest.iot_class) : undefined,
|
|
qualityScale: manifest.quality_scale ? String(manifest.quality_scale) : undefined,
|
|
requirements: Array.isArray(manifest.requirements) ? manifest.requirements.map(String) : [],
|
|
dependencies: Array.isArray(manifest.dependencies) ? manifest.dependencies.map(String) : [],
|
|
afterDependencies: Array.isArray(manifest.after_dependencies) ? manifest.after_dependencies.map(String) : [],
|
|
codeowners: Array.isArray(manifest.codeowners) ? manifest.codeowners.map(String) : [],
|
|
};
|
|
|
|
let handwritten = false;
|
|
try {
|
|
await stat(folderUrl);
|
|
handwritten = !(await isGeneratedFolder(folderUrl));
|
|
} catch {}
|
|
|
|
if (!handwritten) {
|
|
await mkdir(folderUrl, { recursive: true });
|
|
await writeFile(new URL(markerName, folderUrl), 'This folder is generated from Home Assistant component metadata. Replace it with a handwritten TypeScript port when implementing runtime support.\n');
|
|
await writeFile(
|
|
new URL('index.ts', folderUrl),
|
|
`export * from './${folderName}.classes.integration.js';\nexport * from './${folderName}.types.js';\n`
|
|
);
|
|
await writeFile(
|
|
new URL(`${folderName}.types.ts`, folderUrl),
|
|
`export interface I${className.replace(/Integration$/, 'Config')} {\n // TODO: replace with the TypeScript-native config for ${domain}.\n [key: string]: unknown;\n}\n`
|
|
);
|
|
await writeFile(
|
|
new URL(`${folderName}.classes.integration.ts`, folderUrl),
|
|
`import { DescriptorOnlyIntegration } from '../../core/classes.descriptoronlyintegration.js';\n\nexport class ${className} extends DescriptorOnlyIntegration {\n constructor() {\n super({\n domain: ${JSON.stringify(domain)},\n displayName: ${JSON.stringify(manifest.name ? String(manifest.name) : domain)},\n status: 'descriptor-only',\n metadata: ${json(metadata)},\n });\n }\n}\n`
|
|
);
|
|
}
|
|
|
|
ports.push({
|
|
domain,
|
|
folderName,
|
|
className,
|
|
handwritten,
|
|
});
|
|
}
|
|
|
|
ports.sort((a, b) => a.domain.localeCompare(b.domain));
|
|
|
|
const imports = ports
|
|
.filter((port) => !port.handwritten)
|
|
.map((port) => `import { ${port.className} } from '../${port.folderName}/index.js';`)
|
|
.join('\n');
|
|
|
|
const constructorPushes = ports
|
|
.filter((port) => !port.handwritten)
|
|
.map((port) => `generatedHomeAssistantPortIntegrations.push(new ${port.className}());`)
|
|
.join('\n');
|
|
|
|
await writeFile(
|
|
new URL('index.ts', generatedRoot),
|
|
`// Generated by scripts/generate-homeassistant-ports.mjs. Do not edit manually.\n\nimport type { BaseIntegration } from '../../core/classes.baseintegration.js';\n${imports}\n\nexport const generatedHomeAssistantPortIntegrations: BaseIntegration[] = [];\n${constructorPushes}\n\nexport const generatedHomeAssistantPortCount = ${ports.filter((port) => !port.handwritten).length};\nexport const handwrittenHomeAssistantPortDomains = ${json(ports.filter((port) => port.handwritten).map((port) => port.domain))};\n`
|
|
);
|
|
|
|
console.log(`Generated ${ports.filter((port) => !port.handwritten).length} native TypeScript port skeletons. Preserved ${ports.filter((port) => port.handwritten).length} handwritten folders.`);
|