diff --git a/apps/example/nitro.config.ts b/apps/example/nitro.config.ts index 527fd6f4..4352440b 100644 --- a/apps/example/nitro.config.ts +++ b/apps/example/nitro.config.ts @@ -6,7 +6,9 @@ export default defineConfig({ preset: "vercel", modules: [ juniorNitro({ - pluginPackages: examplePluginPackages, + plugins: { + packages: examplePluginPackages, + }, }), ], routes: { diff --git a/apps/example/server.ts b/apps/example/server.ts index cff16907..d9e5f548 100644 --- a/apps/example/server.ts +++ b/apps/example/server.ts @@ -5,7 +5,9 @@ import { examplePluginPackages } from "./plugin-packages"; initSentry(); const app = await createApp({ - pluginPackages: examplePluginPackages, + plugins: { + packages: examplePluginPackages, + }, configDefaults: { "sentry.org": "sentry", }, diff --git a/packages/junior/src/cli/check.ts b/packages/junior/src/cli/check.ts index 1a64f73a..dbdf82c3 100644 --- a/packages/junior/src/cli/check.ts +++ b/packages/junior/src/cli/check.ts @@ -555,6 +555,46 @@ interface AppFileValidationResult { warnings: string[]; } +async function validateAppSourceFiles( + rootDir: string, + registeredConfigKeys: Set, +): Promise { + const errors: string[] = []; + const warnings: string[] = []; + + for (const fileName of ["server.ts", "server.js", "nitro.config.ts"]) { + const sourcePath = path.join(rootDir, fileName); + let source: string; + try { + source = await fs.readFile(sourcePath, "utf8"); + } catch { + continue; + } + + if (/\bpluginPackages\s*:/.test(source)) { + errors.push( + `${sourcePath}: pluginPackages is no longer supported. Use plugins: { packages: [...] }.`, + ); + } + + for (const defaultsBlock of source.matchAll( + /\bconfigDefaults\s*:\s*\{([\s\S]*?)\}/g, + )) { + const block = defaultsBlock[1] ?? ""; + for (const keyMatch of block.matchAll(/["']([^"']+)["']\s*:/g)) { + const key = keyMatch[1]; + if (key && !registeredConfigKeys.has(key)) { + errors.push( + `${sourcePath}: configDefaults key "${key}" is not a registered plugin config key`, + ); + } + } + } + } + + return { errors, warnings }; +} + async function validateAppFiles( appDir: string, ): Promise { @@ -699,6 +739,16 @@ export async function runCheck( errors.push(...result.errors); } + const registeredConfigKeys = new Set( + pluginResults.flatMap((result) => result.manifest?.configKeys ?? []), + ); + const appSourceResult = await validateAppSourceFiles( + resolvedRoot, + registeredConfigKeys, + ); + warnings.push(...appSourceResult.warnings); + errors.push(...appSourceResult.errors); + for (const skillDir of appAndLocalPluginSkillDirs) { const result = await validateSkillDirectory(skillDir, duplicateSkillNames); skillResultsByDir.set(skillDir, result); diff --git a/packages/junior/tests/unit/cli/check-cli.test.ts b/packages/junior/tests/unit/cli/check-cli.test.ts index 89b230dc..1d88e3a9 100644 --- a/packages/junior/tests/unit/cli/check-cli.test.ts +++ b/packages/junior/tests/unit/cli/check-cli.test.ts @@ -183,6 +183,108 @@ describe("check cli", () => { ]); }); + it("fails when app source uses the removed pluginPackages option", async () => { + const repoRoot = makeTempDir("junior-validate-plugin-packages-option-"); + writeFile( + path.join(repoRoot, "server.ts"), + [ + 'import { createApp } from "@sentry/junior";', + "", + "export default await createApp({", + ' pluginPackages: ["@acme/junior-demo"],', + "});", + "", + ].join("\n"), + ); + + const lines: string[] = []; + await expect( + runCheck(repoRoot, { + info: (line) => lines.push(line), + warn: (line) => lines.push(line), + error: (line) => lines.push(line), + }), + ).rejects.toThrow( + "Validation failed (1 error, 0 plugin manifests, 0 skill directories checked).", + ); + + expect( + lines.some((line) => + line.includes( + "pluginPackages is no longer supported. Use plugins: { packages: [...] }.", + ), + ), + ).toBe(true); + }); + + it("fails when app configDefaults references an unregistered plugin key", async () => { + const repoRoot = makeTempDir("junior-validate-config-defaults-"); + writeFile( + path.join(repoRoot, "package.json"), + JSON.stringify( + { + dependencies: { + "@acme/junior-demo": "1.0.0", + }, + }, + null, + 2, + ), + ); + const packageRoot = path.join( + repoRoot, + "node_modules", + "@acme", + "junior-demo", + ); + writeFile( + path.join(packageRoot, "package.json"), + JSON.stringify({ name: "@acme/junior-demo", version: "1.0.0" }), + ); + writeFile( + path.join(packageRoot, "plugin.yaml"), + [ + "name: demo", + "description: Demo packaged plugin", + "config-keys:", + " - org", + "", + ].join("\n"), + ); + writeFile( + path.join(repoRoot, "server.ts"), + [ + 'import { createApp } from "@sentry/junior";', + "", + "export default await createApp({", + " configDefaults: {", + ' "sentry.org": "sentry",', + " },", + "});", + "", + ].join("\n"), + ); + + const lines: string[] = []; + await expect( + runCheck(repoRoot, { + info: (line) => lines.push(line), + warn: (line) => lines.push(line), + error: (line) => lines.push(line), + }), + ).rejects.toThrow( + "Validation failed (1 error, 1 plugin manifest, 0 skill directories checked).", + ); + + expect( + lines.some((line) => + line.includes( + 'configDefaults key "sentry.org" is not a registered plugin config key', + ), + ), + ).toBe(true); + }); + it("warns when official plugin package versions differ from core", async () => { const repoRoot = makeTempDir("junior-validate-version-skew-"); writeFile(