Skip to content

Commit adc1582

Browse files
Merge pull request #1440 from opencomponents/validate-command
validate command
2 parents 5ad92af + e3a8e1c commit adc1582

File tree

11 files changed

+727
-3
lines changed

11 files changed

+727
-3
lines changed

src/cli/commands.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,28 @@ export default {
148148
usage: 'Usage: $0 publish <componentPath>'
149149
},
150150

151+
validate: {
152+
cmd: 'validate <componentPath>',
153+
example: {
154+
cmd: '$0 validate my-new-component/'
155+
},
156+
options: {
157+
skipPackage: {
158+
boolean: true,
159+
description: 'Skip packaging step and validate existing package.json',
160+
default: false
161+
},
162+
registries: {
163+
array: true,
164+
description:
165+
'List of registries to validate against. This setting will take precedence over oc.json file'
166+
}
167+
},
168+
description:
169+
'Validate a component against registry requirements without publishing',
170+
usage: 'Usage: $0 validate <componentPath>'
171+
},
172+
151173
registry: {
152174
cmd: 'registry <command>',
153175
description: 'Manage oc registries in the current project',

src/cli/domain/registry.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,53 @@ export default function registry(opts: RegistryOptions = {}) {
138138
res.registries = res.registries?.filter((x) => x !== registry) || [];
139139

140140
setOcConfig(res);
141+
},
142+
async validateComponent(options: {
143+
url: string;
144+
packageJson: any;
145+
}): Promise<void> {
146+
try {
147+
const response = await request(options.url, {
148+
method: 'POST',
149+
headers: {
150+
...requestsHeaders,
151+
'Content-Type': 'application/json'
152+
},
153+
body: JSON.stringify({ packageJson: options.packageJson })
154+
});
155+
156+
if (response.statusCode === 404) {
157+
throw 'Registry URL is invalid or does not exist';
158+
}
159+
160+
const result = (await response.body.json()) as any;
161+
162+
if (response.statusCode !== 200) {
163+
throw result;
164+
}
165+
} catch (err) {
166+
let parsedError = err as any;
167+
let errMsg = '';
168+
if (!parsedError || typeof parsedError !== 'object') {
169+
try {
170+
parsedError = JSON.parse(String(parsedError));
171+
} catch {}
172+
}
173+
174+
if (!!parsedError.code && parsedError.code === 'ECONNREFUSED') {
175+
errMsg = 'Connection to registry has not been established';
176+
} else if (
177+
parsedError.code !== 'cli_version_not_valid' &&
178+
parsedError.code !== 'node_version_not_valid' &&
179+
!!parsedError.error
180+
) {
181+
errMsg = parsedError.error;
182+
} else {
183+
errMsg = parsedError;
184+
}
185+
186+
throw errMsg;
187+
}
141188
}
142189
};
143190
}

