@cavulsqa/create 2.9.3 → 2.10.0

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 (199) hide show
  1. package/README.md +12 -1
  2. package/bin/create.mjs +18 -25
  3. package/lib/templates.mjs +56 -0
  4. package/package.json +13 -5
  5. package/templates/f7-app/auto-imports.d.ts +9 -0
  6. package/templates/f7-app/package.json +6 -3
  7. package/templates/f7-app/src/env.d.ts +1 -0
  8. package/templates/f7-app/src/main.ts +3 -0
  9. package/templates/f7-app/src/plugins/bootstrapError.ts +2 -0
  10. package/templates/f7-app/src/plugins/recorder.plugin.ts +155 -0
  11. package/templates/f7-app/src/shared/recorder/capacitorSink.ts +124 -0
  12. package/templates/f7-app/vite.config.ts +1 -0
  13. package/templates/m3e-app/.claude/rules/data-fetching.md +68 -0
  14. package/templates/m3e-app/.claude/rules/database.md +106 -0
  15. package/templates/m3e-app/.claude/rules/m3e-ui.md +48 -0
  16. package/templates/m3e-app/.claude/rules/modules.md +43 -0
  17. package/templates/m3e-app/.claude/rules/native.md +60 -0
  18. package/templates/m3e-app/.claude/skills/m3-expressive/SKILL.md +68 -0
  19. package/templates/m3e-app/.claude/skills/m3-expressive/color.md +44 -0
  20. package/templates/m3e-app/.claude/skills/m3-expressive/components.md +325 -0
  21. package/templates/m3e-app/.claude/skills/m3-expressive/layout.md +42 -0
  22. package/templates/m3e-app/.claude/skills/m3-expressive/motion.md +51 -0
  23. package/templates/m3e-app/.claude/skills/m3-expressive/shapes-type.md +48 -0
  24. package/templates/m3e-app/.claude/skills/m3-expressive/sources.md +53 -0
  25. package/templates/m3e-app/.claude/skills/module-architecture/SKILL.md +65 -0
  26. package/templates/m3e-app/.claude/skills/module-architecture/file-templates.md +178 -0
  27. package/templates/m3e-app/.claude/skills/reactive-data/SKILL.md +90 -0
  28. package/templates/m3e-app/.claude/skills/reactive-data/testing.md +55 -0
  29. package/templates/m3e-app/.env.example +32 -0
  30. package/templates/m3e-app/CLAUDE.md +93 -0
  31. package/templates/m3e-app/auto-imports.d.ts +851 -0
  32. package/templates/m3e-app/capacitor.config.ts +44 -0
  33. package/templates/m3e-app/components.d.ts +252 -0
  34. package/templates/m3e-app/index.html +16 -0
  35. package/templates/m3e-app/package.json +107 -0
  36. package/templates/m3e-app/src/App.vue +129 -0
  37. package/templates/m3e-app/src/app/pragmas.config.ts +35 -0
  38. package/templates/m3e-app/src/app/scroll.config.ts +20 -0
  39. package/templates/m3e-app/src/app/storage.config.ts +65 -0
  40. package/templates/m3e-app/src/app/tabs.ts +33 -0
  41. package/templates/m3e-app/src/app/theme.config.ts +51 -0
  42. package/templates/m3e-app/src/assets/css/app.css +18 -0
  43. package/templates/m3e-app/src/assets/css/base.css +53 -0
  44. package/templates/m3e-app/src/assets/css/layout/container-transform.css +113 -0
  45. package/templates/m3e-app/src/assets/css/layout/shell.css +69 -0
  46. package/templates/m3e-app/src/assets/css/layout/transitions.css +121 -0
  47. package/templates/m3e-app/src/assets/css/m3e.css +6 -0
  48. package/templates/m3e-app/src/assets/css/theme/framework7.css +18 -0
  49. package/templates/m3e-app/src/assets/css/theme/tailwind.css +365 -0
  50. package/templates/m3e-app/src/domains/benchmark/benchmark.dataset.ts +209 -0
  51. package/templates/m3e-app/src/domains/benchmark/benchmark.suite.ts +662 -0
  52. package/templates/m3e-app/src/domains/sales/sales.repository.ts +458 -0
  53. package/templates/m3e-app/src/env.d.ts +40 -0
  54. package/templates/m3e-app/src/locales/ar.json +1260 -0
  55. package/templates/m3e-app/src/locales/en.json +1260 -0
  56. package/templates/m3e-app/src/locales/fr.json +1260 -0
  57. package/templates/m3e-app/src/main.ts +41 -0
  58. package/templates/m3e-app/src/modules/demo/components/DemoBenchmark.vue +119 -0
  59. package/templates/m3e-app/src/modules/demo/components/DemoBusLog.vue +63 -0
  60. package/templates/m3e-app/src/modules/demo/components/DemoCreateOrderSheet.vue +203 -0
  61. package/templates/m3e-app/src/modules/demo/components/DemoMetricsSheet.vue +75 -0
  62. package/templates/m3e-app/src/modules/demo/components/DemoOrderList.vue +75 -0
  63. package/templates/m3e-app/src/modules/demo/components/DemoPipelineBenchmark.vue +65 -0
  64. package/templates/m3e-app/src/modules/demo/components/DemoStatCards.vue +71 -0
  65. package/templates/m3e-app/src/modules/demo/composables/useBenchmark.ts +139 -0
  66. package/templates/m3e-app/src/modules/demo/composables/useOrderStatus.ts +64 -0
  67. package/templates/m3e-app/src/modules/demo/composables/useReactiveDemo.ts +171 -0
  68. package/templates/m3e-app/src/modules/demo/router/routes/demo.routes.ts +22 -0
  69. package/templates/m3e-app/src/modules/demo/views/DemoView.vue +210 -0
  70. package/templates/m3e-app/src/modules/demo/views/OrderDetailView.vue +136 -0
  71. package/templates/m3e-app/src/modules/demo/views/OrderSearchView.vue +74 -0
  72. package/templates/m3e-app/src/modules/gallery/components/GalleryBlock.vue +16 -0
  73. package/templates/m3e-app/src/modules/gallery/components/GalleryCarouselTile.vue +71 -0
  74. package/templates/m3e-app/src/modules/gallery/components/GalleryCustomerForm.vue +76 -0
  75. package/templates/m3e-app/src/modules/gallery/components/GalleryProofOfDelivery.vue +69 -0
  76. package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaDay.vue +106 -0
  77. package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaMonth.vue +40 -0
  78. package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaVisitList.vue +45 -0
  79. package/templates/m3e-app/src/modules/gallery/components/chat/ChatAttachSheet.vue +106 -0
  80. package/templates/m3e-app/src/modules/gallery/components/chat/ChatContactPicker.vue +85 -0
  81. package/templates/m3e-app/src/modules/gallery/components/chat/ChatInviteComposer.vue +131 -0
  82. package/templates/m3e-app/src/modules/gallery/components/chat/ChatPollComposer.vue +148 -0
  83. package/templates/m3e-app/src/modules/gallery/components/chat/ChatRecentPhotos.vue +62 -0
  84. package/templates/m3e-app/src/modules/gallery/components/inputs/InputsAccount.vue +79 -0
  85. package/templates/m3e-app/src/modules/gallery/components/inputs/InputsTags.vue +53 -0
  86. package/templates/m3e-app/src/modules/gallery/components/inputs/InputsVerification.vue +73 -0
  87. package/templates/m3e-app/src/modules/gallery/components/inputs/PasswordStrength.vue +37 -0
  88. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryButtons.vue +128 -0
  89. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryCarousels.vue +236 -0
  90. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryCharts.vue +84 -0
  91. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryFabs.vue +70 -0
  92. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryInputs.vue +219 -0
  93. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryNavigation.vue +141 -0
  94. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryOverlays.vue +368 -0
  95. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryPickers.vue +175 -0
  96. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryProgress.vue +118 -0
  97. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryScale.vue +103 -0
  98. package/templates/m3e-app/src/modules/gallery/components/sections/GallerySelection.vue +219 -0
  99. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryShapes.vue +48 -0
  100. package/templates/m3e-app/src/modules/gallery/components/sections/GallerySurfaces.vue +213 -0
  101. package/templates/m3e-app/src/modules/gallery/components/sections/GalleryTables.vue +185 -0
  102. package/templates/m3e-app/src/modules/gallery/components/surfaces/SurfacesContainerTransform.vue +46 -0
  103. package/templates/m3e-app/src/modules/gallery/composables/chatTeam.ts +18 -0
  104. package/templates/m3e-app/src/modules/gallery/composables/composeKind.ts +2 -0
  105. package/templates/m3e-app/src/modules/gallery/composables/currentPosition.ts +35 -0
  106. package/templates/m3e-app/src/modules/gallery/composables/demoOrders.ts +29 -0
  107. package/templates/m3e-app/src/modules/gallery/composables/featuredStories.ts +73 -0
  108. package/templates/m3e-app/src/modules/gallery/composables/fieldFormats.ts +36 -0
  109. package/templates/m3e-app/src/modules/gallery/composables/openExternal.ts +11 -0
  110. package/templates/m3e-app/src/modules/gallery/composables/routePlan.ts +90 -0
  111. package/templates/m3e-app/src/modules/gallery/composables/useAgenda.ts +61 -0
  112. package/templates/m3e-app/src/modules/gallery/composables/useChatCustomers.ts +31 -0
  113. package/templates/m3e-app/src/modules/gallery/composables/useChatDemo.ts +299 -0
  114. package/templates/m3e-app/src/modules/gallery/composables/useGallerySections.ts +230 -0
  115. package/templates/m3e-app/src/modules/gallery/composables/useLocalAttachments.ts +71 -0
  116. package/templates/m3e-app/src/modules/gallery/composables/useLocationShare.ts +38 -0
  117. package/templates/m3e-app/src/modules/gallery/composables/usePhotoScenes.ts +82 -0
  118. package/templates/m3e-app/src/modules/gallery/composables/useTreeDemo.ts +67 -0
  119. package/templates/m3e-app/src/modules/gallery/composables/wilayas.ts +67 -0
  120. package/templates/m3e-app/src/modules/gallery/router/routes/gallery.routes.ts +52 -0
  121. package/templates/m3e-app/src/modules/gallery/views/GalleryAgendaView.vue +117 -0
  122. package/templates/m3e-app/src/modules/gallery/views/GalleryChatView.vue +292 -0
  123. package/templates/m3e-app/src/modules/gallery/views/GalleryContactsView.vue +77 -0
  124. package/templates/m3e-app/src/modules/gallery/views/GalleryFeaturedView.vue +60 -0
  125. package/templates/m3e-app/src/modules/gallery/views/GalleryLoginView.vue +135 -0
  126. package/templates/m3e-app/src/modules/gallery/views/GalleryOnboardingView.vue +116 -0
  127. package/templates/m3e-app/src/modules/gallery/views/GallerySectionView.vue +27 -0
  128. package/templates/m3e-app/src/modules/gallery/views/GalleryTabsView.vue +135 -0
  129. package/templates/m3e-app/src/modules/gallery/views/GalleryView.vue +50 -0
  130. package/templates/m3e-app/src/modules/home/components/HomeHero.vue +57 -0
  131. package/templates/m3e-app/src/modules/home/composables/useHomeFeatures.ts +128 -0
  132. package/templates/m3e-app/src/modules/home/composables/useOpenFeature.ts +22 -0
  133. package/templates/m3e-app/src/modules/home/router/routes/home.routes.ts +21 -0
  134. package/templates/m3e-app/src/modules/home/views/FeatureDetailView.vue +66 -0
  135. package/templates/m3e-app/src/modules/home/views/HomeView.vue +47 -0
  136. package/templates/m3e-app/src/modules/settings/components/RolePalette.vue +38 -0
  137. package/templates/m3e-app/src/modules/settings/components/SeedSwatches.vue +41 -0
  138. package/templates/m3e-app/src/modules/settings/components/SettingsChoice.vue +43 -0
  139. package/templates/m3e-app/src/modules/settings/components/StudioPreview.vue +52 -0
  140. package/templates/m3e-app/src/modules/settings/router/routes/settings.routes.ts +17 -0
  141. package/templates/m3e-app/src/modules/settings/types.ts +7 -0
  142. package/templates/m3e-app/src/modules/settings/views/ColorStudioView.vue +166 -0
  143. package/templates/m3e-app/src/modules/settings/views/SettingsView.vue +157 -0
  144. package/templates/m3e-app/src/plugins/bootstrapError.ts +62 -0
  145. package/templates/m3e-app/src/plugins/capacitor/index.ts +14 -0
  146. package/templates/m3e-app/src/plugins/capacitor/useAndroidBackButton.ts +36 -0
  147. package/templates/m3e-app/src/plugins/capacitor/useKeyboard.ts +48 -0
  148. package/templates/m3e-app/src/plugins/capacitor/useSplashScreen.ts +43 -0
  149. package/templates/m3e-app/src/plugins/capacitor/useStatusBar.ts +16 -0
  150. package/templates/m3e-app/src/plugins/framework7.plugin.ts +36 -0
  151. package/templates/m3e-app/src/plugins/i18n.plugin.ts +87 -0
  152. package/templates/m3e-app/src/plugins/m3e.plugin.ts +22 -0
  153. package/templates/m3e-app/src/plugins/seed.plugin.ts +7 -0
  154. package/templates/m3e-app/src/plugins/sqlite.plugin.ts +32 -0
  155. package/templates/m3e-app/src/router/global/global.routes.ts +12 -0
  156. package/templates/m3e-app/src/router/index.ts +20 -0
  157. package/templates/m3e-app/src/shared/components/error/404.vue +13 -0
  158. package/templates/m3e-app/src/shared/components/layout/EmptyState.vue +30 -0
  159. package/templates/m3e-app/src/shared/components/layout/SectionHeader.vue +10 -0
  160. package/templates/m3e-app/src/shared/components/page/AppPage.vue +104 -0
  161. package/templates/m3e-app/src/shared/composables/navigation/useActiveTab.ts +39 -0
  162. package/templates/m3e-app/src/shared/composables/navigation/useContainerTransform.ts +117 -0
  163. package/templates/m3e-app/src/shared/composables/navigation/useNavigationGuard.ts +48 -0
  164. package/templates/m3e-app/src/shared/composables/navigation/useNavigationVisibility.ts +64 -0
  165. package/templates/m3e-app/src/shared/composables/navigation/useViewRouter.ts +25 -0
  166. package/templates/m3e-app/src/shared/composables/navigation/useWindowClass.ts +22 -0
  167. package/templates/m3e-app/src/shared/composables/theme/useThemeSettings.ts +155 -0
  168. package/templates/m3e-app/src/shared/database/candidates/capacitorSqlite.ts +36 -0
  169. package/templates/m3e-app/src/shared/database/candidates/index.ts +4 -0
  170. package/templates/m3e-app/src/shared/database/candidates/opfsSahPool.ts +25 -0
  171. package/templates/m3e-app/src/shared/database/candidates/types.ts +56 -0
  172. package/templates/m3e-app/src/shared/database/candidates/waSqlite.ts +56 -0
  173. package/templates/m3e-app/src/shared/database/database.ts +219 -0
  174. package/templates/m3e-app/src/shared/database/index.ts +3 -0
  175. package/templates/m3e-app/src/shared/database/migrations.ts +223 -0
  176. package/templates/m3e-app/src/shared/database/opfs.worker.ts +5 -0
  177. package/templates/m3e-app/src/shared/database/queries.ts +26 -0
  178. package/templates/m3e-app/src/shared/database/schema.ts +153 -0
  179. package/templates/m3e-app/src/shared/database/storage.ts +64 -0
  180. package/templates/m3e-app/src/shared/database/wa.worker.ts +5 -0
  181. package/templates/m3e-app/src/shared/utils/lazyRoute.ts +41 -0
  182. package/templates/m3e-app/src/shared/utils/resolvers/resolvers.ts +42 -0
  183. package/templates/m3e-app/src/shared/utils/textDirection.ts +22 -0
  184. package/templates/m3e-app/src/shared/utils/theme/themeSettings.ts +59 -0
  185. package/templates/m3e-app/src/shared/utils/tone.ts +8 -0
  186. package/templates/m3e-app/tests/benchmark.suite.test.ts +104 -0
  187. package/templates/m3e-app/tests/containerTransform.test.ts +58 -0
  188. package/templates/m3e-app/tests/fieldFormats.test.ts +25 -0
  189. package/templates/m3e-app/tests/lazyRoute.test.ts +30 -0
  190. package/templates/m3e-app/tests/locales.test.ts +73 -0
  191. package/templates/m3e-app/tests/migrations.test.ts +74 -0
  192. package/templates/m3e-app/tests/navigationGuard.test.ts +82 -0
  193. package/templates/m3e-app/tests/openDatabase.test.ts +54 -0
  194. package/templates/m3e-app/tests/sales.repository.test.ts +329 -0
  195. package/templates/m3e-app/tests/storage.test.ts +117 -0
  196. package/templates/m3e-app/tests/textDirection.test.ts +11 -0
  197. package/templates/m3e-app/tooling/linkedPackages.ts +112 -0
  198. package/templates/m3e-app/tsconfig.json +21 -0
  199. package/templates/m3e-app/vite.config.ts +144 -0
