@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topogram/cli",
3
- "version": "0.3.52",
3
+ "version": "0.3.53",
4
4
  "description": "Topogram CLI for checking Topogram workspaces and generating app bundles.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -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 was renamed to runtimeKinds`);
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 was renamed to projectionTypes`);
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 was renamed to widgetSupport`);
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)) {
@@ -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 component must be an object");
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 component id '${component.id}'`);
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 was renamed to kind`);
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, "topogram.project.json topology.components was renamed to topology.runtimes");
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");
@@ -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
- pushError(errors, `Field '${key}' was renamed to '${renamedFields.get(key)}' on ${statement.kind} ${statement.id}`, fieldMap.get(key)[0].loc);
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' was renamed to 'widget'`, statement.loc);
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);