splent-cli 1.46.0__tar.gz → 1.48.0__tar.gz

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.
Files changed (317) hide show
  1. {splent_cli-1.46.0/src/splent_cli.egg-info → splent_cli-1.48.0}/PKG-INFO +1 -1
  2. {splent_cli-1.46.0 → splent_cli-1.48.0}/pyproject.toml +4 -2
  3. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_compile.py +136 -12
  4. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_release.py +83 -2
  5. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_build.py +36 -1
  6. splent_cli-1.48.0/src/splent_cli/commands/product/product_frontend_audit.py +323 -0
  7. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_release.py +2 -0
  8. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_validate.py +108 -7
  9. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/services/frontend.py +224 -28
  10. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/services/preflight.py +30 -0
  11. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_.gitignore.j2 +3 -0
  12. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_Dockerfile.prod.j2 +21 -12
  13. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_ci_validate.yml.j2 +21 -4
  14. splent_cli-1.48.0/src/splent_cli/toolchain/Dockerfile.frontend +48 -0
  15. splent_cli-1.48.0/src/splent_cli/toolchain/frontend/package-lock.json +2130 -0
  16. splent_cli-1.48.0/src/splent_cli/toolchain/frontend/package.json +16 -0
  17. splent_cli-1.48.0/src/splent_cli/toolchain/frontend/splent/build.js +76 -0
  18. splent_cli-1.48.0/src/splent_cli/toolchain/frontend/splent/legacy.js +52 -0
  19. splent_cli-1.48.0/src/splent_cli/toolchain/frontend/splent/lint.js +147 -0
  20. splent_cli-1.48.0/src/splent_cli/toolchain/frontend/splent/plugin.js +387 -0
  21. splent_cli-1.48.0/src/splent_cli/toolchain/frontend/splent/preset.js +248 -0
  22. splent_cli-1.48.0/src/splent_cli/toolchain/frontend/splent/singletons.json +18 -0
  23. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/deploy_freshness.py +8 -3
  24. splent_cli-1.48.0/src/splent_cli/utils/frontend_check.py +1013 -0
  25. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/frontend_licenses.py +4 -2
  26. splent_cli-1.48.0/src/splent_cli/utils/npm_semver.py +200 -0
  27. {splent_cli-1.46.0 → splent_cli-1.48.0/src/splent_cli.egg-info}/PKG-INFO +1 -1
  28. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli.egg-info/SOURCES.txt +12 -0
  29. {splent_cli-1.46.0 → splent_cli-1.48.0}/COPYING +0 -0
  30. {splent_cli-1.46.0 → splent_cli-1.48.0}/COPYING.LESSER +0 -0
  31. {splent_cli-1.46.0 → splent_cli-1.48.0}/README.md +0 -0
  32. {splent_cli-1.46.0 → splent_cli-1.48.0}/setup.cfg +0 -0
  33. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/__init__.py +0 -0
  34. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/__main__.py +0 -0
  35. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/cli.py +0 -0
  36. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/__init__.py +0 -0
  37. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/cache/__init__.py +0 -0
  38. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/cache/cache_clear.py +0 -0
  39. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/cache/cache_orphans.py +0 -0
  40. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/cache/cache_outdated.py +0 -0
  41. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/cache/cache_prune.py +0 -0
  42. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/cache/cache_size.py +0 -0
  43. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/cache/cache_status.py +0 -0
  44. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/cache/cache_usage.py +0 -0
  45. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/cache/cache_versions.py +0 -0
  46. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/check/__init__.py +0 -0
  47. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/check/check_contracts.py +0 -0
  48. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/check/check_deps.py +0 -0
  49. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/check/check_docker.py +0 -0
  50. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/check/check_env.py +0 -0
  51. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/check/check_features.py +0 -0
  52. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/check/check_github.py +0 -0
  53. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/check/check_infra.py +0 -0
  54. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/check/check_product.py +0 -0
  55. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/check/check_pypi.py +0 -0
  56. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/check/check_pyproject.py +0 -0
  57. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/check/check_releases.py +0 -0
  58. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/check/check_vendor.py +0 -0
  59. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/clear/__init__.py +0 -0
  60. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/clear/clear_build.py +0 -0
  61. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/clear/clear_log.py +0 -0
  62. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/clear/clear_uploads.py +0 -0
  63. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/command/__init__.py +0 -0
  64. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/command/command_create.py +0 -0
  65. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/coverage.py +0 -0
  66. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/database/db_console.py +0 -0
  67. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/database/db_dump.py +0 -0
  68. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/database/db_migrate.py +0 -0
  69. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/database/db_reset.py +0 -0
  70. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/database/db_restore.py +0 -0
  71. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/database/db_rollback.py +0 -0
  72. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/database/db_seed.py +0 -0
  73. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/database/db_status.py +0 -0
  74. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/database/db_upgrade.py +0 -0
  75. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/doctor.py +0 -0
  76. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/env/env_list.py +0 -0
  77. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/env/env_set.py +0 -0
  78. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/env/env_show.py +0 -0
  79. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/export/__init__.py +0 -0
  80. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/export/export_puml.py +0 -0
  81. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_add.py +0 -0
  82. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_attach.py +0 -0
  83. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_clean.py +0 -0
  84. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_clone.py +0 -0
  85. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_compat.py +0 -0
  86. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_contract.py +0 -0
  87. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_create.py +0 -0
  88. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_delete.py +0 -0
  89. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_detach.py +0 -0
  90. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_discard.py +0 -0
  91. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_drift.py +0 -0
  92. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_env.py +0 -0
  93. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_fork.py +0 -0
  94. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_frontend_init.py +0 -0
  95. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_git.py +0 -0
  96. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_hook_add.py +0 -0
  97. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_hook_remove.py +0 -0
  98. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_hooks.py +0 -0
  99. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_impact.py +0 -0
  100. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_info.py +0 -0
  101. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_inject_config.py +0 -0
  102. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_install.py +0 -0
  103. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_list.py +0 -0
  104. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_npm.py +0 -0
  105. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_order.py +0 -0
  106. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_outdated.py +0 -0
  107. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_pin.py +0 -0
  108. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_pip_install.py +0 -0
  109. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_pull.py +0 -0
  110. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_refine.py +0 -0
  111. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_remove.py +0 -0
  112. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_rename.py +0 -0
  113. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_search.py +0 -0
  114. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_status.py +0 -0
  115. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_sync_template.py +0 -0
  116. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_test.py +0 -0
  117. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_translate.py +0 -0
  118. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_unlock.py +0 -0
  119. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_upgrade.py +0 -0
  120. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_versions.py +0 -0
  121. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/feature/feature_xray.py +0 -0
  122. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/lint.py +0 -0
  123. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/locust.py +0 -0
  124. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/marketplace/login.py +0 -0
  125. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/marketplace/logout.py +0 -0
  126. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/marketplace/marketplace_auth_utils.py +0 -0
  127. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/marketplace/marketplace_index.py +0 -0
  128. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/marketplace/whoami.py +0 -0
  129. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/__init__.py +0 -0
  130. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_auto_require.py +0 -0
  131. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_clean.py +0 -0
  132. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_commands.py +0 -0
  133. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_config.py +0 -0
  134. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_configure.py +0 -0
  135. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_console.py +0 -0
  136. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_containers.py +0 -0
  137. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_create.py +0 -0
  138. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_deploy.py +0 -0
  139. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_derive.py +0 -0
  140. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_deselect.py +0 -0
  141. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_down.py +0 -0
  142. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_drift.py +0 -0
  143. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_env.py +0 -0
  144. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_list.py +0 -0
  145. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_logs.py +0 -0
  146. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_missing.py +0 -0
  147. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_params.py +0 -0
  148. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_port.py +0 -0
  149. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_resolve.py +0 -0
  150. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_restart.py +0 -0
  151. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_routes.py +0 -0
  152. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_run.py +0 -0
  153. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_select.py +0 -0
  154. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_shell.py +0 -0
  155. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_signals.py +0 -0
  156. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_sync_template.py +0 -0
  157. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_test.py +0 -0
  158. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/product/product_up.py +0 -0
  159. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/release/__init__.py +0 -0
  160. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/release/release_core.py +0 -0
  161. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/release/release_resume.py +0 -0
  162. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/selenium.py +0 -0
  163. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/spl/__init__.py +0 -0
  164. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/spl/spl_add_constraints.py +0 -0
  165. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/spl/spl_add_feature.py +0 -0
  166. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/spl/spl_compare.py +0 -0
  167. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/spl/spl_configurations.py +0 -0
  168. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/spl/spl_create.py +0 -0
  169. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/spl/spl_deps.py +0 -0
  170. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/spl/spl_features.py +0 -0
  171. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/spl/spl_fetch.py +0 -0
  172. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/spl/spl_info.py +0 -0
  173. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/spl/spl_list.py +0 -0
  174. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/spl/spl_outdated.py +0 -0
  175. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/spl/spl_pin.py +0 -0
  176. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/spl/spl_publish.py +0 -0
  177. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/spl/spl_remove_feature.py +0 -0
  178. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/spl/spl_utils.py +0 -0
  179. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/tokens_setup.py +0 -0
  180. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/uvl/uvl_utils.py +0 -0
  181. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/commands/version.py +0 -0
  182. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/services/__init__.py +0 -0
  183. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/services/compose.py +0 -0
  184. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/services/context.py +0 -0
  185. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/services/credentials.py +0 -0
  186. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/services/deploy_bundle.py +0 -0
  187. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/services/editable_refresh.py +0 -0
  188. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/services/image_registry.py +0 -0
  189. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/services/index_refresh.py +0 -0
  190. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/services/marketplace.py +0 -0
  191. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/services/marketplace_api.py +0 -0
  192. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/services/marketplace_url.py +0 -0
  193. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/services/neutral_app.py +0 -0
  194. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/services/own_commands.py +0 -0
  195. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/services/pypi_quota.py +0 -0
  196. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/services/registry.py +0 -0
  197. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/services/release.py +0 -0
  198. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/services/release_dry_run.py +0 -0
  199. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/services/release_gate.py +0 -0
  200. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/services/spl_store.py +0 -0
  201. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/services/web_container.py +0 -0
  202. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_MANIFEST.in.j2 +0 -0
  203. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_README.md.j2 +0 -0
  204. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_commands.py.j2 +0 -0
  205. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_config.py.j2 +0 -0
  206. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_docker-compose.dev.yml.j2 +0 -0
  207. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_docker-compose.prod.yml.j2 +0 -0
  208. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_docker.env.example.j2 +0 -0
  209. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_forms.py.j2 +0 -0
  210. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_frontend.npmrc.j2 +0 -0
  211. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_frontend_scripts.js.j2 +0 -0
  212. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_hooks.py.j2 +0 -0
  213. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_init.py.j2 +0 -0
  214. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_init_config.py.j2 +0 -0
  215. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_init_light.py.j2 +0 -0
  216. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_init_service.py.j2 +0 -0
  217. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_migrations_alembic.ini.j2 +0 -0
  218. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_migrations_env.py.j2 +0 -0
  219. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_migrations_script.py.mako.j2 +0 -0
  220. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_models.py.j2 +0 -0
  221. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_pyproject.toml.j2 +0 -0
  222. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_pyproject_minimal.toml.j2 +0 -0
  223. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_repositories.py.j2 +0 -0
  224. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_routes.py.j2 +0 -0
  225. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_scripts.js.j2 +0 -0
  226. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_seeders.py.j2 +0 -0
  227. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_services.py.j2 +0 -0
  228. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_services_service.py.j2 +0 -0
  229. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_signals.py.j2 +0 -0
  230. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_templates_index.html.j2 +0 -0
  231. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_tests_conftest.py.j2 +0 -0
  232. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_tests_e2e.py.j2 +0 -0
  233. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_tests_functional.py.j2 +0 -0
  234. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_tests_integration.py.j2 +0 -0
  235. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_tests_locustfile.py.j2 +0 -0
  236. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_tests_unit.py.j2 +0 -0
  237. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/feature/feature_webpack.config.js.j2 +0 -0
  238. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/licenses/COPYING +0 -0
  239. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/licenses/COPYING.LESSER +0 -0
  240. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_.env.dev.example.j2 +0 -0
  241. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_.env.prod.example.j2 +0 -0
  242. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_.gitignore.j2 +0 -0
  243. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_00_core_requirements_dev.sh.j2 +0 -0
  244. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_00_install_features.sh.j2 +0 -0
  245. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_01_compile_assets.sh.j2 +0 -0
  246. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_02_0_db_wait_connection.sh.j2 +0 -0
  247. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_02_1_db_create_db_test.sh.j2 +0 -0
  248. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_02_2_db_create_splent_migrations.sh.j2 +0 -0
  249. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_03_initialize_migrations.sh.j2 +0 -0
  250. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_04_handle_migrations.sh.j2 +0 -0
  251. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_05_0_start_app_dev.sh.j2 +0 -0
  252. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_05_1_start_app_prod.sh.j2 +0 -0
  253. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_400.html +0 -0
  254. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_401.html +0 -0
  255. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_404.html +0 -0
  256. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_500.html +0 -0
  257. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_Dockerfile.dev.dockerignore.j2 +0 -0
  258. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_Dockerfile.dev.j2 +0 -0
  259. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_Dockerfile.prod.dockerignore.j2 +0 -0
  260. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_README.md.j2 +0 -0
  261. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_app.css +0 -0
  262. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_app.js +0 -0
  263. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_base_template.html +0 -0
  264. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_ci_upgrade.yml.j2 +0 -0
  265. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_config.py.j2 +0 -0
  266. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_docker-compose.dev.yml.j2 +0 -0
  267. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_docker-compose.prod.yml.j2 +0 -0
  268. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_dropzone.css +0 -0
  269. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_entrypoint.dev.sh.j2 +0 -0
  270. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_entrypoint.prod.sh.j2 +0 -0
  271. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_errors.py.j2 +0 -0
  272. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_favicon.svg +0 -0
  273. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_init.py.j2 +0 -0
  274. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_logging.py.j2 +0 -0
  275. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_own.css +0 -0
  276. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_package.json.j2 +0 -0
  277. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_pyproject.toml.j2 +0 -0
  278. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/templates/product/product_uvl.j2 +0 -0
  279. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/__init__.py +0 -0
  280. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/archetype.py +0 -0
  281. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/cache_utils.py +0 -0
  282. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/ci_services.py +0 -0
  283. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/ci_workflow.py +0 -0
  284. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/command_loader.py +0 -0
  285. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/contract_freshness.py +0 -0
  286. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/db_client.py +0 -0
  287. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/db_utils.py +0 -0
  288. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/decorators.py +0 -0
  289. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/dynamic_imports.py +0 -0
  290. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/feature_installer.py +0 -0
  291. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/feature_naming.py +0 -0
  292. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/feature_params.py +0 -0
  293. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/feature_requirements.py +0 -0
  294. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/feature_resolution.py +0 -0
  295. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/feature_utils.py +0 -0
  296. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/forge.py +0 -0
  297. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/git_url.py +0 -0
  298. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/integrity.py +0 -0
  299. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/io_utils.py +0 -0
  300. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/lifecycle.py +0 -0
  301. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/manifest.py +0 -0
  302. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/menu_keys.py +0 -0
  303. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/migration_lock.py +0 -0
  304. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/path_utils.py +0 -0
  305. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/proc.py +0 -0
  306. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/redact.py +0 -0
  307. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/refinement_facts.py +0 -0
  308. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/running_version.py +0 -0
  309. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/template_drift.py +0 -0
  310. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/toml_edit.py +0 -0
  311. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/upgrade_plan.py +0 -0
  312. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/uvl_diagnosis.py +0 -0
  313. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli/utils/uvl_params.py +0 -0
  314. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli.egg-info/dependency_links.txt +0 -0
  315. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli.egg-info/entry_points.txt +0 -0
  316. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli.egg-info/requires.txt +0 -0
  317. {splent_cli-1.46.0 → splent_cli-1.48.0}/src/splent_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: splent_cli
