@topogram/cli 0.3.52 → 0.3.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/generator/registry.js +13 -3
- package/src/project-config.js +20 -10
- package/src/validator/index.js +71 -35
package/package.json
CHANGED
|
@@ -190,6 +190,16 @@ function isStringArray(value, nonEmpty = false) {
|
|
|
190
190
|
value.every((entry) => typeof entry === "string" && entry.length > 0);
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
/**
|
|
194
|
+
* @param {string} oldName
|
|
195
|
+
* @param {string} newName
|
|
196
|
+
* @param {string} example
|
|
197
|
+
* @returns {string}
|
|
198
|
+
*/
|
|
199
|
+
function renameDiagnostic(oldName, newName, example) {
|
|
200
|
+
return `${oldName} was renamed to ${newName}. Example fix: ${example}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
193
203
|
/**
|
|
194
204
|
* @param {string} generatorId
|
|
195
205
|
* @returns {GeneratorManifest|null}
|
|
@@ -393,13 +403,13 @@ export function validateGeneratorManifest(manifest) {
|
|
|
393
403
|
errors.push(`${label} surface must be api, web, database, or native`);
|
|
394
404
|
}
|
|
395
405
|
if (manifest.targetKind != null) {
|
|
396
|
-
errors.push(`${label} targetKind
|
|
406
|
+
errors.push(`${label} ${renameDiagnostic("'targetKind'", "'runtimeKinds'", `"runtimeKinds": ["web_surface"]`)}`);
|
|
397
407
|
}
|
|
398
408
|
if (!isStringArray(manifest.runtimeKinds, true)) {
|
|
399
409
|
errors.push(`${label} runtimeKinds must be a non-empty string array`);
|
|
400
410
|
}
|
|
401
411
|
if (manifest["projectionPlatforms"] != null) {
|
|
402
|
-
errors.push(`${label} projectionPlatforms
|
|
412
|
+
errors.push(`${label} ${renameDiagnostic("'projectionPlatforms'", "'projectionTypes'", `"projectionTypes": ["web_surface"]`)}`);
|
|
403
413
|
}
|
|
404
414
|
if (!isStringArray(manifest.projectionTypes, true)) {
|
|
405
415
|
errors.push(`${label} projectionTypes must be a non-empty string array`);
|
|
@@ -417,7 +427,7 @@ export function validateGeneratorManifest(manifest) {
|
|
|
417
427
|
errors.push(`${label} capabilities must be an object`);
|
|
418
428
|
}
|
|
419
429
|
if (manifest["componentSupport"] != null) {
|
|
420
|
-
errors.push(`${label} componentSupport
|
|
430
|
+
errors.push(`${label} ${renameDiagnostic("'componentSupport'", "'widgetSupport'", `"widgetSupport": { "patterns": ["resource_table"] }`)}`);
|
|
421
431
|
}
|
|
422
432
|
if (manifest.widgetSupport != null) {
|
|
423
433
|
if (typeof manifest.widgetSupport !== "object" || Array.isArray(manifest.widgetSupport)) {
|
package/src/project-config.js
CHANGED
|
@@ -106,6 +106,16 @@ function readJson(filePath) {
|
|
|
106
106
|
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* @param {string} oldName
|
|
111
|
+
* @param {string} newName
|
|
112
|
+
* @param {string} example
|
|
113
|
+
* @returns {string}
|
|
114
|
+
*/
|
|
115
|
+
function renameDiagnostic(oldName, newName, example) {
|
|
116
|
+
return `${oldName} was renamed to ${newName}. Example fix: ${example}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
109
119
|
/**
|
|
110
120
|
* @param {string} root
|
|
111
121
|
* @param {string} fileName
|
|
@@ -347,18 +357,24 @@ function componentLabel(component) {
|
|
|
347
357
|
*/
|
|
348
358
|
function validateComponentShape(errors, component, seenIds) {
|
|
349
359
|
if (!component || typeof component !== "object" || Array.isArray(component)) {
|
|
350
|
-
pushError(errors, "Topology
|
|
360
|
+
pushError(errors, "Topology runtime must be an object");
|
|
351
361
|
return false;
|
|
352
362
|
}
|
|
353
363
|
if (typeof component.id !== "string" || !IDENTIFIER_PATTERN.test(component.id)) {
|
|
354
364
|
pushError(errors, `${componentLabel(component)} id must match ${IDENTIFIER_PATTERN}`);
|
|
355
365
|
} else if (seenIds.has(component.id)) {
|
|
356
|
-
pushError(errors, `Duplicate topology
|
|
366
|
+
pushError(errors, `Duplicate topology runtime id '${component.id}'`);
|
|
357
367
|
} else {
|
|
358
368
|
seenIds.add(component.id);
|
|
359
369
|
}
|
|
360
370
|
if (component.type != null) {
|
|
361
|
-
pushError(errors, `${componentLabel(component)} type
|
|
371
|
+
pushError(errors, `${componentLabel(component)} ${renameDiagnostic("'type'", "'kind'", `"kind": "api_service"`)}`);
|
|
372
|
+
}
|
|
373
|
+
if (component.database != null) {
|
|
374
|
+
pushError(errors, `${componentLabel(component)} ${renameDiagnostic("'database'", "'uses_database'", `"uses_database": "app_db"`)}`);
|
|
375
|
+
}
|
|
376
|
+
if (component.api != null) {
|
|
377
|
+
pushError(errors, `${componentLabel(component)} ${renameDiagnostic("'api'", "'uses_api'", `"uses_api": "app_api"`)}`);
|
|
362
378
|
}
|
|
363
379
|
if (!["api_service", "web_surface", "ios_surface", "android_surface", "database"].includes(component.kind)) {
|
|
364
380
|
pushError(errors, `${componentLabel(component)} kind must be api_service, web_surface, ios_surface, android_surface, or database`);
|
|
@@ -440,12 +456,6 @@ function validateTopologyReferences(errors, components) {
|
|
|
440
456
|
usedPorts.set(component.port, component.id);
|
|
441
457
|
}
|
|
442
458
|
}
|
|
443
|
-
if (component.database != null) {
|
|
444
|
-
pushError(errors, `${componentLabel(component)} database was renamed to uses_database`);
|
|
445
|
-
}
|
|
446
|
-
if (component.api != null) {
|
|
447
|
-
pushError(errors, `${componentLabel(component)} api was renamed to uses_api`);
|
|
448
|
-
}
|
|
449
459
|
if (component.kind === "api_service") {
|
|
450
460
|
if (component.uses_database && byId.get(component.uses_database)?.kind !== "database") {
|
|
451
461
|
pushError(errors, `${componentLabel(component)} references missing database runtime '${component.uses_database}'`);
|
|
@@ -476,7 +486,7 @@ export function validateProjectConfig(config, graph = null, options = {}) {
|
|
|
476
486
|
}
|
|
477
487
|
validateOutputConfig(errors, config);
|
|
478
488
|
if (config.topology?.components != null && config.topology.__normalizedRuntimeAliases !== true) {
|
|
479
|
-
pushError(errors,
|
|
489
|
+
pushError(errors, `topogram.project.json ${renameDiagnostic("'topology.components'", "'topology.runtimes'", `"topology": { "runtimes": [] }`)}`);
|
|
480
490
|
}
|
|
481
491
|
if (!config.topology || typeof config.topology !== "object" || !Array.isArray(config.topology.runtimes)) {
|
|
482
492
|
pushError(errors, "topogram.project.json topology.runtimes must be an array");
|
package/src/validator/index.js
CHANGED
|
@@ -108,6 +108,10 @@ export function pushError(errors, message, loc) {
|
|
|
108
108
|
});
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
function renameDiagnostic(oldName, newName, example) {
|
|
112
|
+
return `${oldName} was renamed to ${newName}. Example fix: ${example}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
111
115
|
export function formatLoc(loc) {
|
|
112
116
|
const line = loc?.start?.line ?? 1;
|
|
113
117
|
const column = loc?.start?.column ?? 1;
|
|
@@ -226,44 +230,45 @@ function validateFieldPresence(errors, statement, fieldMap) {
|
|
|
226
230
|
}
|
|
227
231
|
|
|
228
232
|
const renamedFields = new Map([
|
|
229
|
-
["platform", "type"],
|
|
230
|
-
["ui_components", "widget_bindings"],
|
|
231
|
-
["ui_design", "design_tokens"],
|
|
232
|
-
["ui_routes", "screen_routes"],
|
|
233
|
-
["ui_screens", "screens"],
|
|
234
|
-
["ui_screen_regions", "screen_regions"],
|
|
235
|
-
["ui_navigation", "navigation"],
|
|
236
|
-
["ui_app_shell", "app_shell"],
|
|
237
|
-
["ui_collections", "collection_views"],
|
|
238
|
-
["ui_actions", "screen_actions"],
|
|
239
|
-
["ui_visibility", "visibility_rules"],
|
|
240
|
-
["ui_lookups", "field_lookups"],
|
|
241
|
-
["web_surface", "web_hints"],
|
|
242
|
-
["ios_surface", "ios_hints"],
|
|
243
|
-
["http", "endpoints"],
|
|
244
|
-
["http_errors", "error_responses"],
|
|
245
|
-
["http_fields", "wire_fields"],
|
|
246
|
-
["http_responses", "responses"],
|
|
247
|
-
["http_preconditions", "preconditions"],
|
|
248
|
-
["http_idempotency", "idempotency"],
|
|
249
|
-
["http_cache", "cache"],
|
|
250
|
-
["http_delete", "delete_semantics"],
|
|
251
|
-
["http_async", "async_jobs"],
|
|
252
|
-
["http_status", "async_status"],
|
|
253
|
-
["http_download", "downloads"],
|
|
254
|
-
["http_authz", "authorization"],
|
|
255
|
-
["http_callbacks", "callbacks"],
|
|
256
|
-
["db_tables", "tables"],
|
|
257
|
-
["db_columns", "columns"],
|
|
258
|
-
["db_keys", "keys"],
|
|
259
|
-
["db_indexes", "indexes"],
|
|
260
|
-
["db_relations", "relations"],
|
|
261
|
-
["db_lifecycle", "lifecycle"]
|
|
233
|
+
["platform", ["type", "type web_surface"]],
|
|
234
|
+
["ui_components", ["widget_bindings", "widget_bindings { screen item_list region results widget widget_data_grid }"]],
|
|
235
|
+
["ui_design", ["design_tokens", "design_tokens { density comfortable tone operational }"]],
|
|
236
|
+
["ui_routes", ["screen_routes", "screen_routes { screen item_list path /items }"]],
|
|
237
|
+
["ui_screens", ["screens", "screens { item_list list title \"Items\" }"]],
|
|
238
|
+
["ui_screen_regions", ["screen_regions", "screen_regions { screen item_list region results pattern resource_table }"]],
|
|
239
|
+
["ui_navigation", ["navigation", "navigation { primary item_list label \"Items\" }"]],
|
|
240
|
+
["ui_app_shell", ["app_shell", "app_shell { shell sidebar }"]],
|
|
241
|
+
["ui_collections", ["collection_views", "collection_views { item_list presentation table }"]],
|
|
242
|
+
["ui_actions", ["screen_actions", "screen_actions { screen item_list action create target cap_create_item }"]],
|
|
243
|
+
["ui_visibility", ["visibility_rules", "visibility_rules { screen item_list when cap_list_items }"]],
|
|
244
|
+
["ui_lookups", ["field_lookups", "field_lookups { field owner_id source cap_list_users }"]],
|
|
245
|
+
["web_surface", ["web_hints", "web_hints { router file_based }"]],
|
|
246
|
+
["ios_surface", ["ios_hints", "ios_hints { navigation stack }"]],
|
|
247
|
+
["http", ["endpoints", "endpoints { cap_list_items method GET path /items success 200 }"]],
|
|
248
|
+
["http_errors", ["error_responses", "error_responses { cap_list_items 404 shape_error }"]],
|
|
249
|
+
["http_fields", ["wire_fields", "wire_fields { shape_item title title }"]],
|
|
250
|
+
["http_responses", ["responses", "responses { cap_list_items 200 shape_item_list }"]],
|
|
251
|
+
["http_preconditions", ["preconditions", "preconditions { cap_update_item rule_item_exists }"]],
|
|
252
|
+
["http_idempotency", ["idempotency", "idempotency { cap_create_item key request_id }"]],
|
|
253
|
+
["http_cache", ["cache", "cache { cap_list_items max_age 60 }"]],
|
|
254
|
+
["http_delete", ["delete_semantics", "delete_semantics { cap_delete_item mode soft_delete }"]],
|
|
255
|
+
["http_async", ["async_jobs", "async_jobs { cap_export_items job task_export }"]],
|
|
256
|
+
["http_status", ["async_status", "async_status { cap_export_items path /exports/{job_id} }"]],
|
|
257
|
+
["http_download", ["downloads", "downloads { cap_download_export content_type text/csv }"]],
|
|
258
|
+
["http_authz", ["authorization", "authorization { cap_update_item role editor }"]],
|
|
259
|
+
["http_callbacks", ["callbacks", "callbacks { cap_export_items event completed }"]],
|
|
260
|
+
["db_tables", ["tables", "tables { entity_item table items }"]],
|
|
261
|
+
["db_columns", ["columns", "columns { entity_item field title column title }"]],
|
|
262
|
+
["db_keys", ["keys", "keys { entity_item primary [id] }"]],
|
|
263
|
+
["db_indexes", ["indexes", "indexes { entity_item index [title] }"]],
|
|
264
|
+
["db_relations", ["relations", "relations { entity_item foreign_key owner_id references entity_user.id }"]],
|
|
265
|
+
["db_lifecycle", ["lifecycle", "lifecycle { entity_item timestamps created_at created_at updated_at updated_at }"]]
|
|
262
266
|
]);
|
|
263
267
|
|
|
264
268
|
for (const key of fieldMap.keys()) {
|
|
265
269
|
if (renamedFields.has(key)) {
|
|
266
|
-
|
|
270
|
+
const [newName, example] = renamedFields.get(key);
|
|
271
|
+
pushError(errors, `Field '${key}' on ${statement.kind} ${statement.id} ${renameDiagnostic(`'${key}'`, `'${newName}'`, example)}`, fieldMap.get(key)[0].loc);
|
|
267
272
|
continue;
|
|
268
273
|
}
|
|
269
274
|
if (!spec.allowed.includes(key)) {
|
|
@@ -278,6 +283,36 @@ function validateFieldPresence(errors, statement, fieldMap) {
|
|
|
278
283
|
}
|
|
279
284
|
}
|
|
280
285
|
|
|
286
|
+
function validateProjectionTypeRenames(errors, statement, fieldMap) {
|
|
287
|
+
if (statement.kind !== "projection") {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const typeField = fieldMap.get("type")?.[0];
|
|
292
|
+
const typeValue = symbolValue(typeField?.value);
|
|
293
|
+
const renamedTypes = new Map([
|
|
294
|
+
["ui_shared", "ui_contract"],
|
|
295
|
+
["ui_web", "web_surface"],
|
|
296
|
+
["ui_ios", "ios_surface"],
|
|
297
|
+
["ui_android", "android_surface"],
|
|
298
|
+
["dotnet", "api_contract"],
|
|
299
|
+
["api", "api_contract"],
|
|
300
|
+
["backend", "api_contract"],
|
|
301
|
+
["db_postgres", "db_contract"],
|
|
302
|
+
["db_sqlite", "db_contract"]
|
|
303
|
+
]);
|
|
304
|
+
if (!typeField || !renamedTypes.has(typeValue)) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const nextType = renamedTypes.get(typeValue);
|
|
309
|
+
pushError(
|
|
310
|
+
errors,
|
|
311
|
+
`Projection ${statement.id} ${renameDiagnostic(`type value '${typeValue}'`, `'${nextType}'`, `type ${nextType}`)}`,
|
|
312
|
+
typeField.value.loc
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
281
316
|
function validateBlockEntryLengths(errors, statement, fieldMap, key, minimumWidth) {
|
|
282
317
|
const field = fieldMap.get(key)?.[0];
|
|
283
318
|
if (!field || field.value.type !== "block") {
|
|
@@ -3388,7 +3423,7 @@ export function buildRegistry(workspaceAst, errors) {
|
|
|
3388
3423
|
for (const statement of file.statements) {
|
|
3389
3424
|
if (!STATEMENT_KINDS.has(statement.kind)) {
|
|
3390
3425
|
if (statement.kind === "component") {
|
|
3391
|
-
pushError(errors, `Statement kind 'component'
|
|
3426
|
+
pushError(errors, `Statement kind ${renameDiagnostic("'component'", "'widget'", "widget widget_data_grid { ... }")}`, statement.loc);
|
|
3392
3427
|
} else {
|
|
3393
3428
|
pushError(errors, `Unknown statement kind '${statement.kind}'`, statement.loc);
|
|
3394
3429
|
}
|
|
@@ -3569,6 +3604,7 @@ export function validateWorkspace(workspaceAst) {
|
|
|
3569
3604
|
for (const statement of file.statements) {
|
|
3570
3605
|
const fieldMap = collectFieldMap(statement);
|
|
3571
3606
|
validateFieldPresence(errors, statement, fieldMap);
|
|
3607
|
+
validateProjectionTypeRenames(errors, statement, fieldMap);
|
|
3572
3608
|
validateFieldShapes(errors, statement, fieldMap);
|
|
3573
3609
|
validateStatus(errors, statement, fieldMap);
|
|
3574
3610
|
validateRuleSeverity(errors, statement, fieldMap);
|