mxrb 0.1.2 → 0.1.4

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 (191) hide show
  1. checksums.yaml +4 -4
  2. data/bin/mxrb +458 -10
  3. data/docs/de-DE/README.md +1 -0
  4. data/docs/de-DE/oql-sql.md +42 -0
  5. data/docs/de-DE/project-structure.md +201 -2
  6. data/docs/de-DE/runtime-ruby.md +104 -0
  7. data/docs/de-DE/runtime-schema-migrations.md +44 -0
  8. data/docs/de-DE/validation-matrix.md +42 -4
  9. data/docs/en-US/README.md +1 -0
  10. data/docs/en-US/oql-sql.md +41 -0
  11. data/docs/en-US/project-structure.md +192 -2
  12. data/docs/en-US/runtime-ruby.md +100 -0
  13. data/docs/en-US/runtime-schema-migrations.md +41 -0
  14. data/docs/en-US/validation-matrix.md +39 -4
  15. data/docs/pt-BR/README.md +1 -0
  16. data/docs/pt-BR/entity-dsl.md +8 -0
  17. data/docs/pt-BR/oql-sql.md +41 -0
  18. data/docs/pt-BR/project-structure.md +197 -2
  19. data/docs/pt-BR/runtime-ruby.md +108 -0
  20. data/docs/pt-BR/runtime-schema-migrations.md +40 -0
  21. data/docs/pt-BR/validation-matrix.md +42 -5
  22. data/docs/pt-BR/writing.md +28 -0
  23. data/lib/mxrb/cli/help.rb +426 -0
  24. data/lib/mxrb/cli/release.rb +137 -0
  25. data/lib/mxrb/compiler/compatibility_analyzer.rb +1 -1
  26. data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +8 -1
  27. data/lib/mxrb/compiler/domain_document_compiler.rb +3 -0
  28. data/lib/mxrb/compiler/gallery_bundle_compiler.rb +11 -1
  29. data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +76 -19
  30. data/lib/mxrb/compiler/legacy_custom_widget_compiler.rb +159 -0
  31. data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +43 -7
  32. data/lib/mxrb/compiler/legacy_page_builder.rb +1248 -23
  33. data/lib/mxrb/compiler/packager.rb +27 -13
  34. data/lib/mxrb/compiler/page_bundle_compiler.rb +13 -4
  35. data/lib/mxrb/compiler/portable_packager.rb +16 -11
  36. data/lib/mxrb/compiler/web_bundle_builder.rb +5 -1
  37. data/lib/mxrb/compiler/web_shell_materializer.rb +42 -2
  38. data/lib/mxrb/compiler/widget_package_extractor.rb +3 -0
  39. data/lib/mxrb/domain_diagram/lifecycle.rb +272 -0
  40. data/lib/mxrb/domain_diagram.rb +409 -0
  41. data/lib/mxrb/dsl/builder.rb +62 -11
  42. data/lib/mxrb/environment.rb +226 -0
  43. data/lib/mxrb/exporter.rb +265 -45
  44. data/lib/mxrb/http/server.rb +107 -0
  45. data/lib/mxrb/initializer.rb +80 -9
  46. data/lib/mxrb/integrity/validator.rb +32 -7
  47. data/lib/mxrb/io/bson_codec.rb +42 -1
  48. data/lib/mxrb/io/mpr_file.rb +146 -3
  49. data/lib/mxrb/model/attribute.rb +3 -0
  50. data/lib/mxrb/model/entity.rb +44 -2
  51. data/lib/mxrb/model/microflow.rb +3 -1
  52. data/lib/mxrb/model/module.rb +71 -0
  53. data/lib/mxrb/model/page.rb +126 -15
  54. data/lib/mxrb/model/project.rb +3 -0
  55. data/lib/mxrb/modeler/catalog.rb +178 -0
  56. data/lib/mxrb/official_marketplace/module_package_importer.rb +23 -12
  57. data/lib/mxrb/official_marketplace/widget_package_installer.rb +15 -11
  58. data/lib/mxrb/official_marketplace.rb +9 -3
  59. data/lib/mxrb/oql/reverse_translator.rb +394 -0
  60. data/lib/mxrb/oql/server.rb +4 -8
  61. data/lib/mxrb/oql.rb +32 -4
  62. data/lib/mxrb/progress.rb +242 -0
  63. data/lib/mxrb/project_lifecycle.rb +5 -2
  64. data/lib/mxrb/ruby_app/exporter.rb +1749 -0
  65. data/lib/mxrb/ruby_app/preset.rb +506 -0
  66. data/lib/mxrb/ruby_app/session_manager.rb +122 -0
  67. data/lib/mxrb/ruby_app.rb +1361 -0
  68. data/lib/mxrb/runtime/access_control.rb +408 -0
  69. data/lib/mxrb/runtime/native.rb +1015 -93
  70. data/lib/mxrb/runtime/scheduler.rb +396 -0
  71. data/lib/mxrb/runtime/schema_migrator.rb +598 -0
  72. data/lib/mxrb/runtime/shared_store.rb +335 -0
  73. data/lib/mxrb/runtime/sqlite_store.rb +729 -0
  74. data/lib/mxrb/scaffold/generator.rb +11 -3
  75. data/lib/mxrb/team_server.rb +10 -7
  76. data/lib/mxrb/uml/activity_diagram.rb +118 -0
  77. data/lib/mxrb/uml/class_diagram.rb +118 -0
  78. data/lib/mxrb/uml/sequence_diagram.rb +112 -0
  79. data/lib/mxrb/uml/server.rb +146 -0
  80. data/lib/mxrb/uml/support.rb +35 -0
  81. data/lib/mxrb/version.rb +1 -1
  82. data/lib/mxrb/web_ui/assets/abnfDiagram-N423BO3Z-C4JD3cEC.js +1 -0
  83. data/lib/mxrb/web_ui/assets/arc-3Z53kgFp.js +1 -0
  84. data/lib/mxrb/web_ui/assets/architecture-TIHT7OUA-BStBTLea.js +1 -0
  85. data/lib/mxrb/web_ui/assets/architectureDiagram-T3A2C74G-DBwqnM4J.js +36 -0
  86. data/lib/mxrb/web_ui/assets/array-BifhSqXX.js +1 -0
  87. data/lib/mxrb/web_ui/assets/blockDiagram-VBNYF7ZC-DLtuHt-j.js +132 -0
  88. data/lib/mxrb/web_ui/assets/c4Diagram-5PPSVZJV-ConSk_pe.js +10 -0
  89. data/lib/mxrb/web_ui/assets/channel-BhPvD7Dt.js +1 -0
  90. data/lib/mxrb/web_ui/assets/chunk-2GRJ4B5K-D2HpWsf_.js +1 -0
  91. data/lib/mxrb/web_ui/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  92. data/lib/mxrb/web_ui/assets/chunk-4I5QYGJK-CiIdNwaF.js +1 -0
  93. data/lib/mxrb/web_ui/assets/chunk-5RXB4S5H-Bu_AaEkE.js +231 -0
  94. data/lib/mxrb/web_ui/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  95. data/lib/mxrb/web_ui/assets/chunk-6Q2QTUOP-wQkNmt_X.js +88 -0
  96. data/lib/mxrb/web_ui/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
  97. data/lib/mxrb/web_ui/assets/chunk-GF5L2VYU-G8MmiBzU.js +206 -0
  98. data/lib/mxrb/web_ui/assets/chunk-I66GZJ75-CulcrLPh.js +127 -0
  99. data/lib/mxrb/web_ui/assets/chunk-JQJVKLGR-ukjzcdlL.js +156 -0
  100. data/lib/mxrb/web_ui/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  101. data/lib/mxrb/web_ui/assets/chunk-KBJHAD2P-Wm35lYov.js +1 -0
  102. data/lib/mxrb/web_ui/assets/chunk-KEIR6QF5-BICK3FdT.js +161 -0
  103. data/lib/mxrb/web_ui/assets/chunk-NSK5VX7P-D2nYrCY4.js +2 -0
  104. data/lib/mxrb/web_ui/assets/chunk-QR6OTTB3-BrdIzg4Q.js +62 -0
  105. data/lib/mxrb/web_ui/assets/chunk-RYQCIY6F-CCfZMcW6.js +1 -0
  106. data/lib/mxrb/web_ui/assets/chunk-UBXNYLIW-BPEVYsFK.js +1 -0
  107. data/lib/mxrb/web_ui/assets/chunk-W5SLKNZC-2E6OPhA4.js +1 -0
  108. data/lib/mxrb/web_ui/assets/chunk-WRU74C26-CBIvOyWp.js +70 -0
  109. data/lib/mxrb/web_ui/assets/chunk-XXDRQBXY-pH58XAyl.js +1 -0
  110. data/lib/mxrb/web_ui/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  111. data/lib/mxrb/web_ui/assets/classDiagram-JCYQIIEL-xiqo4D89.js +1 -0
  112. data/lib/mxrb/web_ui/assets/classDiagram-v2-OCEON4UE-xiqo4D89.js +1 -0
  113. data/lib/mxrb/web_ui/assets/cose-bilkent-JH36ORCC-t_flPpii.js +1 -0
  114. data/lib/mxrb/web_ui/assets/cynefin-VYW2F7L2-BIqktTEv.js +1 -0
  115. data/lib/mxrb/web_ui/assets/cynefinDiagram-MW4NZA55-C2d--lut.js +62 -0
  116. data/lib/mxrb/web_ui/assets/cytoscape.esm-B-NFISlW.js +321 -0
  117. data/lib/mxrb/web_ui/assets/dagre-Buvkdvvj.js +1 -0
  118. data/lib/mxrb/web_ui/assets/dagre-VZM6K2ZE-CSz2_tb5.js +4 -0
  119. data/lib/mxrb/web_ui/assets/defaultLocale-BFoDCU3G.js +1 -0
  120. data/lib/mxrb/web_ui/assets/diagram-7IWD3JNH-DCt7u7V_.js +30 -0
  121. data/lib/mxrb/web_ui/assets/diagram-B4RE2ZJO-DqZBqkMu.js +3 -0
  122. data/lib/mxrb/web_ui/assets/diagram-LBJQPF4R-ClIYbIsI.js +24 -0
  123. data/lib/mxrb/web_ui/assets/diagram-Q27KOJAE-DFxhvthJ.js +24 -0
  124. data/lib/mxrb/web_ui/assets/diagram-UB23O5K3-BSUTXptv.js +41 -0
  125. data/lib/mxrb/web_ui/assets/dist-D2qOEbeJ.js +1 -0
  126. data/lib/mxrb/web_ui/assets/domain-mGOZ1KCG.css +1 -0
  127. data/lib/mxrb/web_ui/assets/domain-qsjXhy4v.js +1 -0
  128. data/lib/mxrb/web_ui/assets/ebnfDiagram-BXEA7PRR-B5reICEi.js +1 -0
  129. data/lib/mxrb/web_ui/assets/erDiagram-JOGREHBK-Co5Mxn2p.js +85 -0
  130. data/lib/mxrb/web_ui/assets/eventmodeling-45OFAUF4-C1ylhSks.js +1 -0
  131. data/lib/mxrb/web_ui/assets/flowDiagram-UKHOOZJN-CHXaJIaS.js +1 -0
  132. data/lib/mxrb/web_ui/assets/ganttDiagram-PKOTCBZU-DPPPaWaz.js +292 -0
  133. data/lib/mxrb/web_ui/assets/gitGraph-TEB2WS4Q-qS77ywA9.js +1 -0
  134. data/lib/mxrb/web_ui/assets/gitGraphDiagram-DS77QQ5N-DpAv6-BT.js +106 -0
  135. data/lib/mxrb/web_ui/assets/graphlib-DS17s2tU.js +1 -0
  136. data/lib/mxrb/web_ui/assets/info-DKCQHKI2-C9VD1Y4e.js +1 -0
  137. data/lib/mxrb/web_ui/assets/infoDiagram-6WML65LV-Bk1kbcCy.js +2 -0
  138. data/lib/mxrb/web_ui/assets/init-C-OQMol4.js +1 -0
  139. data/lib/mxrb/web_ui/assets/ishikawaDiagram-WSZJBQD7-0gcISBFR.js +70 -0
  140. data/lib/mxrb/web_ui/assets/journeyDiagram-NVQOT4AX-Dt0RQnlh.js +139 -0
  141. data/lib/mxrb/web_ui/assets/jsx-runtime-DV0a5kSb.js +9 -0
  142. data/lib/mxrb/web_ui/assets/kanban-definition-27J2QSJJ-DIPQM7w9.js +89 -0
  143. data/lib/mxrb/web_ui/assets/katex-CXMH3UgJ.js +257 -0
  144. data/lib/mxrb/web_ui/assets/line-C0rYD-tL.js +1 -0
  145. data/lib/mxrb/web_ui/assets/linear-BPPxWorj.js +1 -0
  146. data/lib/mxrb/web_ui/assets/map-BaFkSB1l.js +1 -0
  147. data/lib/mxrb/web_ui/assets/mermaid-parser.core-B_UPzTxa.js +7 -0
  148. data/lib/mxrb/web_ui/assets/mindmap-definition-FAOFIHXS-CdJRSMuk.js +96 -0
  149. data/lib/mxrb/web_ui/assets/modeler-BNqHthwk.css +1 -0
  150. data/lib/mxrb/web_ui/assets/modeler-CQmqflbg.js +1 -0
  151. data/lib/mxrb/web_ui/assets/ordinal-BDEzSJ7C.js +1 -0
  152. data/lib/mxrb/web_ui/assets/packet-7NZHBO7P-CwQ4NSBs.js +1 -0
  153. data/lib/mxrb/web_ui/assets/path-fybaL0A-.js +1 -0
  154. data/lib/mxrb/web_ui/assets/pegDiagram-VL7TDLO6-D40S7ppx.js +1 -0
  155. data/lib/mxrb/web_ui/assets/pie-RZYD4A2V-CJroHMKh.js +1 -0
  156. data/lib/mxrb/web_ui/assets/pieDiagram-7S7Q4E2Y-QB54__BV.js +39 -0
  157. data/lib/mxrb/web_ui/assets/quadrantDiagram-CIZ2JOQS-DBhDS6ag.js +7 -0
  158. data/lib/mxrb/web_ui/assets/radar-I7S5WNFK-DdIUwd2d.js +1 -0
  159. data/lib/mxrb/web_ui/assets/railroad-3IZDKUUU-B1Lw6Xyq.js +1 -0
  160. data/lib/mxrb/web_ui/assets/railroad-abnf-AHOZXSZD-CQgQoeak.js +1 -0
  161. data/lib/mxrb/web_ui/assets/railroad-ebnf-EBAXGLYW-D7qEQ2x8.js +1 -0
  162. data/lib/mxrb/web_ui/assets/railroad-peg-LSFZ7HO6-C005Vxbb.js +1 -0
  163. data/lib/mxrb/web_ui/assets/railroadDiagram-AXF67PYL-BBP2qk7M.js +1 -0
  164. data/lib/mxrb/web_ui/assets/requirementDiagram-LRYGKXZP-D2cLsAoI.js +84 -0
  165. data/lib/mxrb/web_ui/assets/rough.esm-Dy-Kn_BL.js +1 -0
  166. data/lib/mxrb/web_ui/assets/sankeyDiagram-W5VNT64P-Bpi3FiP2.js +40 -0
  167. data/lib/mxrb/web_ui/assets/sequenceDiagram-SI44F4Z6-DCRcDlVe.js +162 -0
  168. data/lib/mxrb/web_ui/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
  169. data/lib/mxrb/web_ui/assets/src-CvxfUak2.js +1 -0
  170. data/lib/mxrb/web_ui/assets/stateDiagram-OKZ733FA-C2GCEoxl.js +1 -0
  171. data/lib/mxrb/web_ui/assets/stateDiagram-v2-UEYNNEHI-D2gWgedh.js +1 -0
  172. data/lib/mxrb/web_ui/assets/swimlanes-SLNWSIFB-CRI9irEW.js +2 -0
  173. data/lib/mxrb/web_ui/assets/swimlanesDiagram-ULZ7WXOC-C5Wb_-hZ.js +8 -0
  174. data/lib/mxrb/web_ui/assets/theme-_mxc7BdY.css +1 -0
  175. data/lib/mxrb/web_ui/assets/timeline-definition-Z64GVDOM-B1-mNg-h.js +120 -0
  176. data/lib/mxrb/web_ui/assets/treeView-QDETBFTQ-7CTvIwKq.js +1 -0
  177. data/lib/mxrb/web_ui/assets/treemap-6X3UGDF4-CtL22pq5.js +1 -0
  178. data/lib/mxrb/web_ui/assets/uml-C3KAFGTq.js +51 -0
  179. data/lib/mxrb/web_ui/assets/uml-Dn-qd01X.css +1 -0
  180. data/lib/mxrb/web_ui/assets/vennDiagram-T6HMQDX7-BzVnUEAy.js +34 -0
  181. data/lib/mxrb/web_ui/assets/wardley-OPB4EBWU-JNi-QNag.js +1 -0
  182. data/lib/mxrb/web_ui/assets/wardleyDiagram-T6FBY63Y-BUwmAQ-B.js +78 -0
  183. data/lib/mxrb/web_ui/assets/xychartDiagram-ELKLHX3M-DEQS8sKp.js +7 -0
  184. data/lib/mxrb/web_ui/domain.html +15 -0
  185. data/lib/mxrb/web_ui/modeler.html +16 -0
  186. data/lib/mxrb/web_ui/uml.html +31 -0
  187. data/lib/mxrb/web_ui.rb +89 -0
  188. data/lib/mxrb/widget_certification.rb +208 -0
  189. data/lib/mxrb/writer.rb +278 -41
  190. data/lib/mxrb.rb +38 -8
  191. metadata +157 -5