3
- Version: 1.46.0
3
+ Version: 1.48.0
4
4
  Summary: SPLENT-CLI is a CLI to be able to work on your development more easily.
5
5
  Author-email: DiversoLab <diversolab@us.es>
6
6
  License-Expression: LGPL-3.0-or-later
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "splent_cli"
7
- version = "1.46.0"
7
+ version = "1.48.0"
8
8
  description = "SPLENT-CLI is a CLI to be able to work on your development more easily."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -78,8 +78,10 @@ where = ["src"]
78
78
 
79
79
  # The templates product:create, feature:create and the rest render. They are
80
80
  # data, not modules, so without this a wheel from PyPI shipped none of them.
81
+ # The build context of the frontend toolchain image too, so a CLI installed
82
+ # from PyPI can build the image a feature with frontend/ builds in.
81
83
  [tool.setuptools.package-data]
82
- splent_cli = ["templates/**/*"]
84
+ splent_cli = ["templates/**/*", "toolchain/**/*"]
83
85
 
84
86
  [tool.pytest.ini_options]
85
87
  testpaths = ["tests"]
@@ -1,8 +1,10 @@
1
+ import dataclasses
1
2
  import logging
2
3
  import os
3
4
  import posixpath
4
5
  import shlex
5
6
  import subprocess
