diff --git a/packages/appkit/src/type-generator/mv-registry/config.ts b/packages/appkit/src/type-generator/mv-registry/config.ts index a204d3317..6a8b109e3 100644 --- a/packages/appkit/src/type-generator/mv-registry/config.ts +++ b/packages/appkit/src/type-generator/mv-registry/config.ts @@ -46,11 +46,17 @@ function compareKeys(a: string, b: string): number { * Read {@link METRIC_CONFIG_FILE} from a metric-views folder * (`config/metric-views/`). * - * Returns `null` if the file does not exist (the metric-view path is - * additive — apps without definitions.json must not be penalized). There is - * deliberately no fallback to a legacy filename. + * Returns `null` if the file does not exist, is empty, or has not been rendered + * yet (the metric-view path is additive — apps without a defined + * definitions.json must not be penalized). The template ships definitions.json + * wrapped in a `{{if .plugins.analytics}}` guard, so scaffolding without the + * analytics plugin leaves the file empty, and running typegen against the raw + * (un-rendered) template leaves the literal `{{...}}` in place — neither is a + * misconfiguration, so both are treated the same as an absent file rather than + * a fatal parse error. There is deliberately no fallback to a legacy filename. * - * Throws on JSON parse errors so misconfiguration surfaces loudly. + * Throws on JSON parse errors in a *non-empty, rendered* file so genuine + * misconfiguration still surfaces loudly. */ export async function readMetricConfig( metricViewsFolder: string, @@ -66,6 +72,14 @@ export async function readMetricConfig( throw err; } + // An empty file (analytics plugin off → the `{{if}}` guard renders nothing) + // or an un-rendered template (raw `{{...}}` left in place during typegen on + // the template itself) means "no metric views" — treat like an absent file. + const trimmed = raw.trim(); + if (trimmed === "" || trimmed.startsWith("{{")) { + return null; + } + let parsed: unknown; try { parsed = JSON.parse(raw); diff --git a/packages/appkit/src/type-generator/tests/mv-registry.test.ts b/packages/appkit/src/type-generator/tests/mv-registry.test.ts index 6fa77693e..ff9484b23 100644 --- a/packages/appkit/src/type-generator/tests/mv-registry.test.ts +++ b/packages/appkit/src/type-generator/tests/mv-registry.test.ts @@ -94,6 +94,29 @@ describe("readMetricConfig", () => { expect(await readMetricConfig(tmpDir)).toBeNull(); }); + test("returns null for an empty definitions.json (analytics plugin off)", async () => { + // The template wraps definitions.json in `{{if .plugins.analytics}}`, so + // scaffolding without the analytics plugin renders an empty file. That's + // "no metric views", not a parse error. + await fs.writeFile(path.join(tmpDir, "definitions.json"), ""); + expect(await readMetricConfig(tmpDir)).toBeNull(); + }); + + test("returns null for a whitespace-only definitions.json", async () => { + await fs.writeFile(path.join(tmpDir, "definitions.json"), "\n \n"); + expect(await readMetricConfig(tmpDir)).toBeNull(); + }); + + test("returns null for an un-rendered template definitions.json", async () => { + // Running typegen against the raw template (e.g. npm-install warmup before + // `appkit setup`) leaves the Go-template guard literal in place. + await fs.writeFile( + path.join(tmpDir, "definitions.json"), + '{{if .plugins.analytics -}}\n{ "metricViews": {} }\n{{- end}}\n', + ); + expect(await readMetricConfig(tmpDir)).toBeNull(); + }); + test("parses a valid definitions.json", async () => { await fs.writeFile( path.join(tmpDir, "definitions.json"),