@@ -19,16 +19,24 @@ module Mxrb
19
19
  @deployment = File.expand_path(deployment || File.join(@project_root, 'deployment'))
20
20
  end
21
21
 
22
- def pack(output:, force: false)
23
- output_path = File.expand_path(output)
24
- validate_input!(output_path, force:)
25
- version = Mxrb.open(@mpr_path, &:mendix_version)
26
- adapter = Adapter.for(version)
27
- metadata = adapter.validate_deployment!(@deployment)
28
- adapter.validate_freshness!(@mpr_path, @deployment)
29
- files, directories = inventory
30
- write_atomically(output_path, files, directories)
31
- package_result(output_path, version, metadata)
22
+ def pack(output:, force: false) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
23
+ Progress.with("Packing #{File.basename(@mpr_path)}") do |progress|
24
+ output_path = File.expand_path(output)
25
+ validate_input!(output_path, force:)
26
+ version = Mxrb.open(@mpr_path, &:mendix_version)
27
+ adapter = Adapter.for(version)
28
+ metadata = adapter.validate_deployment!(@deployment)
29
+ adapter.validate_freshness!(@mpr_path, @deployment)
30
+ files, directories = inventory
31
+ progress.update(
32
+ current: 3, total: files.size + directories.size + 4,
33
+ detail: "#{files.size} files"
34
+ )
35
+ write_atomically(output_path, files, directories, progress)
36
+ package_result(output_path, version, metadata).tap do
37
+ progress.advance(detail: 'package checksum')
38
+ end
39
+ end
32
40
  end