7
+ from pathlib import Path
6
8
 
7
9
  import click
8
10
 
@@ -368,20 +370,43 @@ def _compile_frontend(root, feature, *, watch, production, release_build) -> boo
368
370
 
369
371
 
370
372
  def _verify_features(features, workspace: str) -> int:
371
- """feature:compile --verify: 1 when the manifest of any feature with
372
- frontend/ does not match its files, else 0. A pinned feature is checked
373
- in the copy the product links, which is what its app serves."""
373
+ """feature:compile --verify: 1 when the bundle of any feature with
374
+ frontend/ does not match its manifest, else 0.
375
+
376
+ A working copy is checked at the workspace root, a pinned feature in the
377
+ copy the product links, which is what its app serves, and where neither
378
+ is, as in the production image, in the package pip installed. When the
379
+ product has docker/frontend.lock.json (product:build writes it), every
380
+ feature it lists is checked too, and has to carry the manifest the lock
381
+ names, of the version it names: the bundle its release built.
382
+ """
383
+ from splent_cli.utils.frontend_check import read_lock
384
+
385
+ product = context.active_app(workspace)
386
+ lock = read_lock(os.path.join(workspace, product)) if product else None
387
+ locked = (lock or {}).get("features") or {}
388
+ refs = list(features)
389
+ named = {_split_ref(feature)[1] for feature in refs}
390
+ for name, entry in sorted(locked.items()):
391
+ if name not in named:
392
+ version = entry.get("version")
393
+ namespace = entry.get("namespace") or "splent_io"
394
+ refs.append(f"{namespace}/{name}" + (f"@v{version}" if version else ""))
395
+
374
396
  failed = 0
