Skip to content

Commit 2d17ea5

Browse files
committed
refactor(create-plugin): move app-sdk addition off direct output
1 parent e35423a commit 2d17ea5

2 files changed

Lines changed: 33 additions & 44 deletions

File tree

packages/create-plugin/src/codemods/additions/scripts/experimental-app-sdk.test.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Context } from '../../context.js';
2-
import { output } from '../../../utils/utils.console.js';
32
import appSdk from './experimental-app-sdk.js';
43

54
vi.mock(import('../../../utils/utils.plugin.js'), async (importOriginal) => {
@@ -10,15 +9,11 @@ vi.mock(import('../../../utils/utils.plugin.js'), async (importOriginal) => {
109
};
1110
});
1211

13-
1412
vi.mock(import('../../utils.js'), async (importOriginal) => {
1513
const originalModule = await importOriginal();
1614
// Disk I/O is slow so render the templates once and key off the requested path.
1715
const render = (file: string) =>
18-
originalModule.renderTemplate(
19-
new URL(`../../../../templates/app-sdk/${file}`, import.meta.url).pathname,
20-
false
21-
);
16+
originalModule.renderTemplate(new URL(`../../../../templates/app-sdk/${file}`, import.meta.url).pathname, false);
2217
const rendered: Record<string, string> = {
2318
'.config/app-sdk/generate-kinds.mjs': render('.config/app-sdk/generate-kinds.mjs'),
2419
'.config/AGENTS/app-sdk.md': render('.config/AGENTS/app-sdk.md'),
@@ -78,11 +73,7 @@ function createAppContext({
7873
}
7974

8075
describe('experimental-app-sdk addition', () => {
81-
// Silence terminal output, and let us assert on what the user is told.
82-
beforeEach(() => {
83-
vi.spyOn(output, 'log').mockImplementation(() => {});
84-
vi.spyOn(output, 'warning').mockImplementation(() => {});
85-
});
76+
// no output spies needed - the codemod prints nothing, it records on the context
8677

8778
afterEach(() => {
8879
vi.restoreAllMocks();
@@ -304,35 +295,44 @@ describe('experimental-app-sdk addition', () => {
304295
});
305296
});
306297

298+
// the codemod records what to say; the command renders it. see Context.addNextStep / Context.skip
307299
describe('user messaging', () => {
308300
it('explains why it skipped an unsupported plugin type', () => {
309301
const context = createAppContext({ pluginType: 'panel' });
310302

311303
appSdk(context);
312304

313-
expect(output.warning).toHaveBeenCalledWith(
314-
expect.objectContaining({ title: expect.stringContaining('needs an app plugin') })
315-
);
305+
expect(context.getSkip()?.reason).toContain('needs an app plugin');
316306
});
317307

318-
it('prints next steps after scaffolding', () => {
308+
it('explains a missing plugin.json rather than failing silently', () => {
309+
const context = new Context();
310+
311+
appSdk(context);
312+
313+
expect(context.getSkip()?.reason).toContain('src/plugin.json');
314+
expect(context.getSkip()?.hints).toContain('Run this from the root of your plugin.');
315+
});
316+
317+
it('records next steps after scaffolding', () => {
319318
const context = createAppContext();
320319

321320
appSdk(context);
322321

323-
expect(output.log).toHaveBeenCalledWith(expect.objectContaining({ title: expect.stringContaining('Next steps') }));
322+
expect(context.getSkip()).toBeUndefined();
323+
expect(context.listNextSteps().join('\n')).toContain('generate:kinds');
324324
});
325325

326326
it('stays quiet on a re-run', () => {
327327
const context = createAppContext();
328328
appSdk(context);
329-
vi.mocked(output.log).mockClear();
329+
const afterFirstRun = context.listNextSteps().length;
330330

331331
appSdk(context);
332332

333-
expect(output.log).not.toHaveBeenCalled();
333+
// nothing was scaffolded the second time, so nothing new to say
334+
expect(context.listNextSteps()).toHaveLength(afterFirstRun);
334335
});
335-
336336
});
337337

338338
it('is idempotent', async () => {

packages/create-plugin/src/codemods/additions/scripts/experimental-app-sdk.ts

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { fileURLToPath } from 'node:url';
22
import { parseDocument, stringify, YAMLMap, Scalar } from 'yaml';
33
import type { Context } from '../../context.js';
4-
import { output } from '../../../utils/utils.console.js';
54
import { additionsDebug, renderTemplate } from '../../utils.js';
65
import { getTemplateData } from '../../../utils/utils.templates.js';
76

@@ -43,7 +42,7 @@ export default function appSdk(context: Context): Context {
4342

4443
// Only guide the user when we actually scaffolded something; a re-run should stay quiet.
4544
if (Object.keys(context.listChanges()).length > changesBefore) {
46-
printNextSteps();
45+
addNextSteps(context);
4746
}
4847

4948
return context;
@@ -57,7 +56,7 @@ function isAppPlugin(context: Context): boolean {
5756
const pluginJsonContent = context.getFile('src/plugin.json');
5857

5958
if (!pluginJsonContent) {
60-
skip('Could not find src/plugin.json.', ['Run this from the root of your plugin.']);
59+
context.skip('could not find src/plugin.json.', ['Run this from the root of your plugin.']);
6160
return false;
6261
}
6362

@@ -66,28 +65,23 @@ function isAppPlugin(context: Context): boolean {
6665
pluginJson = JSON.parse(pluginJsonContent);
6766
} catch (error) {
6867
additionsDebug(`Failed to parse src/plugin.json: ${error}`);
69-
skip('Could not parse src/plugin.json.');
68+
context.skip('could not parse src/plugin.json.');
7069
return false;
7170
}
7271

7372
if (pluginJson.type !== 'app') {
74-
skip(`grafana-app-sdk codegen needs an app plugin, but this is a ${pluginJson.type} plugin.`, [
75-
'The app-sdk serves Kubernetes-style resources from an app plugin.',
76-
]);
73+
context.skip(
74+
pluginJson.type
75+
? `needs an app plugin, but this is a ${pluginJson.type} plugin.`
76+
: 'needs an app plugin, but src/plugin.json declares no type.',
77+
['The app-sdk serves Kubernetes-style resources from an app plugin.']
78+
);
7779
return false;
7880
}
7981

8082
return true;
8183
}
8284

83-
/**
84-
* Explains why nothing happened. The runner reports success for a no-op codemod, so without this the
85-
* user is left guessing.
86-
*/
87-
function skip(title: string, body: string[] = []) {
88-
output.warning({ title: `Skipping app-sdk: ${title}`, body });
89-
}
90-
9185
function addTemplateFiles(context: Context) {
9286
for (const file of TEMPLATE_FILES) {
9387
if (context.doesFileExist(file)) {
@@ -223,16 +217,11 @@ function addFeatureToggle(context: Context) {
223217
context.updateFile(composePath, stringify(composeData, { lineWidth: 120, singleQuote: true }));
224218
}
225219

226-
/** Tells the user what to run next. */
227-
function printNextSteps() {
220+
/** Records what to run next. The command renders these once the changes are on disk. */
221+
function addNextSteps(context: Context) {
228222
const { packageManagerName } = getTemplateData();
229223

230-
output.log({
231-
title: 'Added grafana-app-sdk code generation. Next steps:',
232-
body: [
233-
'Edit your kinds in ./kinds (start with kinds/example.cue), then run:',
234-
` ${packageManagerName} run generate:kinds`,
235-
'See ./kinds/README.md for the full workflow.',
236-
],
237-
});
224+
context.addNextStep('Edit your kinds in ./kinds, starting with kinds/example.cue');
225+
context.addNextStep(`Run \`${packageManagerName} run generate:kinds\` to generate from them`);
226+
context.addNextStep('See ./kinds/README.md for the full workflow');
238227
}

0 commit comments

Comments
 (0)