33
41
 
34
42
  private
@@ -69,13 +77,19 @@ module Mxrb
69
77
  path.delete_prefix("#{@deployment}/").tr('\\', '/')
70
78
  end
71
79
 
72
- def write_atomically(output, files, directories)
80
+ def write_atomically(output, files, directories, progress) # rubocop:disable Metrics/MethodLength
73
81
  FileUtils.mkdir_p(File.dirname(output))
74
82
  Dir.mktmpdir('mxrb-mda-', File.dirname(output)) do |tmpdir|
75
83
  temporary = File.join(tmpdir, 'package.mda')
76
84
  Zip::File.open(temporary, create: true) do |archive|
77
- directories.each { add_directory(archive, _1) }
78
- files.each { add_file(archive, _1) }
85
+ directories.each do |directory|
86
+ add_directory(archive, directory)
87
+ progress.advance(detail: relative(directory))
88
+ end
89
+ files.each do |file|
90
+ add_file(archive, file)
91
+ progress.advance(detail: relative(file))
92
+ end
79
93
  end
80
94
  FileUtils.mv(temporary, output, force: true)
81
95
  end
@@ -341,7 +341,10 @@ module Mxrb
341
341
  return grid.render
342
342
  end
343
343
 
344
- gallery = GalleryBundleCompiler.new(@source, @qualified_name, widget)
344
+ gallery = GalleryBundleCompiler.new(
345
+ @source, @qualified_name, widget,
346
+ render_widgets: ->(widgets, scope, entity) { render_scoped_widgets(widgets, scope, entity) }
347
+ )
345
348
  if gallery.supported?