375
- for feature in features:
397
+ for feature in refs:
376
398
  namespace, base_name, version = _split_ref(feature)
377
- if version:
378
- product = context.require_app()
379
- root = os.path.join(
380
- workspace, product, "features", namespace, f"{base_name}@{version}"
399
+ entry = locked.get(base_name)
400
+ copy = _copy_of(workspace, product, namespace, base_name, version)
401
+ if copy is None and entry is not None:
402
+ failed += 1
403
+ click.secho(
404
+ f"❌ {feature}: docker/frontend.lock.json lists it, and it is "
405
+ "neither here nor installed",
406
+ fg="red",
381
407
  )
382
- else:
383
- root = os.path.join(workspace, base_name)
384
- if not frontend.declares(root):
408
+ continue
409
+ if entry is None and (copy is None or not copy.declares):
385
410
  click.echo(
386
411
  click.style(
387
412
  f" {feature} has no frontend/, nothing to verify.",
@@ -389,7 +414,11 @@ def _verify_features(features, workspace: str) -> int:
389
414
  )
390
415
  )
391
416
  continue
392
- problems = frontend.verify(root, namespace, base_name)
417
+ problems = frontend.verify_assets(
418
+ copy.assets, lock=copy.lock, production=bool(version) or copy.installed
419
+ )
420
+ if entry is not None:
421
+ problems += _against_lock(copy, entry, base_name)
393
422
  if problems:
394
423
  failed += 1
395
424
  click.secho(
@@ -402,6 +431,101 @@ def _verify_features(features, workspace: str) -> int:
402
431
  return 1 if failed else 0
403
432
 
404
433
 
434
+ @dataclasses.dataclass
435
+ class _Copy:
436
+ """The copy of a feature --verify checks: its assets/, its frontend
437
+ lock (a package pip installed has none), whether pip installed it, and
438
+ whether it declares frontend/ (an installed one does when it carries a
439
+ manifest)."""
440
+
441
+ assets: Path
442
+ lock: Path | None
443
+ installed: bool
444
+ declares: bool
445
+
446
+
447
+ def _copy_of(workspace, product, namespace, base_name, version) -> _Copy | None:
448
+ """The working copy, the product's link to a pinned release, or else
449
+ the package pip installed. None when there is none."""
450
+ if version:
451
+ root = (
452
+ os.path.join(
453
+ workspace, product, "features", namespace, f"{base_name}@{version}"
454
+ )
455
+ if product
456
+ else None
457
+ )
458
+ else:
459
+ root = os.path.join(workspace, base_name)
460
+ if root and os.path.isdir(root):
461
+ return _Copy(
462
+ frontend.assets_dir(root, namespace, base_name),
463
+ Path(root) / frontend.FRONTEND_DIR / frontend.LOCK,
464
+ False,
465
+ frontend.declares(root),
466
+ )
467
+ import importlib.util
468
+
469
+ try:
470
+ spec = importlib.util.find_spec(f"{namespace}.{base_name}")
471
+ except (ImportError, ValueError):
472
+ spec = None
473
+ if spec is None or not spec.submodule_search_locations:
474
+ return None
475
+ assets = Path(list(spec.submodule_search_locations)[0]) / "assets"
476
+ return _Copy(assets, None, True, (assets / "meta" / "manifest.json").is_file())
477
+
478
+
479
+ def _against_lock(copy: _Copy, entry: dict, base_name: str) -> list[str]:
480
+ """What does not match between a feature's bundle and its entry in
481
+ docker/frontend.lock.json."""
482
+ problems = []
483
+ wanted = entry.get("manifest_sha256")
484
+ found = frontend.manifest_sha256(copy.assets)
485
+ if wanted and found and found != wanted:
486
+ problems.append(
487
+ "assets/meta/manifest.json is not the one docker/frontend.lock.json "
488
+ "names: run splent product:build"
489
+ )
490
+ if not copy.installed:
491
+ return problems
492
+ import importlib.metadata
493
+
494
+ distribution = entry.get("distribution") or base_name
495
+ try:
496
+ installed = importlib.metadata.version(distribution)
497
+ except importlib.metadata.PackageNotFoundError:
498
+ installed = None
499
+ if entry.get("version") and installed != entry["version"]:
500
+ problems.append(
501
+ f"pip installed {installed or 'no version'}, and "
502
+ f"docker/frontend.lock.json names {entry['version']}"
503
+ )
504
+ if found is None:
505
+ problems.append(
506
+ f"it was installed {_channel(distribution)}, without the bundle its "
507
+ "release built: release it again, so its wheel and its tag carry it"
508
+ )
509
+ return problems
510
+
511
+
512
+ def _channel(distribution: str) -> str:
513
+ """How pip installed DISTRIBUTION: from its git tag (git+) or a wheel."""
514
+ import importlib.metadata
515
+ import json
516
+
517
+ try:
518
+ direct = importlib.metadata.distribution(distribution).read_text(
519
+ "direct_url.json"
520
+ )
521
+ url = json.loads(direct or "{}")
522
+ except (importlib.metadata.PackageNotFoundError, ValueError):
523
+ url = {}
524
+ if "vcs_info" in url:
525
+ return f"from its git tag (git+{url.get('url', '')})"
526
+ return "from a wheel"
527
+
528
+
405
529
  def _feature_ref(name: str) -> str:
406
530
  """*name* as typed, with the ``splent_feature_`` prefix on the feature's
407
531
  own name: ``auth`` and ``splent_io/auth`` are ``splent_feature_auth``
@@ -237,6 +237,68 @@ def _extract_templates(src_dir: Path) -> list[str]:
237
237
 
238
238
 
239
239
  _SLOT_CALL = re.compile(r"""(?:get|render)_template_hooks\s*\(\s*['"]([^'"]+)['"]""")
240
+ _SLOT_FUNCTIONS = ("get_template_hooks", "render_template_hooks")
241
+ _MACRO = re.compile(
242
+ r"\{%-?\s*macro\s+(\w+)\s*\(\s*(\w+)(.*?)\{%-?\s*endmacro", re.DOTALL
243
+ )
244
+
245
+
246
+ def _python_slot_helpers(source: str) -> set[str]:
247
+ """The functions of SOURCE that open the slot their first argument
248
+ names: they hand it to get_template_hooks or render_template_hooks."""
249
+ try:
250
+ tree = ast.parse(source)
251
+ except SyntaxError:
252
+ return set()
253
+ helpers = set()
254
+ for node in ast.walk(tree):
255
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
256
+ continue
257
+ params = [arg.arg for arg in node.args.posonlyargs + node.args.args]
258
+ if params[:1] in (["self"], ["cls"]):
259
+ params = params[1:]
260
+ if not params:
261
+ continue
262
+ for call in ast.walk(node):
263
+ if not isinstance(call, ast.Call) or not call.args:
264
+ continue
265
+ called = call.func
266
+ if isinstance(called, ast.Attribute):
267
+ name = called.attr
268
+ elif isinstance(called, ast.Name):
269
+ name = called.id
270
+ else:
271
+ name = None
272
+ first = call.args[0]
273
+ if (
274
+ name in _SLOT_FUNCTIONS
275
+ and isinstance(first, ast.Name)
276
+ and first.id == params[0]
277
+ ):
278
+ helpers.add(node.name)
279
+ break
280
+ return helpers
281
+
282
+
283
+ def _template_slot_helpers(source: str) -> set[str]:
284
+ """The macros of SOURCE that open the slot their first argument names."""
285
+ helpers = set()
286
+ for name, param, body in _MACRO.findall(source):
287
+ opens = re.compile(
288
+ r"(?:get|render)_template_hooks\s*\(\s*" + re.escape(param) + r"\s*[,)]"
289
+ )
290
+ if opens.search(body):
291
+ helpers.add(name)
292
+ return helpers
293
+
294
+
295
+ def _helper_calls(helpers: set[str]) -> re.Pattern | None:
296
+ """A call of one of HELPERS with a slot name as its first argument, as
297
+ a template writes it: ``hub.slot_of("x.y", …)`` or ``slot_of('x.y')``."""
298
+ if not helpers:
299
+ return None
300
+ names = "|".join(re.escape(name) for name in sorted(helpers))
301
+ return re.compile(r"""\b(?:""" + names + r""")\s*\(\s*['"]([^'"]+)['"]""")
240
302
 
241
303
 
242
304
  def _extract_template_hook_slots(src_dir: Path) -> list[str]:
@@ -250,19 +312,38 @@ def _extract_template_hook_slots(src_dir: Path) -> list[str]:
250
312
  that slot is as much the feature's: a scan of the templates alone left
251
313
  it out of ``extensible.hooks``. Tests are not the feature's code, and a
252
314
  layout.* slot the code reads is the product's.
315
+
316
+ A helper may also take the slot's name from its caller: a function of
317
+ the feature's Python, or a macro of its templates, that hands its first
318
+ argument to one of the two. Then the slot is the literal a template
319
+ (or the code) passes it, ``shell.slot_of("layout.sidebar", …)``.
253
320
  """
254
321
  if not src_dir.exists():
255
322
  return []
256
323
  slots: set[str] = set()
257
324
  templates_dir = src_dir / "templates"
325
+ templates = {}
258
326
  if templates_dir.exists():
259
327
  for html_file in templates_dir.rglob("*.html"):
260
- slots.update(_SLOT_CALL.findall(html_file.read_text()))
328
+ templates[html_file] = html_file.read_text()
329
+ sources = {}
261
330
  for py_file in _feature_sources(src_dir):
262
331
  try:
263
- found = _SLOT_CALL.findall(py_file.read_text())
332
+ sources[py_file] = py_file.read_text()
264
333
  except (OSError, UnicodeDecodeError):
265
334
  continue
335
+ helpers = set()
336
+ for text in sources.values():
337
+ helpers |= _python_slot_helpers(text)
338
+ for text in templates.values():
339
+ helpers |= _template_slot_helpers(text)
340
+ calls = _helper_calls(helpers)
341
+ for text in templates.values():
342
+ slots.update(_SLOT_CALL.findall(text))
343
+ if calls:
344
+ slots.update(calls.findall(text))
345
+ for text in sources.values():
346
+ found = _SLOT_CALL.findall(text) + (calls.findall(text) if calls else [])
266
347
  slots.update(s for s in found if not s.startswith(_PRODUCT_SLOT_PREFIX))
267
348
  return sorted(slots)
268
349
 
@@ -62,6 +62,10 @@ LEGACY_REQUIREMENTS_FILE = "requirements.prod.txt"
62
62
  # Dockerfile applies it as a constraint on every pip install, so two builds of
63
63
  # one commit install the same set. Versioned in docker/.
64
64
  LOCK_FILE = "requirements.lock"
65
+ # What the features' bundles are and ship, which the image verifies the
66
+ # installed ones against (splent_cli.utils.frontend_check). Versioned in
67
+ # docker/ too.
68
+ FRONTEND_LOCK_FILE = "frontend.lock.json"
65
69
  LOCK_HEADER = (
66
70
  "# Every third-party package the production image installs, at the version\n"
67
71
  "# the last build resolved. Written by splent product:build and\n"
@@ -1202,6 +1206,30 @@ def _contracts_snapshot(workspace: str, declared_features: Sequence[str]) -> dic
1202
1206
  return {"format": CONTRACTS_FORMAT, "features": features}
1203
1207
 
1204
1208
 
1209
+ def write_frontend_lock(
1210
+ workspace: str, product_path: str, data: dict, declared_features: Sequence[str]
1211
+ ) -> str:
1212
+ """Write docker/frontend.lock.json for the features the image installs.
1213
+
1214
+ For each one with frontend/, the release, the sha256 of the manifest its
1215
+ bundle carries and what the bundle ships, with the provider that
1216
+ satisfies each of its peers; the shell; and the features still built in
1217
+ legacy mode (splent_cli.utils.frontend_check.lock). The production
1218
+ image checks every bundle pip installed against it, and the product's
1219
+ CI audits what they ship from it. Returns its path.
1220
+ """
1221
+ from splent_cli.utils import frontend_check
1222
+
1223
+ check = frontend_check.check_product(workspace, product_path, declared_features)
1224
+ path = os.path.join(product_path, "docker", FRONTEND_LOCK_FILE)
1225
+ atomic_write(
1226
+ path,
1227
+ frontend_check.lock_text(frontend_check.lock(check, data)),
1228
+ dir_owner=True,
1229
+ )
1230
+ return path
1231
+
1232
+
1205
1233
  def _not_shipped(directory, names):
1206
1234
  """Bytecode a local run of a feature left behind, never part of the bundle."""
1207
1235
  return {n for n in names if n == "__pycache__" or n.endswith(".pyc")}
@@ -1481,7 +1509,8 @@ def write_deploy_artifacts(
1481
1509
  * ``[tool.splent.runtime]`` in pyproject.toml, the framework and CLI the
1482
1510
  image installs from PyPI;
1483
1511
  * ``splent.contracts.json`` and ``docker/spl/``, the refinement metadata
1484
- and the UVL model the image reads.
1512
+ and the UVL model the image reads;
1513
+ * ``docker/frontend.lock.json``, the bundles the image verifies.
1485
1514
 
1486
1515
  All of them are versioned with the product. product:build and
1487
1516
  product:release both go through here, so a release commits exactly what
@@ -1791,6 +1820,12 @@ def write_deploy_artifacts(
1791
1820
  written.append(CONTRACTS_SNAPSHOT)
1792
1821
  click.echo(f"✅ Created: {contracts_path}")
1793
1822
 
1823
+ frontend_lock = write_frontend_lock(
1824
+ workspace, product_path, data, declared_features
1825
+ )
1826
+ written.append(f"docker/{FRONTEND_LOCK_FILE}")
1827
+ click.echo(f"✅ Created: {frontend_lock}")
1828
+
1794
1829
  staged_model = stage_spl_model(workspace, product, docker_path)
1795
1830
  if staged_model:
1796
1831
  written.append(f"docker/{SPL_STAGING_DIR}")