src/cli/facade/validate.ts

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import path from 'node:path';
2+
import colors from 'colors/safe';
3+
import fs from 'fs-extra';
4+
import { fromPromise } from 'universalify';
5+
import strings from '../../resources/index';
6+
import type { Component } from '../../types';
7+
import handleDependencies from '../domain/handle-dependencies';
8+
import type { Local } from '../domain/local';
9+
import type { RegistryCli } from '../domain/registry';
10+
import type { Logger } from '../logger';
11+
12+
const validate = ({
13+
logger,
14+
registry,
15+
local
16+
}: {
17+
logger: Logger;
18+
registry: RegistryCli;
19+
local: Local;
20+
}) =>
21+
fromPromise(
22+
async (opts: {
23+
componentPath: string;
24+
skipPackage?: boolean;
25+
registries?: string[];
26+
}): Promise<void> => {
27+
const componentPath = opts.componentPath;
28+
const skipPackage = opts.skipPackage;
29+
const packageDir = path.resolve(componentPath, '_package');
30+
31+
let errorMessage: string;
32+
33+
const readPackageJson = () =>
34+
fs.readJson(path.join(packageDir, 'package.json'));
35+
36+
const packageComponent = async (): Promise<Component> => {
37+
logger.warn(strings.messages.cli.PACKAGING(packageDir));
38+
const packageOptions = {
39+
production: true,
40+
componentPath: path.resolve(componentPath)
41+
};
42+
43+
const component = await local.package(packageOptions);
44+
return component;
45+
};
46+
47+
const validateComponentWithRegistry = async (options: {
48+
registryUrl: string;
49+
packageJson: any;
50+
componentName: string;
51+
componentVersion: string;
52+
}): Promise<void> => {
53+
const { registryUrl, packageJson, componentName, componentVersion } =
54+
options;
55+
const registryNormalised = registryUrl.replace(/\/$/, '');
56+
const validateUrl = `${registryNormalised}/~registry/validate/${componentName}/${componentVersion}`;
57+
58+
logger.warn(
59+
`Validating component against registry: ${registryNormalised}`
60+
);
61+
62+
try {
63+
await registry.validateComponent({
64+
url: validateUrl,
65+
packageJson
66+
});
67+
logger.ok(
68+
`✓ Validation successful for registry: ${registryNormalised}`
69+
);
70+
} catch (err: any) {
71+
if (err.code === 'cli_version_not_valid') {
72+
const upgradeCommand = strings.commands.cli.UPGRADE(
73+
err.details.suggestedVersion
74+
);
75+
const errorDetails =
76+
strings.errors.cli.OC_CLI_VERSION_NEEDS_UPGRADE(
77+
colors.blue(upgradeCommand)
78+
);
79+
80+
errorMessage = strings.errors.cli.VALIDATION_FAIL(errorDetails);
81+
logger.err(errorMessage);
82+
throw errorMessage;
83+
} else if (err.code === 'node_version_not_valid') {
84+
const details = strings.errors.cli.NODE_CLI_VERSION_NEEDS_UPGRADE(
85+
err.details.suggestedVersion
86+
);
87+
88+
errorMessage = strings.errors.cli.VALIDATION_FAIL(details);
89+
logger.err(errorMessage);
90+
throw errorMessage;
91+
} else {
92+
if (err.message) {
93+
errorMessage = err.message;
94+
} else if (err && typeof err === 'object') {
95+
try {
96+
errorMessage = JSON.stringify(err);
97+
} catch {
98+
errorMessage = String(err);
99+
}
100+
} else {
101+
errorMessage = String(err);
102+
}
103+
errorMessage = `✗ Validation failed for registry ${registryNormalised}: ${errorMessage}`;
104+
logger.err(errorMessage);
105+
throw errorMessage;
106+
}
107+
}
108+
};
109+
110+
const validateWithRegistries = async (
111+
registryLocations: string[],
112+
component: Component
113+
) => {
114+
const packageJsonPath = path.join(packageDir, 'package.json');
115+
const packageJson = await fs.readJson(packageJsonPath);
116+
117+
for (const registryUrl of registryLocations) {
118+
await validateComponentWithRegistry({
119+
registryUrl,
120+
packageJson,
121+
componentName: component.name,
122+
componentVersion: component.version
123+
});
124+
}
125+
};
126+
127+
try {
128+
const registryLocations = opts.registries || (await registry.get());
129+
130+
if (!skipPackage) {
131+
await handleDependencies({
132+
components: [path.resolve(componentPath)],
133+
logger
134+
}).catch((err) => {
135+
logger.err(err);
136+
return Promise.reject(err);
137+
});
138+
139+
const component = await packageComponent().catch((err) => {
140+
errorMessage = strings.errors.cli.PACKAGE_CREATION_FAIL(
141+
String(err)
142+
);
143+
logger.err(errorMessage);
144+
return Promise.reject(errorMessage);
145+
});
146+
await validateWithRegistries(registryLocations, component);
147+
} else {
148+
if (fs.existsSync(packageDir)) {
149+
const component = await readPackageJson().catch((err) => {
150+
logger.err(String(err));
151+
return Promise.reject(err);
152+
});
153+
await validateWithRegistries(registryLocations, component);
154+
} else {
155+
errorMessage = strings.errors.cli.PACKAGE_FOLDER_MISSING;
156+
logger.err(errorMessage);
157+
throw errorMessage;
158+
}
159+
}
160+
161+
logger.ok(
162+
'✓ Component validation completed successfully for all registries'
163+
);
164+
} catch (err) {
165+
// Don't log again if it's already been logged by validateComponentWithRegistry
166+
if (!String(err).includes('✗ Validation failed for registry')) {
167+
logger.err(String(err));
168+
}
169+
throw err;
170+
}
171+
}
172+
);
173+
174+
export default validate;