346
349
  @uses_gallery = true
347
350
  nano_reference = nanoflow_reference(gallery.data_source.nanoflow_name) if gallery.data_source.nanoflow?
@@ -1818,17 +1821,21 @@ module Mxrb
1818
1821
  if @generic_widgets&.any?
1819
1822
  imports.concat([
1820
1823
  'import { AttributeProperty } from "mendix/AttributeProperty";',
1824
+ 'import { ListAttributeProperty } from "mendix/ListAttributeProperty";',
1821
1825
  'import { ExpressionProperty } from "mendix/ExpressionProperty";',
1822
1826
  'import { ListExpressionProperty } from "mendix/ListExpressionProperty";',
1823
- 'import { DatabaseObjectListProperty } from "mendix/DatabaseObjectListProperty";'
1827
+ 'import { DatabaseObjectListProperty } from "mendix/DatabaseObjectListProperty";',
1828
+ 'import { SelectionProperty } from "mendix/SelectionProperty";',
1829
+ 'import { TemplatedWidgetProperty } from "mendix/TemplatedWidgetProperty";'
1824
1830
  ])
1825
1831
  end
1826
1832
  if @uses_data_grid
1827
1833
  imports.concat([
1828
1834
  'import { DatabaseObjectListProperty } from "mendix/DatabaseObjectListProperty";',
1829
- 'import { AttributeProperty } from "mendix/AttributeProperty";',
1835
+ 'import { ListAttributeProperty } from "mendix/ListAttributeProperty";',
1830
1836
  'import { ExpressionProperty } from "mendix/ExpressionProperty";',
1831
1837
  'import { ListExpressionProperty } from "mendix/ListExpressionProperty";',
1838
+ 'import { SelectionProperty } from "mendix/SelectionProperty";',
1832
1839
  'import { TemplatedWidgetProperty } from "mendix/TemplatedWidgetProperty";',
1833
1840
  'import Datagrid from "../widgets/com/mendix/widget/web/datagrid/Datagrid.mjs";'
1834
1841
  ])
