Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Removed

- **`plugin export`:** Backend plugins and modules no longer ship the legacy `dist/configSchema.json` copy. Exports write only `dist/.config-schema.json` and remove a stale legacy copy when re-exporting without `--clean`, matching the RHDH schema loader ([RHIDP-17026](https://redhat.atlassian.net/browse/RHIDP-17026)).

### Added

- **`plugin dev`:** Add `--watch` to `rhdh-cli plugin dev start`, and a standalone `rhdh-cli plugin dev update --watch`, for continuous re-export/re-stage/restart on source changes ([RHIDP-16673](https://redhat.atlassian.net/browse/RHIDP-16673), [#222](https://github.com/redhat-developer/rhdh-cli/pull/222)). Watches `src/`, `package.json`, `tsconfig.json` with a 500ms debounce and serializes cycles so a change arriving mid-cycle queues exactly one follow-up; prints a refresh URL once RHDH responds. `plugin dev start` now also shows phased `[1/4]`–`[4/4]` progress through build/export, runtime start, plugin install, and readiness polling. `plugin dev update` and `plugin dev restart` fail fast with an actionable message when RHDH Local isn't running yet, instead of surfacing a raw compose/container error.
Expand Down
102 changes: 102 additions & 0 deletions src/commands/export-dynamic-plugin/command.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright (c) Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0.
*/

jest.mock('../../lib/paths', () => ({
paths: {
targetRoot: '/tmp/test-plugin',
resolveTarget: (part: string) => `/tmp/test-plugin/${part}`,
},
}));
jest.mock('../../lib/schema/collect', () => ({ getConfigSchema: jest.fn() }));
jest.mock('../../lib/tasks', () => ({ Task: { log: jest.fn() } }));
jest.mock('./backend', () => ({
backend: jest.fn().mockResolvedValue('/tmp/test-plugin/dist-dynamic'),
}));
jest.mock('./frontend', () => ({
frontend: jest.fn().mockResolvedValue('/tmp/test-plugin/dist-dynamic'),
}));
jest.mock('./check-heavy-deps', () => ({
checkHeavyDependencies: jest.fn().mockResolvedValue(undefined),
}));
jest.mock('./dev', () => ({
applyDevOptions: jest.fn().mockResolvedValue(undefined),
}));
jest.mock('fs-extra', () => ({
...jest.requireActual('fs-extra'),
readJson: jest.fn(),
readJSON: jest.fn().mockResolvedValue({ backstage: {} }),
existsSync: jest.fn().mockReturnValue(false),
ensureDir: jest.fn().mockResolvedValue(undefined),
remove: jest.fn().mockResolvedValue(undefined),
writeJson: jest.fn().mockResolvedValue(undefined),
}));

import fs from 'fs-extra';

import { getConfigSchema } from '../../lib/schema/collect';
import { backend } from './backend';
import { command } from './command';
import { frontend } from './frontend';

const target = '/tmp/test-plugin/dist-dynamic/dist';
const schema = {
$schema: 'http://json-schema.org/draft-07/schema#',
type: 'object',
properties: { example: { type: 'string' } },
} as const;

beforeEach(() => {
jest.clearAllMocks();
jest.mocked(getConfigSchema).mockResolvedValue(schema);
});

test.each(['backend-plugin', 'backend-plugin-module'])(
'exports only the modern schema for %s and removes a stale legacy copy',
async role => {
jest.mocked(fs.readJson).mockResolvedValue({
name: '@internal/example',
backstage: { role },
});

await command({});

expect(backend).toHaveBeenCalledTimes(1);
expect(frontend).not.toHaveBeenCalled();
expect(fs.remove).toHaveBeenCalledTimes(1);
expect(fs.remove).toHaveBeenCalledWith(`${target}/configSchema.json`);
expect(fs.writeJson).toHaveBeenCalledTimes(1);
expect(fs.writeJson).toHaveBeenCalledWith(
`${target}/.config-schema.json`,
schema,
{ encoding: 'utf8', spaces: 2 },
);
expect(jest.mocked(fs.remove).mock.invocationCallOrder[0]).toBeLessThan(
jest.mocked(fs.writeJson).mock.invocationCallOrder[0],
);
},
);

test.each(['frontend-plugin', 'frontend-plugin-module'])(
'exports only the modern schema for %s',
async role => {
jest.mocked(fs.readJson).mockResolvedValue({
name: '@internal/example',
backstage: { role },
});

await command({});

expect(frontend).toHaveBeenCalledTimes(1);
expect(backend).not.toHaveBeenCalled();
expect(fs.remove).not.toHaveBeenCalled();
expect(fs.writeJson).toHaveBeenCalledTimes(1);
expect(fs.writeJson).toHaveBeenCalledWith(
`${target}/.config-schema.json`,
schema,
{ encoding: 'utf8', spaces: 2 },
);
},
);
23 changes: 9 additions & 14 deletions src/commands/export-dynamic-plugin/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,34 +40,29 @@ export async function command(opts: OptionValues): Promise<void> {

let targetPath: string;
const roleInfo = PackageRoles.getRoleInfo(role);
let configSchemaPaths: string[];
if (role === 'backend-plugin' || role === 'backend-plugin-module') {
targetPath = await backend(opts);
configSchemaPaths = [
path.join(targetPath, 'dist/configSchema.json'),
path.join(targetPath, 'dist/.config-schema.json'),
];
// A re-export without --clean must not retain an earlier legacy schema.
await fs.remove(path.join(targetPath, 'dist/configSchema.json'));
} else if (role === 'frontend-plugin' || role === 'frontend-plugin-module') {
targetPath = await frontend(roleInfo, opts);
configSchemaPaths = [path.join(targetPath, 'dist/.config-schema.json')];
} else {
throw new Error(
'Only packages with the "backend-plugin", "backend-plugin-module", "frontend-plugin" or "frontend-plugin-module" roles can be exported as dynamic plugins',
);
}

const configSchemaPath = path.join(targetPath, 'dist/.config-schema.json');
Task.log(
`Saving self-contained config schema in ${chalk.cyan(configSchemaPaths.join(' and '))}`,
`Saving self-contained config schema in ${chalk.cyan(configSchemaPath)}`,
);

const configSchema = await getConfigSchema(rawPkg.name);
for (const configSchemaPath of configSchemaPaths) {
await fs.ensureDir(path.dirname(paths.resolveTarget(configSchemaPath)));
await fs.writeJson(paths.resolveTarget(configSchemaPath), configSchema, {
encoding: 'utf8',
spaces: 2,
});
}
await fs.ensureDir(path.dirname(configSchemaPath));
await fs.writeJson(configSchemaPath, configSchema, {
encoding: 'utf8',
spaces: 2,
});

const heavyDepKind: HeavyDepKind =
role === 'backend-plugin' || role === 'backend-plugin-module'
Expand Down
Loading