@cavulsqa/create 2.9.2 → 2.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cavulsqa/create",
3
- "version": "2.9.2",
3
+ "version": "2.9.3",
4
4
  "description": "Create a Vue + Framework7 + Capacitor + SQLite app from the cavulsqa templates.",
5
5
  "keywords": [
6
6
  "capacitor",
@@ -52,7 +52,7 @@
52
52
  }
53
53
  ]
54
54
  },
55
- "templatesFingerprint": "sha256-w5ItqHFEFJCJwe7V3W0FEhJf7foJkanjYLY85j/Wbd8=",
55
+ "templatesFingerprint": "sha256-6R7B6B3lY7wTuktecqa5vTipEb1bILSBEAhOvLXmiTM=",
56
56
  "scripts": {
57
57
  "build": "node scripts/bundleTemplates.mjs",
58
58
  "check": "vp check",
@@ -71,6 +71,9 @@ The short version of the rules, each earned by a real bug:
71
71
  kysely reports `no result`. See `.claude/rules/database.md`.
72
72
  - **Money is integer cents.** A float total is a rounding bug waiting to happen.
73
73
  - **`f7route` and `f7router` are props**, not imports: `defineProps<{ f7route: Router.Route }>()`.
74
+ - **A route loads its component through `lazyRoute`**, never a bare `import().then(resolve)`.
75
+ Framework7 locks the router before calling the hook and only unlocks it from `resolve` or
76
+ `reject`; a hook that settles neither — one failed chunk — freezes that view for the session.
74
77
  - **In locale files, `@` and `|` are message syntax.** `@` starts a linked message and `|` a plural
75
78
  branch. A literal `@` must be `{'@'}` or the whole locale file fails to compile.
76
79
  - **No comments that narrate.** A comment explains a non-obvious _why_ — a platform quirk, an
@@ -58,12 +58,14 @@ declare global {
58
58
  const initCapacitor: typeof import('./src/plugins/capacitor/index').initCapacitor
59
59
  const inject: typeof import('vue').inject
60
60
  const injectLocal: typeof import('@vueuse/core').injectLocal
61
+ const installNavigationGuard: typeof import('./src/shared/composables/useNavigationGuard').installNavigationGuard
61
62
  const isDefined: typeof import('@vueuse/core').isDefined
62
63
  const isProxy: typeof import('vue').isProxy
63
64
  const isReactive: typeof import('vue').isReactive
64
65
  const isReadonly: typeof import('vue').isReadonly
65
66
  const isRef: typeof import('vue').isRef
66
67
  const isShallow: typeof import('vue').isShallow
68
+ const lazyRoute: typeof import('./src/shared/utils/lazyRoute').lazyRoute
67
69
  const makeDestructurable: typeof import('@vueuse/core').makeDestructurable
68
70
  const markRaw: typeof import('vue').markRaw
69
71
  const nextTick: typeof import('vue').nextTick
@@ -230,6 +232,7 @@ declare global {
230
232
  const useMouseInElement: typeof import('@vueuse/core').useMouseInElement
231
233
  const useMousePressed: typeof import('@vueuse/core').useMousePressed
232
234
  const useMutationObserver: typeof import('@vueuse/core').useMutationObserver
235
+ const useNavigationGuard: typeof import('./src/shared/composables/useNavigationGuard').useNavigationGuard
233
236
  const useNavigatorLanguage: typeof import('@vueuse/core').useNavigatorLanguage
234
237
  const useNetwork: typeof import('@vueuse/core').useNetwork
235
238
  const useNow: typeof import('@vueuse/core').useNow
@@ -403,12 +406,14 @@ declare module 'vue' {
403
406
  readonly initCapacitor: UnwrapRef<typeof import('./src/plugins/capacitor/index')['initCapacitor']>
404
407
  readonly inject: UnwrapRef<typeof import('vue')['inject']>
405
408
  readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
409
+ readonly installNavigationGuard: UnwrapRef<typeof import('./src/shared/composables/useNavigationGuard')['installNavigationGuard']>
406
410
  readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
407
411
  readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
408
412
  readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
409
413
  readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
410
414
  readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
411
415
  readonly isShallow: UnwrapRef<typeof import('vue')['isShallow']>
416
+ readonly lazyRoute: UnwrapRef<typeof import('./src/shared/utils/lazyRoute')['lazyRoute']>
412
417
  readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
413
418
  readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
414
419
  readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
@@ -575,6 +580,7 @@ declare module 'vue' {
575
580
  readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
576
581
  readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
577
582
  readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
583
+ readonly useNavigationGuard: UnwrapRef<typeof import('./src/shared/composables/useNavigationGuard')['useNavigationGuard']>
578
584
  readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
579
585
  readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
580
586
  readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
@@ -20,9 +20,9 @@
20
20
  "@capacitor/preferences": "^8.0.0",
21
21
  "@capacitor/splash-screen": "^8.0.0",
22
22
  "@capacitor/status-bar": "^8.0.0",
23
- "@cavulsqa/mobile-db": "^1.2.0",
24
- "@cavulsqa/reactive-db": "^1.2.0",
25
- "@cavulsqa/reactive-vue": "^1.2.0",
23
+ "@cavulsqa/mobile-db": "^1.2.1",
24
+ "@cavulsqa/reactive-db": "^1.2.1",
25
+ "@cavulsqa/reactive-vue": "^1.2.1",
26
26
  "@sqlite.org/sqlite-wasm": "3.53.0-build1",
27
27
  "@tailwindcss/vite": "^4.3.3",
28
28
  "@vueuse/core": "^14.4.0",
@@ -57,6 +57,7 @@ const parameters = framework7Parameters(appTheme.value.theme, appTheme.value.dar
57
57
  onMounted(() => {
58
58
  // The native handlers need the Framework7 instance, which exists only once the shell is mounted.
59
59
  f7ready(async (instance) => {
60
+ useNavigationGuard();
60
61
  await initCapacitor(instance);
61
62
  hideSplashScreen();
62
63
  });
@@ -1,33 +1,21 @@
1
1
  import type { Router } from "framework7/types";
2
+ import { lazyRoute } from "@/shared/utils/lazyRoute";
2
3
 
3
4
  const demoRoutes: Router.RouteParameters[] = [
4
5
  {
5
6
  name: "demo",
6
7
  path: "/demo/",
7
- // `async` is Framework7's route hook, not an async function - resolve from the promise.
8
- async({ resolve }) {
9
- void import("@/modules/demo/views/DemoView.vue").then((view) => {
10
- resolve({ component: view.default });
11
- });
12
- },
8
+ async: lazyRoute(() => import("@/modules/demo/views/DemoView.vue")),
13
9
  },
14
10
  {
15
11
  name: "demo-search",
16
12
  path: "/demo/search/",
17
- async({ resolve }) {
18
- void import("@/modules/demo/views/OrderSearchView.vue").then((view) => {
19
- resolve({ component: view.default });
20
- });
21
- },
13
+ async: lazyRoute(() => import("@/modules/demo/views/OrderSearchView.vue")),
22
14
  },
23
15
  {
24
16
  name: "demo-order",
25
17
  path: "/demo/order/:id/",
26
- async({ resolve }) {
27
- void import("@/modules/demo/views/OrderDetailView.vue").then((view) => {
28
- resolve({ component: view.default });
29
- });
30
- },
18
+ async: lazyRoute(() => import("@/modules/demo/views/OrderDetailView.vue")),
31
19
  },
32
20
  ];
33
21
 
@@ -1,4 +1,5 @@
1
1
  import type { Router } from "framework7/types";
2
+ import { lazyRoute } from "@/shared/utils/lazyRoute";
2
3
 
3
4
  /**
4
5
  * The detail route is a child of the tab's own view, which is what keeps the tab bar in place and
@@ -8,21 +9,12 @@ const homeRoutes: Router.RouteParameters[] = [
8
9
  {
9
10
  name: "home",
10
11
  path: "/home/",
11
- // `async` is Framework7's route hook, not an async function - resolve from the promise.
12
- async({ resolve }) {
13
- void import("@/modules/home/views/HomeView.vue").then((view) => {
14
- resolve({ component: view.default });
15
- });
16
- },
12
+ async: lazyRoute(() => import("@/modules/home/views/HomeView.vue")),
17
13
  },
18
14
  {
19
15
  name: "home-feature",
20
16
  path: "/home/feature/:id/",
21
- async({ resolve }) {
22
- void import("@/modules/home/views/FeatureDetailView.vue").then((view) => {
23
- resolve({ component: view.default });
24
- });
25
- },
17
+ async: lazyRoute(() => import("@/modules/home/views/FeatureDetailView.vue")),
26
18
  },
27
19
  ];
28
20
 
@@ -1,15 +1,11 @@
1
1
  import type { Router } from "framework7/types";
2
+ import { lazyRoute } from "@/shared/utils/lazyRoute";
2
3
 
3
4
  const settingsRoutes: Router.RouteParameters[] = [
4
5
  {
5
6
  name: "settings",
6
7
  path: "/settings/",
7
- // `async` is Framework7's route hook, not an async function - resolve from the promise.
8
- async({ resolve }) {
9
- void import("@/modules/settings/views/SettingsView.vue").then((view) => {
10
- resolve({ component: view.default });
11
- });
12
- },
8
+ async: lazyRoute(() => import("@/modules/settings/views/SettingsView.vue")),
13
9
  },
14
10
  ];
15
11
 
@@ -1,15 +1,11 @@
1
1
  import type { Router } from "framework7/types";
2
+ import { lazyRoute } from "@/shared/utils/lazyRoute";
2
3
 
3
4
  const globalRoutes: Router.RouteParameters[] = [
4
5
  {
5
6
  name: "not-found",
6
7
  path: "(.*)",
7
- // `async` is Framework7's route hook, not an async function - resolve from the promise.
8
- async({ resolve }) {
9
- void import("@/shared/components/error/404.vue").then((view) => {
10
- resolve({ component: view.default });
11
- });
12
- },
8
+ async: lazyRoute(() => import("@/shared/components/error/404.vue")),
13
9
  },
14
10
  ];
15
11
 
@@ -0,0 +1,48 @@
1
+ import { f7 } from "framework7-vue";
2
+
3
+ interface GuardableRouter {
4
+ allowPageChange: boolean;
5
+ navigate: (...args: never[]) => unknown;
6
+ back: (...args: never[]) => unknown;
7
+ __navigationGuarded?: boolean;
8
+ }
9
+
10
+ /**
11
+ * Framework7 only enforces `allowPageChange` for synchronous routes. Routes here are async, and
12
+ * `asyncResolve` calls the internal `load()` with `ignorePageChange: true`, so a navigate issued
13
+ * while a transition is still animating runs a forward on top of an in-flight backward. That
14
+ * destroys the previous page element and leaves the router holding two copies of the same page;
15
+ * from there `loadBack` takes its same-url early return and back() can never move again, which
16
+ * reads to the user as a frozen screen.
17
+ *
18
+ * Dropping the losing call is a single boolean read - no queueing, no deferral - so the navigation
19
+ * that does win runs at exactly the same speed as before.
20
+ */
21
+ export function installNavigationGuard(router: GuardableRouter): void {
22
+ if (router.__navigationGuarded) return;
23
+ router.__navigationGuarded = true;
24
+
25
+ let depth = 0;
26
+
27
+ const guard = (method: (...args: never[]) => unknown) => {
28
+ return (...args: never[]) => {
29
+ if (depth === 0 && !router.allowPageChange) return router;
30
+ depth += 1;
31
+ try {
32
+ return method(...args);
33
+ } finally {
34
+ depth -= 1;
35
+ }
36
+ };
37
+ };
38
+
39
+ router.navigate = guard(router.navigate.bind(router));
40
+ router.back = guard(router.back.bind(router));
41
+ }
42
+
43
+ export function useNavigationGuard(): void {
44
+ const views = f7.views as unknown as Array<{ router?: GuardableRouter }>;
45
+ for (const view of views ?? []) {
46
+ if (view?.router) installNavigationGuard(view.router);
47
+ }
48
+ }
@@ -0,0 +1,25 @@
1
+ import type { Router } from "framework7/types";
2
+
3
+ type RouteComponentModule = { default: unknown };
4
+
5
+ /**
6
+ * Framework7 sets `allowPageChange = false` before calling a route's async hook and only restores
7
+ * it from `resolve` or `reject`. A hook that settles neither - a chunk that fails to download, or a
8
+ * module that throws while evaluating - leaves that view's router locked for the rest of the
9
+ * session, so every later navigation and back press is silently ignored. Routing the import
10
+ * through here guarantees the router is always released.
11
+ */
12
+ export function lazyRoute(
13
+ load: () => Promise<RouteComponentModule>,
14
+ ): Router.RouteParameters["async"] {
15
+ return function routeAsync({ resolve, reject }) {
16
+ load()
17
+ .then((module) => {
18
+ resolve({ component: module.default });
19
+ })
20
+ .catch((error: unknown) => {
21
+ console.error("[router] Unable to load route component", error);
22
+ reject();
23
+ });
24
+ };
25
+ }
@@ -0,0 +1,30 @@
1
+ import { expect, test, vi } from "vite-plus/test";
2
+ import { lazyRoute } from "../src/shared/utils/lazyRoute.js";
3
+
4
+ function callRoute(handler: ReturnType<typeof lazyRoute>) {
5
+ const resolve = vi.fn();
6
+ const reject = vi.fn();
7
+ (handler as (ctx: unknown) => void)({ resolve, reject });
8
+ return { resolve, reject };
9
+ }
10
+
11
+ test("resolves the route with the module's default export", async () => {
12
+ const component = { name: "HomeView" };
13
+ const { resolve, reject } = callRoute(lazyRoute(() => Promise.resolve({ default: component })));
14
+
15
+ await vi.waitFor(() => expect(resolve).toHaveBeenCalledOnce());
16
+ expect(resolve).toHaveBeenCalledWith({ component });
17
+ expect(reject).not.toHaveBeenCalled();
18
+ });
19
+
20
+ test("rejects the route when the chunk fails to load, releasing the router", async () => {
21
+ // Without this the router keeps allowPageChange === false forever and the view freezes.
22
+ const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
23
+ const { resolve, reject } = callRoute(
24
+ lazyRoute(() => Promise.reject(new Error("chunk load failed"))),
25
+ );
26
+
27
+ await vi.waitFor(() => expect(reject).toHaveBeenCalledOnce());
28
+ expect(resolve).not.toHaveBeenCalled();
29
+ consoleError.mockRestore();
30
+ });
@@ -0,0 +1,82 @@
1
+ import { expect, test } from "vite-plus/test";
2
+ import { installNavigationGuard } from "../src/shared/composables/useNavigationGuard.js";
3
+
4
+ function makeRouter() {
5
+ const calls: string[] = [];
6
+ const router = {
7
+ allowPageChange: true,
8
+ navigate(url: string) {
9
+ calls.push(`navigate:${url}`);
10
+ },
11
+ back() {
12
+ calls.push("back");
13
+ },
14
+ };
15
+ return { router, calls };
16
+ }
17
+
18
+ test("drops a navigate issued while a transition is still in flight", () => {
19
+ const { router, calls } = makeRouter();
20
+ installNavigationGuard(router as never);
21
+
22
+ router.allowPageChange = false;
23
+ router.navigate("/home/feature/1/");
24
+
25
+ expect(calls).toEqual([]);
26
+ });
27
+
28
+ test("drops a back issued while a transition is still in flight", () => {
29
+ const { router, calls } = makeRouter();
30
+ installNavigationGuard(router as never);
31
+
32
+ router.allowPageChange = false;
33
+ router.back();
34
+
35
+ expect(calls).toEqual([]);
36
+ });
37
+
38
+ test("lets navigation through while the router is idle", () => {
39
+ const { router, calls } = makeRouter();
40
+ installNavigationGuard(router as never);
41
+
42
+ router.navigate("/home/feature/1/");
43
+ router.back();
44
+
45
+ expect(calls).toEqual(["navigate:/home/feature/1/", "back"]);
46
+ });
47
+
48
+ test("still allows Framework7's own recursive calls after it locks the router", () => {
49
+ // back() re-enters itself with { force: true }, and navigate() re-enters for named routes, both
50
+ // after allowPageChange is already false. Dropping those would break normal navigation.
51
+ const calls: string[] = [];
52
+ const router = {
53
+ allowPageChange: true,
54
+ navigate(url: string) {
55
+ calls.push(`navigate:${url}`);
56
+ router.allowPageChange = false;
57
+ if (url === "outer") router.navigate("inner");
58
+ },
59
+ back() {
60
+ calls.push("back");
61
+ router.allowPageChange = false;
62
+ router.navigate("from-back");
63
+ },
64
+ };
65
+ installNavigationGuard(router as never);
66
+
67
+ router.navigate("outer");
68
+ router.allowPageChange = true;
69
+ router.back();
70
+
71
+ expect(calls).toEqual(["navigate:outer", "navigate:inner", "back", "navigate:from-back"]);
72
+ });
73
+
74
+ test("installing twice does not wrap the router twice", () => {
75
+ const { router, calls } = makeRouter();
76
+ installNavigationGuard(router as never);
77
+ installNavigationGuard(router as never);
78
+
79
+ router.navigate("/demo/");
80
+
81
+ expect(calls).toEqual(["navigate:/demo/"]);
82
+ });