@@ -1988,7 +1995,9 @@ module Mxrb
1988
1995
 
1989
1996
  def render_content(content)
1990
1997
  JSON.pretty_generate(content).sub(/\A\{/, '{').sub(/\}\z/, '}')
1991
- .gsub(/"(renderKey =>[^\n]+)"/) { Regexp.last_match(1).gsub('\\"', '"') }
1998
+ .gsub(/"(renderKey =>[^\n]+)"/) do
1999
+ JSON.parse(%("#{Regexp.last_match(1)}"))
2000
+ end
1992
2001
  end
1993
2002
 
1994
2003
  def layout
@@ -309,17 +309,22 @@ module Mxrb
309
309
  @mendix_home = mendix_home && File.expand_path(mendix_home)
310
310
  end
311
311
 
312
- def pack(output:, force: false)
313
- output_path = File.expand_path(output)
314
- validate_output!(output_path, force:)
315
- version = Mxrb.open(@mpr_path, &:mendix_version)
316
- adapter = Adapter.for(version)
317
- metadata = adapter.validate_deployment!(@deployment)
318
- adapter.validate_freshness!(@mpr_path, @deployment)
319
- runtime = runtime_root(version)
320
- validate_runtime!(runtime)
321
- PortableArchiveWriter.new(@deployment, runtime, metadata).write(output_path)
322
- result(output_path, version, metadata)
312
+ def pack(output:, force: false) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
313
+ Progress.with("Packing portable Runtime for #{File.basename(@mpr_path)}") do |progress|
314
+ output_path = File.expand_path(output)
315
+ validate_output!(output_path, force:)
316
+ progress.update(detail: 'validating deployment')
317
+ version = Mxrb.open(@mpr_path, &:mendix_version)
318
+ adapter = Adapter.for(version)
319
+ metadata = adapter.validate_deployment!(@deployment)
320
+ adapter.validate_freshness!(@mpr_path, @deployment)
321
+ runtime = runtime_root(version)
322
+ validate_runtime!(runtime)
323
+ progress.update(detail: 'writing Runtime archive')
324
+ PortableArchiveWriter.new(@deployment, runtime, metadata).write(output_path)
325
+ progress.update(detail: 'calculating checksum')
326
+ result(output_path, version, metadata)
327
+ end
323
328
  end
324
329
 
325
330
  private
@@ -120,7 +120,11 @@ module Mxrb
120
120
  constants: { LAYOUT_SCOPE_ID_PREFIX: 'l' },
121
121
  registerServiceWorker: false, enableServiceWorkerCaching: true
122
122
  }
123
- File.write(path, "import { startApp } from \"mendix\";\n\nstartApp(#{JSON.pretty_generate(payload)});\n")
123
+ File.write(
124
+ path,
125
+ "import { startApp } from \"mendix\";\n\n" \
126
+ "startApp(#{JSON.pretty_generate(payload)});\n"
127
+ )
124
128
  end
