#!/usr/bin/env node
/**
* Resolve App Metadata — discover `bundle_name` / `app_name` from a HarmonyOS
* project and write `app-metadata.json`.
*
* Usage: resolve-metadata-tool.ts --project-dir
--output
* Reads `/AppScope/app.json5` (JSON5 with comments/trailing commas),
* extracts `app.bundleName` and `app.label` (resolving `$string:xxx` references
* from the base-qualifier string.json), writes `{bundle_name, app_name, project_root}`
* to `--output`, and emits the same JSON to stdout.
*/
import fs from 'node:fs';
import path from 'node:path';
// ---- JSON5 strip (remove comments + trailing commas, then JSON.parse) ----
function stripJson5(text: string): string {
// Remove single-line comments (// ...) — but not inside strings
let result = '';
let inString = false;
let stringChar = '';
for (let i = 0; i < text.length; i++) {
const ch = text[i];
const next = text[i + 1];
if (inString) {
result += ch;
if (ch === '\\' ) { result += next ?? ''; i++; continue; }
if (ch === stringChar) inString = false;
continue;
}
if (ch === '"' || ch === "'") { inString = true; stringChar = ch; result += ch; continue; }
if (ch === '/' && next === '/') { while (i < text.length && text[i] !== '\n') i++; continue; }
if (ch === '/' && next === '*') { i += 2; while (i < text.length - 1 && !(text[i] === '*' && text[i + 1] === '/')) i++; i++; continue; }
result += ch;
}
// Remove trailing commas before } or ]
result = result.replace(/,(\s*[}\]])/g, '$1');
return result;
}
function parseJson5(filePath: string): unknown {
const raw = fs.readFileSync(filePath, 'utf-8');
return JSON.parse(stripJson5(raw));
}
// ---- string resource resolver ----
function resolveStringResource(projectRoot: string, key: string): string | null {
const stringJson = path.join(projectRoot, 'AppScope', 'resources', 'base', 'element', 'string.json');
if (!fs.existsSync(stringJson)) return null;
let doc: unknown;
try { doc = parseJson5(stringJson); } catch { return null; }
const arr = (doc as { string?: unknown })?.string;
if (!Array.isArray(arr)) return null;
const entry = arr.find((e) => (e as { name?: unknown })?.name === key);
const value = (entry as { value?: unknown } | undefined)?.value;
return typeof value === 'string' && value ? value : null;
}
// ---- main ----
function main(): void {
const args = process.argv.slice(2);
let projectDir = '';
let output = '';
for (let i = 0; i < args.length; i++) {
if (args[i] === '--project-dir') projectDir = args[++i];
else if (args[i] === '--output') output = args[++i];
else if (args[i] === '-h' || args[i] === '--help') {
console.log('Usage: resolve-metadata-tool.ts --project-dir --output ');
process.exit(0);
}
}
if (!projectDir || !output) {
console.error('Usage: resolve-metadata-tool.ts --project-dir --output ');
process.exit(1);
}
const projectRoot = path.resolve(projectDir);
const appJson5 = path.join(projectRoot, 'AppScope', 'app.json5');
if (!fs.existsSync(appJson5)) {
console.error(`not a HarmonyOS project root (no AppScope/app.json5): ${projectRoot}`);
process.exit(1);
}
let doc: unknown;
try {
doc = parseJson5(appJson5);
} catch (e) {
console.error(`failed to parse ${appJson5}: ${e instanceof Error ? e.message : String(e)}`);
process.exit(1);
}
const app = (doc as { app?: Record })?.app;
const bundleName = app?.bundleName;
if (typeof bundleName !== 'string' || !bundleName) {
console.error(`missing app.bundleName in ${appJson5}`);
process.exit(1);
}
const label = app?.label;
let appName: string;
if (typeof label === 'string' && label.startsWith('$string:')) {
const key = label.slice('$string:'.length);
const resolved = resolveStringResource(projectRoot, key);
if (resolved) {
appName = resolved;
} else {
console.error(`warning: string resource '${key}' not found — using literal '${label}'`);
appName = label;
}
} else if (typeof label === 'string' && label) {
appName = label;
} else {
console.error(`warning: missing app.label — using bundleName as app_name`);
appName = bundleName;
}
const meta = { bundle_name: bundleName, app_name: appName, project_root: projectRoot };
fs.mkdirSync(path.dirname(path.resolve(output)), { recursive: true });
fs.writeFileSync(path.resolve(output), JSON.stringify(meta) + '\n', 'utf-8');
console.log(JSON.stringify(meta));
}
main();