@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 +2 -2
- package/templates/f7-app/CLAUDE.md +3 -0
- package/templates/f7-app/auto-imports.d.ts +6 -0
- package/templates/f7-app/package.json +3 -3
- package/templates/f7-app/src/App.vue +1 -0
- package/templates/f7-app/src/modules/demo/router/routes/demo.routes.ts +4 -16
- package/templates/f7-app/src/modules/home/router/routes/home.routes.ts +3 -11
- package/templates/f7-app/src/modules/settings/router/routes/settings.routes.ts +2 -6
- package/templates/f7-app/src/router/global/global.routes.ts +2 -6
- package/templates/f7-app/src/shared/composables/useNavigationGuard.ts +48 -0
- package/templates/f7-app/src/shared/utils/lazyRoute.ts +25 -0
- package/templates/f7-app/tests/lazyRoute.test.ts +30 -0
- package/templates/f7-app/tests/navigationGuard.test.ts +82 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cavulsqa/create",
|
|
3
|
-
"version": "2.9.
|
|
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-
|
|
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.
|
|
24
|
-
"@cavulsqa/reactive-db": "^1.2.
|
|
25
|
-
"@cavulsqa/reactive-vue": "^1.2.
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
});
|