125
129
 
126
130
  def languages
@@ -24,7 +24,8 @@ module Mxrb
24
24
  def materialize
25
25
  return 0 unless File.directory?(@web)
26
26
 
27
- changed = html_files.count { render_html(_1) }
27
+ changed = materialize_widget_stylesheet ? 1 : 0
28
+ changed += html_files.count { render_html(_1) }
28
29
  changed += materialize_dynamic_imports
29
30
  write_missing(File.join(@web, 'js', 'login_i18n.js'), login_i18n)
30
31
  write_missing(File.join(@web, 'lib', 'bootstrap', 'css', 'bootstrap.min.css'), login_styles)
@@ -51,6 +52,29 @@ module Mxrb
51
52
 
52
53
  def html_files = Dir.glob(File.join(@web, '*.html')).sort
53
54
 
55
+ def materialize_widget_stylesheet
56
+ destination = File.join(@web, 'widgets', 'mxrb-widgets.css')
57
+ styles = Dir.glob(File.join(@web, 'widgets', '**', '*.css')).sort
58
+ styles.delete(destination)
59
+ return remove_widget_stylesheet(destination) if styles.empty?
60
+
61
+ rendered = widget_stylesheet(styles)
62
+ return false if File.file?(destination) && File.binread(destination) == rendered
63
+
64
+ File.binwrite(destination, rendered)
65
+ true
66
+ end
67
+
68
+ def remove_widget_stylesheet(path)
69
+ existed = File.exist?(path)
70
+ FileUtils.rm_f(path)
71
+ existed
72
+ end
73
+
74
+ def widget_stylesheet(styles)
75
+ styles.map { File.binread(_1) }.join("\n") << "\n"
76
+ end
77
+
54
78
  def render_javascript(source)
55
79
  rendered = source.gsub(DYNAMIC_PAGE_CACHE) do
56
80
  %(`?#{cache_token}\${#{Regexp.last_match(1)}}`)
@@ -68,7 +92,7 @@ module Mxrb
68
92
  rendered = rendered.gsub('{{themecss}}', theme_css)
69
93
  rendered = rendered.gsub('{{manifest}}', manifest)
70
94
  rendered = rendered.gsub(PLACEHOLDER, '')
71
- rendered = inject_navigation_compatibility(rendered) if File.basename(path) == 'index.html'
95
+ rendered = render_application_html(rendered) if File.basename(path) == 'index.html'
72
96
  return false if source == rendered
73
97
 
74
98
  File.write(path, rendered)
@@ -115,6 +139,22 @@ module Mxrb
115
139
  html.sub('</head>', "#{navigation_compatibility}\n</head>")
116
140
  end
117
141
 
142
+ def render_application_html(html)
143
+ inject_navigation_compatibility(inject_widget_stylesheet(html))
144
+ end
145
+
146
+ def inject_widget_stylesheet(html)
147
+ return html unless File.file?(File.join(@web, 'widgets', 'mxrb-widgets.css'))
148
+
149
+ stylesheet = '<link data-mxrb-widget-styles rel="stylesheet" ' \
150
+ "href=\"widgets/mxrb-widgets.css?#{cache_token}\">"
151
+ return html.sub(/<link\b[^>]*data-mxrb-widget-styles[^>]*>/, stylesheet) if
152
+ html.include?('data-mxrb-widget-styles')
153
+ return html unless html.include?('</head>')
154
+
155
+ html.sub('</head>', "#{stylesheet}\n</head>")
156
+ end
157
+
118
158
  def navigation_compatibility # rubocop:disable Metrics/MethodLength
119
159
  <<~HTML.chomp
120
160
  <script data-mxrb-navigation-compat="#{CACHE_REVISION}">
@@ -10,10 +10,13 @@ module Mxrb
10
10
  class WidgetPackageExtractor
11
11
  def initialize(project_root, web_root)
12
12
  @source = File.join(project_root, 'widgets')
13
+ @web_root = web_root
13
14
  @destination = File.join(web_root, 'widgets')
14
15
  end
15
16
 
16
17
  def extract
18
+ FileUtils.rm_f(File.join(@web_root, 'mxrb-widgets.css'))
19
+ FileUtils.rm_f(File.join(@destination, 'mxrb-widgets.css'))
17
20
  return 0 unless File.directory?(@source)
18
21
 
19
22
  FileUtils.mkdir_p(@destination)