src/cli/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import registry from './facade/registry';
1515
import registryAdd from './facade/registry-add';
1616
import registryLs from './facade/registry-ls';
1717
import registryRemove from './facade/registry-remove';
18+
import validate from './facade/validate';
1819
import logger from './logger';
1920
import validateCommand from './validate-command';
2021

@@ -29,7 +30,8 @@ const cliFunctions = {
2930
registry,
3031
'registry-add': registryAdd,
3132
'registry-ls': registryLs,
32-
'registry-remove': registryRemove
33+
'registry-remove': registryRemove,
34+
validate
3335
};
3436

3537
const currentNodeVersion = process.version;

src/registry/domain/options-sanitiser.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface RegistryOptions<T = any>
2323
ui?: boolean;
2424
experimental?: boolean;
2525
api?: boolean;
26+
validate?: boolean;
2627
}
2728
| boolean;
2829
/**
@@ -90,11 +91,18 @@ export default function optionsSanitiser(input: RegistryOptions): Config {
9091
? // We keep previous default behavior for backward compatibility
9192
true
9293
: (options.discovery?.experimental ?? true);
94+
const showValidation = !showApi
95+
? false
96+
: typeof options.discovery === 'boolean'
97+
? // We keep previous default behavior for backward compatibility
98+
false
99+
: (options.discovery?.validate ?? false);
93100

94101
options.discovery = {
95102
ui: showUI,
96103
experimental: showExperimental,
97-
api: showApi
104+
api: showApi,
105+
validate: showValidation
98106
};
99107

100108
if (typeof options.pollingInterval === 'undefined') {

src/registry/router.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import HistoryRoute from './routes/history';
1212
import PluginsRoute from './routes/plugins';
1313
import PublishRoute from './routes/publish';
1414
import StaticRedirectorRoute from './routes/static-redirector';
15+
import ValidateRoute from './routes/validate';
1516

1617
export function create(app: Express, conf: Config, repository: Repository) {
1718
const routes = {
@@ -24,7 +25,8 @@ export function create(app: Express, conf: Config, repository: Repository) {
2425
staticRedirector: StaticRedirectorRoute(repository),
2526
plugins: PluginsRoute(conf),
2627
dependencies: DependenciesRoute(conf),
27-
history: HistoryRoute(repository)
28+
history: HistoryRoute(repository),
29+
validate: ValidateRoute()
2830
};
2931

3032
const prefix = conf.prefix;
@@ -41,6 +43,9 @@ export function create(app: Express, conf: Config, repository: Repository) {
4143
app.get(`${prefix}~registry/plugins`, routes.plugins);
4244
app.get(`${prefix}~registry/dependencies`, routes.dependencies);
4345
app.get(`${prefix}~registry/history`, routes.history);
46+
if (conf.discovery.validate) {
47+
app.post(`${prefix}~registry/validate`, routes.validate);
48+
}
4449

4550
if (conf.local) {
4651
app.get(

0 commit comments

Comments
 (0)