@@ -0,0 +1,117 @@
1
+ import { afterEach, expect, test } from "vite-plus/test";
2
+ import { DEFAULT_ORDER, storageChain } from "../src/app/storage.config.js";
3
+ import {
4
+ describeOpenFailure,
5
+ MINIMUM_CHROMIUM_FOR_OPFS,
6
+ probeOpfsCapable,
7
+ webviewLikelyTooOld,
8
+ webviewVersion,
9
+ } from "../src/shared/database/storage.js";
10
+
11
+ const original = globalThis.navigator;
12
+
13
+ afterEach(() => {
14
+ if (original)
15
+ Object.defineProperty(globalThis, "navigator", { value: original, configurable: true });
16
+ });
17
+
18
+ function withNavigator(value: unknown): void {
19
+ Object.defineProperty(globalThis, "navigator", { value, configurable: true });
20
+ }
21
+
22
+ /**
23
+ * The regression this exists for: the probe used to require `createSyncAccessHandle` on
24
+ * `FileSystemFileHandle.prototype`. That method is Worker-only in Chromium, so it is absent on the
25
+ * main thread of every device - and the check rejected a phone on WebView 150 that had been running
26
+ * OPFS for hours. A main-thread probe may only look at what the main thread can see.
27
+ */
28
+ test("a WebView that supports OPFS passes even without the worker-only method", () => {
29
+ withNavigator({ storage: { getDirectory: () => Promise.resolve({}) } });
30
+ expect("createSyncAccessHandle" in (globalThis.FileSystemFileHandle?.prototype ?? {})).toBe(
31
+ false,
32
+ );
33
+ expect(probeOpfsCapable().supported).toBe(true);
34
+ });
35
+
36
+ test("a WebView with no OPFS at all is refused, and told what to do", () => {
37
+ withNavigator({ storage: {} });
38
+ const probe = probeOpfsCapable();
39
+ expect(probe.supported).toBe(false);
40
+ expect(probe.reason).toMatch(/Android System WebView/);
41
+ });
42
+
43
+ test("an open failure is translated into something actionable", () => {
44
+ const sah = describeOpenFailure(new Error("createSyncAccessHandle is not a function"));
45
+ expect(sah).toMatch(/Android System WebView/);
46
+ // The other cause of the same failure, and the one a person can actually fix themselves.
47
+ expect(sah).toMatch(/still holds the database open/);
48
+ // The engine's own words are kept, so a bug report has something to go on.
49
+ expect(sah).toContain("createSyncAccessHandle is not a function");
50
+
51
+ expect(describeOpenFailure(new Error("disk I/O error"))).toBe(
52
+ "The database could not be opened: disk I/O error",
53
+ );
54
+ });
55
+
56
+ /**
57
+ * The chain is a claim about ordering, so it is checked like one: fastest first, durable before
58
+ * volatile, and nothing in it without a stated cost.
59
+ */
60
+ test("the storage chain is ordered and honest about what each entry costs", () => {
61
+ expect(storageChain.length).toBeGreaterThan(0);
62
+
63
+ // The ranking is asserted against the default order, not the live chain: VITE_STORAGE_ENGINE
64
+ // reorders the chain on purpose, and a generated app that chose another engine is not broken.
65
+ expect(DEFAULT_ORDER[0]?.id).toBe("sqlite-wasm-opfs-sahpool");
66
+ expect(DEFAULT_ORDER[0]?.evidence).toBe("measured");
67
+
68
+ // Whatever the environment asked for, the chain must still be every candidate exactly once -
69
+ // promoting one may not drop the others, or the fallback silently disappears.
70
+ expect([...storageChain].sort((a, b) => (a.id < b.id ? -1 : 1))).toEqual(
71
+ [...DEFAULT_ORDER].sort((a, b) => (a.id < b.id ? -1 : 1)),
72
+ );
73
+
74
+ for (const candidate of DEFAULT_ORDER) {
75
+ expect(candidate.tradeoff.length, candidate.id).toBeGreaterThan(20);
76
+ expect(candidate.label.length, candidate.id).toBeGreaterThan(5);
77
+ }
78
+
79
+ // Nothing in the chain may be volatile: a silent stop somewhere data is not kept is worse than
80
+ // an error that names every attempt.
81
+ expect(storageChain.filter((candidate) => !candidate.durable)).toEqual([]);
82
+
83
+ // Every engine gets a probe of its own; sharing one would hide that IndexedDB needs no OPFS.
84
+ expect(storageChain.every((candidate) => typeof candidate.probe === "function")).toBe(true);
85
+
86
+ // Ids are unique, or results keyed by id would overwrite each other.
87
+ expect(new Set(storageChain.map((c) => c.id)).size).toBe(storageChain.length);
88
+ });
89
+
90
+ /** The floor is reported, never enforced: a vendor build can differ, so it informs and nothing else. */
91
+ test("the WebView version is read from the user agent and judged against the floor", () => {
92
+ withNavigator({
93
+ storage: { getDirectory: () => Promise.resolve({}) },
94
+ userAgent: "Mozilla/5.0 (Linux; Android 16) AppleWebKit/537.36 Chrome/150.0.7871.183 Mobile",
95
+ });
96
+ expect(webviewVersion()).toBe(150);
97
+ expect(webviewLikelyTooOld()).toBe(false);
98
+ // Support is still asserted, because the floor must not become a second gate on a working device.
99
+ expect(probeOpfsCapable().supported).toBe(true);
100
+
101
+ withNavigator({
102
+ storage: { getDirectory: () => Promise.resolve({}) },
103
+ userAgent: "Mozilla/5.0 (Linux; Android 7.0) AppleWebKit/537.36 Chrome/51.0.2704.90 Mobile",
104
+ });
105
+ expect(webviewVersion()).toBe(51);
106
+ expect(webviewLikelyTooOld()).toBe(true);
107
+
108
+ withNavigator({
109
+ storage: { getDirectory: () => Promise.resolve({}) },
110
+ userAgent: "something else",
111
+ });
112
+ expect(webviewVersion()).toBe(null);
113
+ // Unknown is not old: an unparseable agent must not lock anybody out.
114
+ expect(webviewLikelyTooOld()).toBe(false);
115
+
116
+ expect(MINIMUM_CHROMIUM_FOR_OPFS).toBe(109);
117
+ });
@@ -0,0 +1,11 @@
1
+ import { expect, test } from "vite-plus/test";
2
+ import { textDirection } from "../src/shared/utils/textDirection.js";
3
+
4
+ test("a language reads the way its script is written", () => {
5
+ expect(textDirection("ar")).toBe("rtl");
6
+ expect(textDirection("ar-DZ")).toBe("rtl");
7
+ expect(textDirection("he")).toBe("rtl");
8
+ expect(textDirection("fr")).toBe("ltr");
9
+ expect(textDirection("en-US")).toBe("ltr");
10
+ expect(textDirection("not a locale")).toBe("ltr");
11
+ });
@@ -0,0 +1,112 @@
1
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
2
+ import { join, resolve, sep } from "node:path";
3
+ import type { Plugin, ViteDevServer } from "vite-plus";
4
+
5
+ /** Quiet time after the last write before a rebuild counts as finished. */
6
+ const SETTLE_MS = 300;
7
+
8
+ const pathKey = (path: string) => {
9
+ const absolute = resolve(path);
10
+ return process.platform === "win32" ? absolute.toLowerCase() : absolute;
11
+ };
12
+
13
+ /**
14
+ * `@cavulsqa` packages linked from the workspace, as opposed to installed from the registry - the
15
+ * ones whose `dist` changes under a running dev server while their watchers rebuild them.
16
+ */
17
+ export function linkedPackages(root: string, names: readonly string[]): string[] {
18
+ return names.filter((name) => {
19
+ const path = join(root, "node_modules", name);
20
+ return existsSync(path) && !realpathSync(path).split(sep).includes("node_modules");
21
+ });
22
+ }
23
+
24
+ function exportedFiles(packageDir: string): string[] {
25
+ const manifest = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8")) as {
26
+ exports?: unknown;
27
+ main?: string;
28
+ module?: string;
29
+ };
30
+ const files = new Set<string>();
31
+ const collect = (value: unknown) => {
32
+ if (typeof value === "string" && !value.includes("*")) files.add(join(packageDir, value));
33
+ else if (value && typeof value === "object") Object.values(value).forEach(collect);
34
+ };
35
+ collect(manifest.exports);
36
+ collect(manifest.main);
37
+ collect(manifest.module);
38
+ return [...files];
39
+ }
40
+
41
+ /**
42
+ * One clean reload per rebuild of a linked package. Left to itself the dev server reloads on the
43
+ * first file the build touches - often while `dist` has just been emptied, so the page asks for an
44
+ * entry that is not there yet and boots blank. It also never notices the stylesheet `app.css`
45
+ * imports, so a rebuild paired new JavaScript with old scoped styles. Here every change or
46
+ * deletion under a linked `dist` - through both of Vite's HMR hooks - is held back until writing
47
+ * stops and every exported file exists again with the same size on two checks in a row (a file that
48
+ * exists can still be half written); then the dev server restarts, and the page reloads when it
49
+ * reconnects.
50
+ *
51
+ * A restart rather than a module invalidation, because the native resolver caches what it saw
52
+ * while `dist` was empty: an app file transformed in that window keeps failing with "Failed to
53
+ * resolve import" after the rebuild, since the resolver's cache is keyed by the real path and the
54
+ * app reaches the package through its `node_modules` link. A restart starts every cache fresh.
55
+ */
56
+ export function linkedRebuilds(root: string, names: readonly string[]): Plugin {
57
+ const packages = names.map((name) => realpathSync(join(root, "node_modules", name)));
58
+ const dists = packages.map((dir) => pathKey(join(dir, "dist")) + sep);
59
+ const required = packages.flatMap(exportedFiles);
60
+ const inDist = (file: string) => dists.some((dist) => pathKey(file).startsWith(dist));
61
+
62
+ let server: ViteDevServer | undefined;
63
+ let timer: ReturnType<typeof setTimeout> | undefined;
64
+ let sizes = "";
65
+
66
+ const snapshot = () =>
67
+ required.map((file) => (existsSync(file) ? statSync(file).size : -1)).join(",");
68
+
69
+ const reload = () => {
70
+ if (!server) return;
71
+ const now = snapshot();
72
+ const settled = now === sizes && !now.split(",").includes("-1");
73
+ sizes = now;
74
+ if (!settled) {
75
+ timer = setTimeout(reload, SETTLE_MS);
76
+ return;
77
+ }
78
+ const restarting = server;
79
+ server = undefined;
80
+ restarting.config.logger.info("linked package rebuilt - restarting", { timestamp: true });
81
+ restarting.restart().catch((error: unknown) => {
82
+ restarting.config.logger.error(`restart after a linked rebuild failed: ${String(error)}`);
83
+ });
84
+ };
85
+
86
+ const schedule = () => {
87
+ clearTimeout(timer);
88
+ sizes = "";
89
+ timer = setTimeout(reload, SETTLE_MS);
90
+ };
91
+
92
+ return {
93
+ name: "cavulsqa:linked-rebuilds",
94
+ apply: "serve",
95
+ configureServer(devServer) {
96
+ if (packages.length === 0) return;
97
+ server = devServer;
98
+ devServer.watcher.add(packages.map((dir) => join(dir, "dist")));
99
+ for (const event of ["add", "change", "unlink"] as const) {
100
+ devServer.watcher.on(event, (file) => {
101
+ if (inDist(file)) schedule();
102
+ });
103
+ }
104
+ },
105
+ hotUpdate({ file }) {
106
+ if (inDist(file)) return [];
107
+ },
108
+ handleHotUpdate({ file }) {
109
+ if (inDist(file)) return [];
110
+ },
111
+ };
112
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "jsx": "preserve",
8
+ "strict": true,
9
+ "noUnusedLocals": true,
10
+ "noEmit": true,
11
+ "isolatedModules": true,
12
+ "esModuleInterop": true,
13
+ "resolveJsonModule": true,
14
+ "skipLibCheck": true,
15
+ "types": ["vite/client"],
16
+ "paths": {
17
+ "@/*": ["./src/*"]
18
+ }
19
+ },
20
+ "include": ["src/**/*.ts", "src/**/*.vue", "auto-imports.d.ts", "components.d.ts"]
21
+ }
@@ -0,0 +1,144 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { fileURLToPath, URL } from "node:url";
3
+ import { M3eResolver } from "@cavulsqa/m3e-vue/resolver";
4
+ import VueI18nPlugin from "@intlify/unplugin-vue-i18n/vite";
5
+ import tailwindcss from "@tailwindcss/vite";
6
+ import vue from "@vitejs/plugin-vue";
7
+ import AutoImport from "unplugin-auto-import/vite";
8
+ import Icons from "unplugin-icons/vite";
9
+ import IconsResolver from "unplugin-icons/resolver";
10
+ import Components from "unplugin-vue-components/vite";
11
+ import { defineConfig, loadEnv } from "vite-plus";
12
+ import { linkedPackages, linkedRebuilds } from "./tooling/linkedPackages.js";
13
+ import {
14
+ Framework7VueResolver,
15
+ getFramework7AutoImports,
16
+ } from "./src/shared/utils/resolvers/resolvers.js";
17
+
18
+ const SRC = fileURLToPath(new URL("./src", import.meta.url));
19
+
20
+ const pkg = JSON.parse(
21
+ readFileSync(fileURLToPath(new URL("./package.json", import.meta.url)), "utf-8"),
22
+ ) as { name: string; version: string };
23
+
24
+ /**
25
+ * One port for the dev server and the live-reload URL `capacitor.config.ts` builds, both read from
26
+ * `VITE_LIVE_RELOAD_PORT` - change it when another dev server already holds 5173.
27
+ */
28
+ function devPort(mode: string): number {
29
+ const port = Number(loadEnv(mode, ROOT, "VITE_").VITE_LIVE_RELOAD_PORT);
30
+ return Number.isInteger(port) && port > 0 ? port : 5173;
31
+ }
32
+
33
+ const ROOT = fileURLToPath(new URL(".", import.meta.url));
34
+ /**
35
+ * Linked workspace packages skip pre-bundling: Vite keeps a pre-bundle until the lockfile changes,
36
+ * so a package rebuilt by its watcher would keep serving its old JavaScript.
37
+ */
38
+ const LINKED = linkedPackages(ROOT, ["@cavulsqa/m3e", "@cavulsqa/m3e-vue"]);
39
+
40
+ export default defineConfig(({ mode }) => ({
41
+ define: {
42
+ __APP_NAME__: JSON.stringify("App"),
43
+ __APP_VERSION__: JSON.stringify(pkg.version),
44
+ },
45
+
46
+ plugins: [
47
+ linkedRebuilds(ROOT, LINKED),
48
+ vue({
49
+ template: {
50
+ compilerOptions: {
51
+ isCustomElement: (tag) => tag === "jeep-sqlite",
52
+ },
53
+ },
54
+ }),
55
+
56
+ tailwindcss(),
57
+
58
+ /**
59
+ * Material Symbols as tree-shaken SVG components from the installed collection, never a font
60
+ * and never fetched: `autoInstall` off means a misspelt name fails the build instead of
61
+ * downloading something at dev time or rendering an empty box on a device.
62
+ */
63
+ Icons({ autoInstall: false, compiler: "vue3", scale: 1 }),
64
+
65
+ VueI18nPlugin({
66
+ include: [fileURLToPath(new URL("./src/locales/**", import.meta.url))],
67
+ }),
68
+
69
+ AutoImport({
70
+ include: [/\.[tj]sx?$/, /\.vue$/, /\.vue\?vue/],
71
+ exclude: [/[\\/]node_modules[\\/]/, /[\\/]\.git[\\/]/, /[\\/]dist[\\/]/],
72
+ imports: [
73
+ "vue",
74
+ "vue-i18n",
75
+ "@vueuse/core",
76
+ getFramework7AutoImports(),
77
+ {
78
+ "@cavulsqa/m3e-vue": [
79
+ "useSnackbar",
80
+ "useNotification",
81
+ "useDialog",
82
+ "useActionSheet",
83
+ "useHaptics",
84
+ "useReducedMotion",
85
+ ],
86
+ },
87
+ ],
88
+ dirs: [
89
+ "src/shared/composables/**",
90
+ "src/shared/utils/**",
91
+ "src/plugins/**",
92
+ "src/modules/**/composables/**",
93
+ ],
94
+ dts: "auto-imports.d.ts",
95
+ vueTemplate: true,
96
+ viteOptimizeDeps: true,
97
+ injectAtEnd: true,
98
+ dirsScanOptions: { types: true },
99
+ }),
100
+
101
+ /**
102
+ * Three resolvers, one per source: `M3*` from @cavulsqa/m3e-vue (everything visible), the five
103
+ * Framework7 engine components (app, views, view, page, page content - nothing visual), and
104
+ * `i-ms-*` icons from Material Symbols. No screen imports any of them by hand.
105
+ */
106
+ Components({
107
+ dts: "components.d.ts",
108
+ dirs: ["src/shared/components/**", "src/modules/**/views/**", "src/modules/**/components/**"],
109
+ extensions: ["vue"],
110
+ deep: true,
111
+ resolvers: [
112
+ M3eResolver(),
113
+ Framework7VueResolver(),
114
+ IconsResolver({
115
+ prefix: "i",
116
+ alias: { ms: "material-symbols" },
117
+ enabledCollections: ["material-symbols"],
118
+ }),
119
+ ],
120
+ }),
121
+ ],
122
+
123
+ resolve: { alias: { "@": SRC } },
124
+ server: { port: devPort(mode), strictPort: true },
125
+ optimizeDeps: {
126
+ exclude: LINKED,
127
+ include: LINKED.includes("@cavulsqa/m3e")
128
+ ? ["@cavulsqa/m3e > @material/material-color-utilities"]
129
+ : [],
130
+ },
131
+ build: { target: "esnext" },
132
+ test: {
133
+ server: {
134
+ deps: {
135
+ /** material-color-utilities 0.4.0 has an extensionless import Node's ESM loader rejects. */
136
+ inline: ["@material/material-color-utilities"],
137
+ },
138
+ },
139
+ },
140
+ lint: { options: { typeAware: false } },
141
+ fmt: {
142
+ ignorePatterns: ["**/auto-imports.d.ts", "**/components.d.ts"],
143
+ },
144
+ }));