@@ -0,0 +1,272 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'fileutils'
5
+ require 'json'
6
+ require 'net/http'
7
+ require 'rbconfig'
8
+ require 'securerandom'
9
+ require 'time'
10
+
11
+ module Mxrb
12
+ module DomainDiagram
13
+ # Background lifecycle for the ER editor. Control happens through a
14
+ # token-authenticated loopback endpoint rather than a potentially stale PID.
15
+ # rubocop:disable Metrics
16
+ class Lifecycle
17
+ STATE_ROOT_ENV = 'MXRB_DIAGRAM_STATE_ROOT'
18
+ START_TIMEOUT = 8
19
+ STOP_TIMEOUT = 8
20
+
21
+ Status = Data.define(:state, :source, :output, :url, :pid, :log) do
22
+ def running? = state == 'running'
23
+ def to_h = { state:, source:, output:, url:, pid:, log: }.compact
24
+ end
25
+
26
+ attr_reader :source, :state_root, :executable
27
+
28
+ def initialize(source, state_root: self.class.default_state_root, executable: $PROGRAM_NAME)
29
+ @source = File.expand_path(source)
30
+ @state_root = File.expand_path(state_root)
31
+ @executable = File.expand_path(executable)
32
+ end
33
+
34
+ def self.default_state_root
35
+ configured = ENV[STATE_ROOT_ENV]
36
+ return configured unless configured.to_s.empty?
37
+
38
+ File.join(Dir.home, '.local', 'state', 'mxrb', 'diagram-er')
39
+ end
40
+
41
+ def up(output: nil, modules: nil, port: nil, force: false)
42
+ previous = read_state
43
+ raise ArgumentError, "diagram already running at #{url_for(previous)}" if running?(previous)
44
+ raise ArgumentError, 'managed diagram is already starting' if starting?(previous)
45
+
46
+ settings = settings_for(previous, output:, modules:, port:)
47
+ managed_paths = prepare_output(previous, settings, force:)
48
+ token = SecureRandom.hex(24)
49
+ state = settings.merge(
50
+ 'version' => 1, 'source' => source, 'token' => token, 'state' => 'starting',
51
+ 'pid' => nil, 'log' => log_path, 'managed_paths' => managed_paths,
52
+ 'created_at' => Time.now.utc.iso8601
53
+ )
54
+ write_state(state)
55
+ pid = spawn_worker(state)
56
+ Process.detach(pid)
57
+ write_state(read_state.merge('pid' => pid))
58
+ wait_until(START_TIMEOUT) { running?(read_state, token:) }
59
+ status
60
+ rescue StandardError
61
+ mark_stopped(token) if defined?(token) && token
62
+ raise
63
+ end
64
+
65
+ def status
66
+ state = read_state
67
+ return Status.new('absent', source, nil, nil, nil, nil) unless state
68
+
69
+ lifecycle_state = if running?(state)
70
+ 'running'
71
+ elsif starting?(state)
72
+ 'starting'
73
+ else
74
+ 'stopped'
75
+ end
76
+ Status.new(
77
+ lifecycle_state, source, state['output'], url_for(state), state['pid'], state['log']
78
+ )
79
+ end
80
+
81
+ def down
82
+ state = read_state or raise ArgumentError, "no managed diagram for #{source}"
83
+ unless running?(state)
84
+ raise ArgumentError, 'managed diagram is still starting; retry in a moment' if starting?(state)
85
+
86
+ mark_stopped(state.fetch('token'))
87
+ return status
88
+ end
89
+
90
+ response = request(state, Net::HTTP::Post, '/api/admin/shutdown')
91
+ raise ArgumentError, 'managed diagram refused the shutdown request' unless response.is_a?(Net::HTTPSuccess)
92
+
93
+ wait_until(STOP_TIMEOUT) { !running?(read_state) }
94
+ mark_stopped(state.fetch('token'))
95
+ status
96
+ end
97
+
98
+ def destroy(confirm: false)
99
+ raise ArgumentError, 'destroy requires --yes' unless confirm
100
+
101
+ state = read_state or raise ArgumentError, "no managed diagram for #{source}"
102
+ current = status
103
+ down if %w[running starting].include?(current.state)
104
+ Array(state['managed_paths']).reverse_each do |path|
105
+ remove_managed_path(path, state.fetch('output'))
106
+ end
107
+ FileUtils.remove_entry_secure(instance_dir) if File.exist?(instance_dir)
108
+ Status.new('absent', source, nil, nil, nil, nil)
109
+ end
110
+
111
+ # Internal worker entrypoint used by `mxrb diagram-er __serve`.
112
+ def run_worker(output:, modules:, port:, token:)
113
+ state = read_state or raise ArgumentError, 'diagram lifecycle state is missing'
114
+ raise ArgumentError, 'diagram lifecycle token does not match' unless state['token'] == token
115
+
116
+ server = Server.new(
117
+ source, output:, modules:, port:, reuse: true, lifecycle_token: token
118
+ )
119
+ trap('INT') { Thread.new { server.shutdown } }
120
+ trap('TERM') { Thread.new { server.shutdown } }
121
+ server.start do
122
+ write_state(state.merge(
123
+ 'pid' => Process.pid, 'state' => 'running',
124
+ 'started_at' => Time.now.utc.iso8601
125
+ ))
126
+ end
127
+ ensure
128
+ mark_stopped(token) if defined?(token) && token
129
+ end
130
+
131
+ private
132
+
133
+ def settings_for(previous, output:, modules:, port:)
134
+ selected_output = File.expand_path(output || previous&.fetch('output', nil) || default_output)
135
+ if previous && output && selected_output != File.expand_path(previous.fetch('output'))
136
+ raise ArgumentError, 'destroy the existing managed diagram before changing --output'
137
+ end
138
+
139
+ {
140
+ 'output' => selected_output,
141
+ 'modules' => modules.nil? ? Array(previous&.fetch('modules', [])) : Array(modules),
142
+ 'port' => Integer(port || previous&.fetch('port', nil) || 4568),
143
+ 'host' => '127.0.0.1'
144
+ }
145
+ end
146
+
147
+ def prepare_output(previous, settings, force:)
148
+ output = settings.fetch('output')
149
+ if previous && File.file?(output) && !force
150
+ Server.new(source, output:, port: settings.fetch('port'), reuse: true)
151
+ return Array(previous['managed_paths'])
152
+ end
153
+
154
+ server = Server.new(source, output:, port: settings.fetch('port'), force:)
155
+ (Array(previous&.fetch('managed_paths', [])) + server.managed_paths).uniq
156
+ end
157
+
158
+ def spawn_worker(state)
159
+ arguments = [
160
+ RbConfig.ruby, executable, 'diagram-er', '__serve', source,
161
+ '--output', state.fetch('output'), '--port', state.fetch('port').to_s,
162
+ '--token', state.fetch('token'), '--state-root', state_root
163
+ ]
164
+ Array(state['modules']).each { arguments.concat(['--module', _1.to_s]) }
165
+ prepare_instance_dir
166
+ FileUtils.touch(state.fetch('log'))
167
+ File.chmod(0o600, state.fetch('log'))
168
+ Process.spawn(*arguments, out: state.fetch('log'), err: %i[child out])
169
+ end
170
+
171
+ def starting?(state)
172
+ return false unless state&.fetch('state', nil) == 'starting'
173
+
174
+ created_at = Time.iso8601(state.fetch('created_at'))
175
+ Time.now.utc - created_at <= START_TIMEOUT
176
+ rescue ArgumentError, KeyError
177
+ false
178
+ end
179
+
180
+ def running?(state, token: state&.fetch('token', nil))
181
+ return false unless state && token
182
+
183
+ response = request(state, Net::HTTP::Get, '/api/admin/status', token:)
184
+ response&.is_a?(Net::HTTPSuccess) == true
185
+ end
186
+
187
+ def request(state, request_class, path, token: state.fetch('token'))
188
+ request = request_class.new(path)
189
+ request['X-MXRB-Lifecycle-Token'] = token
190
+ Net::HTTP.start(
191
+ state.fetch('host'), Integer(state.fetch('port')),
192
+ open_timeout: 0.25, read_timeout: 0.5
193
+ ) { _1.request(request) }
194
+ rescue IOError, SystemCallError, Timeout::Error, SocketError
195
+ nil
196
+ end
197
+
198
+ def wait_until(timeout)
199
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
200
+ loop do
201
+ return true if yield
202
+ if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
203
+ raise ArgumentError, "diagram lifecycle timed out; see #{log_path}"
204
+ end
205
+
206
+ sleep 0.05
207
+ end
208
+ end
209
+
210
+ def mark_stopped(token)
211
+ state = read_state
212
+ return unless state && state['token'] == token
213
+
214
+ write_state(state.merge(
215
+ 'pid' => nil, 'state' => 'stopped',
216
+ 'stopped_at' => Time.now.utc.iso8601
217
+ ))
218
+ end
219
+
220
+ def remove_managed_path(path, output)
221
+ expanded = File.expand_path(path)
222
+ output = File.expand_path(output)
223
+ external = File.join(File.dirname(output), 'mprcontents')
224
+ raise ArgumentError, 'refusing to remove the source MPR' if expanded == source
225
+
226
+ unless expanded == output || expanded == external
227
+ raise ArgumentError, "refusing to remove unmanaged path #{expanded}"
228
+ end
229
+ return unless File.exist?(expanded) || File.symlink?(expanded)
230
+
231
+ if File.directory?(expanded) && !File.symlink?(expanded)
232
+ FileUtils.remove_entry_secure(expanded)
233
+ else
234
+ FileUtils.rm_f(expanded)
235
+ end
236
+ end
237
+
238
+ def read_state
239
+ JSON.parse(File.binread(state_path))
240
+ rescue Errno::ENOENT
241
+ nil
242
+ rescue JSON::ParserError => e
243
+ raise ArgumentError, "invalid diagram lifecycle state: #{e.message}"
244
+ end
245
+
246
+ def write_state(payload)
247
+ prepare_instance_dir
248
+ temporary = "#{state_path}.#{Process.pid}.tmp"
249
+ File.binwrite(temporary, JSON.pretty_generate(payload))
250
+ File.chmod(0o600, temporary)
251
+ File.rename(temporary, state_path)
252
+ ensure
253
+ FileUtils.rm_f(temporary) if defined?(temporary) && temporary
254
+ end
255
+
256
+ def default_output
257
+ File.join(File.dirname(source), "#{File.basename(source, '.mpr')}.domain-layout.mpr")
258
+ end
259
+
260
+ def prepare_instance_dir
261
+ FileUtils.mkdir_p(instance_dir)
262
+ File.chmod(0o700, instance_dir)
263
+ end
264
+
265
+ def url_for(state) = "http://#{state.fetch('host')}:#{state.fetch('port')}"
266
+ def instance_dir = File.join(state_root, Digest::SHA256.hexdigest(source)[0, 24])
267
+ def state_path = File.join(instance_dir, 'state.json')
268
+ def log_path = File.join(instance_dir, 'server.log')
269
+ end
270
+ # rubocop:enable Metrics
271
+ end
272
+ end