@cavulsqa/create 2.9.2 → 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.
- package/README.md +12 -1
- package/bin/create.mjs +18 -25
- package/lib/templates.mjs +56 -0
- package/package.json +13 -5
- package/templates/f7-app/CLAUDE.md +3 -0
- package/templates/f7-app/auto-imports.d.ts +15 -0
- package/templates/f7-app/package.json +6 -3
- package/templates/f7-app/src/App.vue +1 -0
- package/templates/f7-app/src/env.d.ts +1 -0
- package/templates/f7-app/src/main.ts +3 -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/plugins/bootstrapError.ts +2 -0
- package/templates/f7-app/src/plugins/recorder.plugin.ts +155 -0
- 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/recorder/capacitorSink.ts +124 -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/templates/f7-app/vite.config.ts +1 -0
- package/templates/m3e-app/.claude/rules/data-fetching.md +68 -0
- package/templates/m3e-app/.claude/rules/database.md +106 -0
- package/templates/m3e-app/.claude/rules/m3e-ui.md +48 -0
- package/templates/m3e-app/.claude/rules/modules.md +43 -0
- package/templates/m3e-app/.claude/rules/native.md +60 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/SKILL.md +68 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/color.md +44 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/components.md +325 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/layout.md +42 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/motion.md +51 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/shapes-type.md +48 -0
- package/templates/m3e-app/.claude/skills/m3-expressive/sources.md +53 -0
- package/templates/m3e-app/.claude/skills/module-architecture/SKILL.md +65 -0
- package/templates/m3e-app/.claude/skills/module-architecture/file-templates.md +178 -0
- package/templates/m3e-app/.claude/skills/reactive-data/SKILL.md +90 -0
- package/templates/m3e-app/.claude/skills/reactive-data/testing.md +55 -0
- package/templates/m3e-app/.env.example +32 -0
- package/templates/m3e-app/CLAUDE.md +93 -0
- package/templates/m3e-app/auto-imports.d.ts +851 -0
- package/templates/m3e-app/capacitor.config.ts +44 -0
- package/templates/m3e-app/components.d.ts +252 -0
- package/templates/m3e-app/index.html +16 -0
- package/templates/m3e-app/package.json +107 -0
- package/templates/m3e-app/src/App.vue +129 -0
- package/templates/m3e-app/src/app/pragmas.config.ts +35 -0
- package/templates/m3e-app/src/app/scroll.config.ts +20 -0
- package/templates/m3e-app/src/app/storage.config.ts +65 -0
- package/templates/m3e-app/src/app/tabs.ts +33 -0
- package/templates/m3e-app/src/app/theme.config.ts +51 -0
- package/templates/m3e-app/src/assets/css/app.css +18 -0
- package/templates/m3e-app/src/assets/css/base.css +53 -0
- package/templates/m3e-app/src/assets/css/layout/container-transform.css +113 -0
- package/templates/m3e-app/src/assets/css/layout/shell.css +69 -0
- package/templates/m3e-app/src/assets/css/layout/transitions.css +121 -0
- package/templates/m3e-app/src/assets/css/m3e.css +6 -0
- package/templates/m3e-app/src/assets/css/theme/framework7.css +18 -0
- package/templates/m3e-app/src/assets/css/theme/tailwind.css +365 -0
- package/templates/m3e-app/src/domains/benchmark/benchmark.dataset.ts +209 -0
- package/templates/m3e-app/src/domains/benchmark/benchmark.suite.ts +662 -0
- package/templates/m3e-app/src/domains/sales/sales.repository.ts +458 -0
- package/templates/m3e-app/src/env.d.ts +40 -0
- package/templates/m3e-app/src/locales/ar.json +1260 -0
- package/templates/m3e-app/src/locales/en.json +1260 -0
- package/templates/m3e-app/src/locales/fr.json +1260 -0
- package/templates/m3e-app/src/main.ts +41 -0
- package/templates/m3e-app/src/modules/demo/components/DemoBenchmark.vue +119 -0
- package/templates/m3e-app/src/modules/demo/components/DemoBusLog.vue +63 -0
- package/templates/m3e-app/src/modules/demo/components/DemoCreateOrderSheet.vue +203 -0
- package/templates/m3e-app/src/modules/demo/components/DemoMetricsSheet.vue +75 -0
- package/templates/m3e-app/src/modules/demo/components/DemoOrderList.vue +75 -0
- package/templates/m3e-app/src/modules/demo/components/DemoPipelineBenchmark.vue +65 -0
- package/templates/m3e-app/src/modules/demo/components/DemoStatCards.vue +71 -0
- package/templates/m3e-app/src/modules/demo/composables/useBenchmark.ts +139 -0
- package/templates/m3e-app/src/modules/demo/composables/useOrderStatus.ts +64 -0
- package/templates/m3e-app/src/modules/demo/composables/useReactiveDemo.ts +171 -0
- package/templates/m3e-app/src/modules/demo/router/routes/demo.routes.ts +22 -0
- package/templates/m3e-app/src/modules/demo/views/DemoView.vue +210 -0
- package/templates/m3e-app/src/modules/demo/views/OrderDetailView.vue +136 -0
- package/templates/m3e-app/src/modules/demo/views/OrderSearchView.vue +74 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryBlock.vue +16 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryCarouselTile.vue +71 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryCustomerForm.vue +76 -0
- package/templates/m3e-app/src/modules/gallery/components/GalleryProofOfDelivery.vue +69 -0
- package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaDay.vue +106 -0
- package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaMonth.vue +40 -0
- package/templates/m3e-app/src/modules/gallery/components/agenda/AgendaVisitList.vue +45 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatAttachSheet.vue +106 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatContactPicker.vue +85 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatInviteComposer.vue +131 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatPollComposer.vue +148 -0
- package/templates/m3e-app/src/modules/gallery/components/chat/ChatRecentPhotos.vue +62 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/InputsAccount.vue +79 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/InputsTags.vue +53 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/InputsVerification.vue +73 -0
- package/templates/m3e-app/src/modules/gallery/components/inputs/PasswordStrength.vue +37 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryButtons.vue +128 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryCarousels.vue +236 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryCharts.vue +84 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryFabs.vue +70 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryInputs.vue +219 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryNavigation.vue +141 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryOverlays.vue +368 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryPickers.vue +175 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryProgress.vue +118 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryScale.vue +103 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GallerySelection.vue +219 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryShapes.vue +48 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GallerySurfaces.vue +213 -0
- package/templates/m3e-app/src/modules/gallery/components/sections/GalleryTables.vue +185 -0
- package/templates/m3e-app/src/modules/gallery/components/surfaces/SurfacesContainerTransform.vue +46 -0
- package/templates/m3e-app/src/modules/gallery/composables/chatTeam.ts +18 -0
- package/templates/m3e-app/src/modules/gallery/composables/composeKind.ts +2 -0
- package/templates/m3e-app/src/modules/gallery/composables/currentPosition.ts +35 -0
- package/templates/m3e-app/src/modules/gallery/composables/demoOrders.ts +29 -0
- package/templates/m3e-app/src/modules/gallery/composables/featuredStories.ts +73 -0
- package/templates/m3e-app/src/modules/gallery/composables/fieldFormats.ts +36 -0
- package/templates/m3e-app/src/modules/gallery/composables/openExternal.ts +11 -0
- package/templates/m3e-app/src/modules/gallery/composables/routePlan.ts +90 -0
- package/templates/m3e-app/src/modules/gallery/composables/useAgenda.ts +61 -0
- package/templates/m3e-app/src/modules/gallery/composables/useChatCustomers.ts +31 -0
- package/templates/m3e-app/src/modules/gallery/composables/useChatDemo.ts +299 -0
- package/templates/m3e-app/src/modules/gallery/composables/useGallerySections.ts +230 -0
- package/templates/m3e-app/src/modules/gallery/composables/useLocalAttachments.ts +71 -0
- package/templates/m3e-app/src/modules/gallery/composables/useLocationShare.ts +38 -0
- package/templates/m3e-app/src/modules/gallery/composables/usePhotoScenes.ts +82 -0
- package/templates/m3e-app/src/modules/gallery/composables/useTreeDemo.ts +67 -0
- package/templates/m3e-app/src/modules/gallery/composables/wilayas.ts +67 -0
- package/templates/m3e-app/src/modules/gallery/router/routes/gallery.routes.ts +52 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryAgendaView.vue +117 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryChatView.vue +292 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryContactsView.vue +77 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryFeaturedView.vue +60 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryLoginView.vue +135 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryOnboardingView.vue +116 -0
- package/templates/m3e-app/src/modules/gallery/views/GallerySectionView.vue +27 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryTabsView.vue +135 -0
- package/templates/m3e-app/src/modules/gallery/views/GalleryView.vue +50 -0
- package/templates/m3e-app/src/modules/home/components/HomeHero.vue +57 -0
- package/templates/m3e-app/src/modules/home/composables/useHomeFeatures.ts +128 -0
- package/templates/m3e-app/src/modules/home/composables/useOpenFeature.ts +22 -0
- package/templates/m3e-app/src/modules/home/router/routes/home.routes.ts +21 -0
- package/templates/m3e-app/src/modules/home/views/FeatureDetailView.vue +66 -0
- package/templates/m3e-app/src/modules/home/views/HomeView.vue +47 -0
- package/templates/m3e-app/src/modules/settings/components/RolePalette.vue +38 -0
- package/templates/m3e-app/src/modules/settings/components/SeedSwatches.vue +41 -0
- package/templates/m3e-app/src/modules/settings/components/SettingsChoice.vue +43 -0
- package/templates/m3e-app/src/modules/settings/components/StudioPreview.vue +52 -0
- package/templates/m3e-app/src/modules/settings/router/routes/settings.routes.ts +17 -0
- package/templates/m3e-app/src/modules/settings/types.ts +7 -0
- package/templates/m3e-app/src/modules/settings/views/ColorStudioView.vue +166 -0
- package/templates/m3e-app/src/modules/settings/views/SettingsView.vue +157 -0
- package/templates/m3e-app/src/plugins/bootstrapError.ts +62 -0
- package/templates/m3e-app/src/plugins/capacitor/index.ts +14 -0
- package/templates/m3e-app/src/plugins/capacitor/useAndroidBackButton.ts +36 -0
- package/templates/m3e-app/src/plugins/capacitor/useKeyboard.ts +48 -0
- package/templates/m3e-app/src/plugins/capacitor/useSplashScreen.ts +43 -0
- package/templates/m3e-app/src/plugins/capacitor/useStatusBar.ts +16 -0
- package/templates/m3e-app/src/plugins/framework7.plugin.ts +36 -0
- package/templates/m3e-app/src/plugins/i18n.plugin.ts +87 -0
- package/templates/m3e-app/src/plugins/m3e.plugin.ts +22 -0
- package/templates/m3e-app/src/plugins/seed.plugin.ts +7 -0
- package/templates/m3e-app/src/plugins/sqlite.plugin.ts +32 -0
- package/templates/m3e-app/src/router/global/global.routes.ts +12 -0
- package/templates/m3e-app/src/router/index.ts +20 -0
- package/templates/m3e-app/src/shared/components/error/404.vue +13 -0
- package/templates/m3e-app/src/shared/components/layout/EmptyState.vue +30 -0
- package/templates/m3e-app/src/shared/components/layout/SectionHeader.vue +10 -0
- package/templates/m3e-app/src/shared/components/page/AppPage.vue +104 -0
- package/templates/m3e-app/src/shared/composables/navigation/useActiveTab.ts +39 -0
- package/templates/m3e-app/src/shared/composables/navigation/useContainerTransform.ts +117 -0
- package/templates/m3e-app/src/shared/composables/navigation/useNavigationGuard.ts +48 -0
- package/templates/m3e-app/src/shared/composables/navigation/useNavigationVisibility.ts +64 -0
- package/templates/m3e-app/src/shared/composables/navigation/useViewRouter.ts +25 -0
- package/templates/m3e-app/src/shared/composables/navigation/useWindowClass.ts +22 -0
- package/templates/m3e-app/src/shared/composables/theme/useThemeSettings.ts +155 -0
- package/templates/m3e-app/src/shared/database/candidates/capacitorSqlite.ts +36 -0
- package/templates/m3e-app/src/shared/database/candidates/index.ts +4 -0
- package/templates/m3e-app/src/shared/database/candidates/opfsSahPool.ts +25 -0
- package/templates/m3e-app/src/shared/database/candidates/types.ts +56 -0
- package/templates/m3e-app/src/shared/database/candidates/waSqlite.ts +56 -0
- package/templates/m3e-app/src/shared/database/database.ts +219 -0
- package/templates/m3e-app/src/shared/database/index.ts +3 -0
- package/templates/m3e-app/src/shared/database/migrations.ts +223 -0
- package/templates/m3e-app/src/shared/database/opfs.worker.ts +5 -0
- package/templates/m3e-app/src/shared/database/queries.ts +26 -0
- package/templates/m3e-app/src/shared/database/schema.ts +153 -0
- package/templates/m3e-app/src/shared/database/storage.ts +64 -0
- package/templates/m3e-app/src/shared/database/wa.worker.ts +5 -0
- package/templates/m3e-app/src/shared/utils/lazyRoute.ts +41 -0
- package/templates/m3e-app/src/shared/utils/resolvers/resolvers.ts +42 -0
- package/templates/m3e-app/src/shared/utils/textDirection.ts +22 -0
- package/templates/m3e-app/src/shared/utils/theme/themeSettings.ts +59 -0
- package/templates/m3e-app/src/shared/utils/tone.ts +8 -0
- package/templates/m3e-app/tests/benchmark.suite.test.ts +104 -0
- package/templates/m3e-app/tests/containerTransform.test.ts +58 -0
- package/templates/m3e-app/tests/fieldFormats.test.ts +25 -0
- package/templates/m3e-app/tests/lazyRoute.test.ts +30 -0
- package/templates/m3e-app/tests/locales.test.ts +73 -0
- package/templates/m3e-app/tests/migrations.test.ts +74 -0
- package/templates/m3e-app/tests/navigationGuard.test.ts +82 -0
- package/templates/m3e-app/tests/openDatabase.test.ts +54 -0
- package/templates/m3e-app/tests/sales.repository.test.ts +329 -0
- package/templates/m3e-app/tests/storage.test.ts +117 -0
- package/templates/m3e-app/tests/textDirection.test.ts +11 -0
- package/templates/m3e-app/tooling/linkedPackages.ts +112 -0
- package/templates/m3e-app/tsconfig.json +21 -0
- package/templates/m3e-app/vite.config.ts +144 -0
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ pnpm create @cavulsqa --name caputa --app-id com.example.caputa --yes
|
|
|
29
29
|
| flag | default | |
|
|
30
30
|
| ------------ | ---------------- | ------------------------------------------------------- |
|
|
31
31
|
| `--name` | asked | package and directory name |
|
|
32
|
-
| `--template` |
|
|
32
|
+
| `--template` | `f7-app` | `f7-app` or `m3e-app`; asked from a numbered menu |
|
|
33
33
|
| `--dir` | `./<name>` | where to write it |
|
|
34
34
|
| `--app-name` | `Name` | launcher name, window title, Settings screen |
|
|
35
35
|
| `--app-id` | `com.ayb.<name>` | Android application id |
|
|
@@ -40,6 +40,13 @@ pnpm create @cavulsqa --name caputa --app-id com.example.caputa --yes
|
|
|
40
40
|
|
|
41
41
|
## What you get
|
|
42
42
|
|
|
43
|
+
Two templates, one data layer:
|
|
44
|
+
|
|
45
|
+
| Template | Look and feel |
|
|
46
|
+
| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
47
|
+
| `f7-app` | Framework7's own components, native on iOS and on Material (`theme: auto`) |
|
|
48
|
+
| `m3e-app` | Material 3 Expressive: `@cavulsqa/m3e-vue` components on 2025 dynamic colour, Google Sans Flex, spring motion, a colour studio and a component gallery. Framework7 stays underneath as the navigation engine only, so it is Android-first by design |
|
|
49
|
+
|
|
43
50
|
The `f7-app` template: a tabbed shell, a `domains` / `modules` / `shared` layout, and a working
|
|
44
51
|
sales demo over a six-table schema — dashboard aggregates, search, an order sheet, swipe actions,
|
|
45
52
|
a detail screen — with tests against real SQLite.
|
|
@@ -49,6 +56,10 @@ under Kysely, `@cavulsqa/reactive-db` for the change bus, and `@cavulsqa/reactiv
|
|
|
49
56
|
`useReactiveQuery`. A write announces the tables it touched and every query watching them refetches;
|
|
50
57
|
nothing in a screen asks for a refresh.
|
|
51
58
|
|
|
59
|
+
`m3e-app` has the same layout and data layer, with every screen built from `M3*` components and an
|
|
60
|
+
`m3-expressive` skill that carries the design system: which component, which colour role, which
|
|
61
|
+
spring, and a link to the spec page for each.
|
|
62
|
+
|
|
52
63
|
`CLAUDE.md`, `.claude/rules/` and `.claude/skills/` come with it, so an agent opening the generated
|
|
53
64
|
repository knows the architecture, the conventions, and the traps that have already cost someone a
|
|
54
65
|
day.
|
package/bin/create.mjs
CHANGED
|
@@ -7,11 +7,17 @@
|
|
|
7
7
|
* silent default, because a generated app named "my-app" in the wrong directory is worse than a
|
|
8
8
|
* failed command.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
11
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
12
12
|
import { createInterface } from "node:readline/promises";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
import { scaffold } from "../lib/scaffold.mjs";
|
|
15
|
+
import {
|
|
16
|
+
defaultTemplate,
|
|
17
|
+
listTemplates,
|
|
18
|
+
resolveTemplateChoice,
|
|
19
|
+
templateMenu,
|
|
20
|
+
} from "../lib/templates.mjs";
|
|
15
21
|
|
|
16
22
|
const PACKAGE = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
17
23
|
const BUNDLED = join(PACKAGE, "templates");
|
|
@@ -38,7 +44,7 @@ function usage() {
|
|
|
38
44
|
|
|
39
45
|
Options:
|
|
40
46
|
--name NAME package and directory name
|
|
41
|
-
--template NAME which template (default: the
|
|
47
|
+
--template NAME which template (default: the manifest's default, or you are asked)
|
|
42
48
|
--dir PATH where to write it (default: ./<name>)
|
|
43
49
|
--app-name NAME launcher name and window title (default: Name)
|
|
44
50
|
--app-id ID android application id (default: com.ayb.<name>)
|
|
@@ -49,24 +55,7 @@ Options:
|
|
|
49
55
|
--help this
|
|
50
56
|
|
|
51
57
|
Templates bundled in this package:
|
|
52
|
-
${listTemplates()
|
|
53
|
-
.map((entry) => ` ${entry.name} ${entry.description}`)
|
|
54
|
-
.join("\n")}`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function listTemplates() {
|
|
58
|
-
const manifest = JSON.parse(readFileSync(join(PACKAGE, "package.json"), "utf8"));
|
|
59
|
-
const declared = manifest.createConfig?.templates ?? [];
|
|
60
|
-
if (!existsSync(BUNDLED)) return declared;
|
|
61
|
-
|
|
62
|
-
// The manifest is what `vp create` reads; the directory is what actually shipped. Trust the
|
|
63
|
-
// directory, and let the manifest supply the descriptions.
|
|
64
|
-
return readdirSync(BUNDLED, { withFileTypes: true })
|
|
65
|
-
.filter((entry) => entry.isDirectory())
|
|
66
|
-
.map((entry) => ({
|
|
67
|
-
name: entry.name,
|
|
68
|
-
description: declared.find((listed) => listed.name === entry.name)?.description ?? "",
|
|
69
|
-
}));
|
|
58
|
+
${templateMenu(listTemplates(PACKAGE))}`);
|
|
70
59
|
}
|
|
71
60
|
|
|
72
61
|
/**
|
|
@@ -109,15 +98,19 @@ async function main() {
|
|
|
109
98
|
throw new Error(`"${String(name)}" is not a usable package name`);
|
|
110
99
|
}
|
|
111
100
|
|
|
112
|
-
const templates = listTemplates();
|
|
101
|
+
const templates = listTemplates(PACKAGE);
|
|
113
102
|
if (!templates.length) throw new Error("this build of @cavulsqa/create bundles no templates");
|
|
114
103
|
|
|
115
104
|
let template = flags.get("template");
|
|
116
105
|
if (typeof template !== "string") {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
106
|
+
const fallback = defaultTemplate(templates);
|
|
107
|
+
if (templates.length === 1 || !rl) {
|
|
108
|
+
template = fallback.name;
|
|
109
|
+
} else {
|
|
110
|
+
console.log(`\nTemplates:\n${templateMenu(templates)}\n`);
|
|
111
|
+
const answer = await ask("Template", String(templates.indexOf(fallback) + 1));
|
|
112
|
+
template = resolveTemplateChoice(answer, templates) ?? answer;
|
|
113
|
+
}
|
|
121
114
|
}
|
|
122
115
|
if (!templates.some((entry) => entry.name === template)) {
|
|
123
116
|
throw new Error(
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The templates this build offers, in the manifest's order, with the manifest's descriptions.
|
|
6
|
+
*
|
|
7
|
+
* The manifest is what `vp create` reads; the directory is what actually shipped. A template is
|
|
8
|
+
* offered only when both agree, and a shipped directory the manifest forgot still appears - last,
|
|
9
|
+
* without a description - rather than silently vanishing.
|
|
10
|
+
*/
|
|
11
|
+
export function listTemplates(packageDir) {
|
|
12
|
+
const manifest = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
|
|
13
|
+
const declared = manifest.createConfig?.templates ?? [];
|
|
14
|
+
const bundled = join(packageDir, "templates");
|
|
15
|
+
if (!existsSync(bundled)) return declared.map(toEntry);
|
|
16
|
+
|
|
17
|
+
const shipped = new Set(
|
|
18
|
+
readdirSync(bundled, { withFileTypes: true })
|
|
19
|
+
.filter((entry) => entry.isDirectory())
|
|
20
|
+
.map((entry) => entry.name),
|
|
21
|
+
);
|
|
22
|
+
const listed = declared.filter((entry) => shipped.has(entry.name)).map(toEntry);
|
|
23
|
+
const unlisted = [...shipped]
|
|
24
|
+
.filter((name) => !declared.some((entry) => entry.name === name))
|
|
25
|
+
.sort((a, b) => a.localeCompare(b))
|
|
26
|
+
.map((name) => toEntry({ name }));
|
|
27
|
+
return [...listed, ...unlisted];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function toEntry(entry) {
|
|
31
|
+
return {
|
|
32
|
+
name: entry.name,
|
|
33
|
+
description: entry.description ?? "",
|
|
34
|
+
isDefault: entry.default === true,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The one marked `default` in the manifest, else the first - never whatever readdir returns first. */
|
|
39
|
+
export function defaultTemplate(templates) {
|
|
40
|
+
return templates.find((entry) => entry.isDefault) ?? templates[0];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A typed answer as a template: its number in the list, or its name. Null when it is neither. */
|
|
44
|
+
export function resolveTemplateChoice(answer, templates) {
|
|
45
|
+
const value = String(answer).trim();
|
|
46
|
+
if (/^\d+$/.test(value)) return templates[Number(value) - 1]?.name ?? null;
|
|
47
|
+
return templates.some((entry) => entry.name === value) ? value : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The numbered menu the interactive prompt prints. */
|
|
51
|
+
export function templateMenu(templates) {
|
|
52
|
+
const width = Math.max(...templates.map((entry) => entry.name.length));
|
|
53
|
+
return templates
|
|
54
|
+
.map((entry, index) => ` ${index + 1}) ${entry.name.padEnd(width)} ${entry.description}`)
|
|
55
|
+
.join("\n");
|
|
56
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cavulsqa/create",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Create a Vue +
|
|
3
|
+
"version": "2.10.0",
|
|
4
|
+
"description": "Create a Vue + Capacitor + SQLite app from the cavulsqa templates: Framework7 (iOS and Material) or Material 3 Expressive.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"capacitor",
|
|
7
7
|
"create",
|
|
8
8
|
"framework7",
|
|
9
|
+
"material-3",
|
|
10
|
+
"material-expressive",
|
|
9
11
|
"scaffold",
|
|
10
12
|
"sqlite",
|
|
11
13
|
"template",
|
|
@@ -47,12 +49,18 @@
|
|
|
47
49
|
"templates": [
|
|
48
50
|
{
|
|
49
51
|
"name": "f7-app",
|
|
50
|
-
"description": "Vue 3 + Framework7 + Capacitor + SQLite, tabbed shell, offline-first data layer",
|
|
51
|
-
"template": "./templates/f7-app"
|
|
52
|
+
"description": "Vue 3 + Framework7 + Capacitor + SQLite, tabbed shell, iOS and Material themes, offline-first data layer",
|
|
53
|
+
"template": "./templates/f7-app",
|
|
54
|
+
"default": true
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "m3e-app",
|
|
58
|
+
"description": "Material 3 Expressive: m3e-vue components, 2025 dynamic colour with a colour studio, adaptive bar/rail shell, offline-first data layer",
|
|
59
|
+
"template": "./templates/m3e-app"
|
|
52
60
|
}
|
|
53
61
|
]
|
|
54
62
|
},
|
|
55
|
-
"templatesFingerprint": "sha256-
|
|
63
|
+
"templatesFingerprint": "sha256-3YSsClBPuShazZVtnS9LabHQ330E0wPZiedhN6vIadU=",
|
|
56
64
|
"scripts": {
|
|
57
65
|
"build": "node scripts/bundleTemplates.mjs",
|
|
58
66
|
"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
|
|
@@ -96,6 +98,8 @@ declare global {
|
|
|
96
98
|
const reactiveOmit: typeof import('@vueuse/core').reactiveOmit
|
|
97
99
|
const reactivePick: typeof import('@vueuse/core').reactivePick
|
|
98
100
|
const readonly: typeof import('vue').readonly
|
|
101
|
+
const recorderDiagnostics: typeof import('./src/plugins/recorder.plugin').recorderDiagnostics
|
|
102
|
+
const recorderPlugin: typeof import('./src/plugins/recorder.plugin').recorderPlugin
|
|
99
103
|
const ref: typeof import('vue').ref
|
|
100
104
|
const refAutoReset: typeof import('@vueuse/core').refAutoReset
|
|
101
105
|
const refDebounced: typeof import('@vueuse/core').refDebounced
|
|
@@ -104,6 +108,7 @@ declare global {
|
|
|
104
108
|
const refThrottled: typeof import('@vueuse/core').refThrottled
|
|
105
109
|
const refWithControl: typeof import('@vueuse/core').refWithControl
|
|
106
110
|
const renderBootstrapError: typeof import('./src/plugins/bootstrapError').renderBootstrapError
|
|
111
|
+
const reportProblem: typeof import('./src/plugins/recorder.plugin').reportProblem
|
|
107
112
|
const request: typeof import('framework7/lite').request
|
|
108
113
|
const resolveComponent: typeof import('vue').resolveComponent
|
|
109
114
|
const seedPlugin: typeof import('./src/plugins/seed.plugin').seedPlugin
|
|
@@ -230,6 +235,7 @@ declare global {
|
|
|
230
235
|
const useMouseInElement: typeof import('@vueuse/core').useMouseInElement
|
|
231
236
|
const useMousePressed: typeof import('@vueuse/core').useMousePressed
|
|
232
237
|
const useMutationObserver: typeof import('@vueuse/core').useMutationObserver
|
|
238
|
+
const useNavigationGuard: typeof import('./src/shared/composables/useNavigationGuard').useNavigationGuard
|
|
233
239
|
const useNavigatorLanguage: typeof import('@vueuse/core').useNavigatorLanguage
|
|
234
240
|
const useNetwork: typeof import('@vueuse/core').useNetwork
|
|
235
241
|
const useNow: typeof import('@vueuse/core').useNow
|
|
@@ -338,6 +344,9 @@ declare global {
|
|
|
338
344
|
export type { TabbarVisibility } from './src/shared/composables/useTabbarVisibility'
|
|
339
345
|
import('./src/shared/composables/useTabbarVisibility')
|
|
340
346
|
// @ts-ignore
|
|
347
|
+
export type { RecorderBackend, RecorderDiagnostics } from './src/plugins/recorder.plugin'
|
|
348
|
+
import('./src/plugins/recorder.plugin')
|
|
349
|
+
// @ts-ignore
|
|
341
350
|
export type { CaseComparison } from './src/modules/demo/composables/useBenchmark'
|
|
342
351
|
import('./src/modules/demo/composables/useBenchmark')
|
|
343
352
|
// @ts-ignore
|
|
@@ -403,12 +412,14 @@ declare module 'vue' {
|
|
|
403
412
|
readonly initCapacitor: UnwrapRef<typeof import('./src/plugins/capacitor/index')['initCapacitor']>
|
|
404
413
|
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
|
405
414
|
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
|
|
415
|
+
readonly installNavigationGuard: UnwrapRef<typeof import('./src/shared/composables/useNavigationGuard')['installNavigationGuard']>
|
|
406
416
|
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
|
|
407
417
|
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
|
408
418
|
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
|
409
419
|
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
|
410
420
|
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
|
411
421
|
readonly isShallow: UnwrapRef<typeof import('vue')['isShallow']>
|
|
422
|
+
readonly lazyRoute: UnwrapRef<typeof import('./src/shared/utils/lazyRoute')['lazyRoute']>
|
|
412
423
|
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
|
|
413
424
|
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
|
414
425
|
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
|
@@ -441,6 +452,8 @@ declare module 'vue' {
|
|
|
441
452
|
readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']>
|
|
442
453
|
readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']>
|
|
443
454
|
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
|
|
455
|
+
readonly recorderDiagnostics: UnwrapRef<typeof import('./src/plugins/recorder.plugin')['recorderDiagnostics']>
|
|
456
|
+
readonly recorderPlugin: UnwrapRef<typeof import('./src/plugins/recorder.plugin')['recorderPlugin']>
|
|
444
457
|
readonly ref: UnwrapRef<typeof import('vue')['ref']>
|
|
445
458
|
readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
|
|
446
459
|
readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
|
|
@@ -449,6 +462,7 @@ declare module 'vue' {
|
|
|
449
462
|
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
|
|
450
463
|
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
|
|
451
464
|
readonly renderBootstrapError: UnwrapRef<typeof import('./src/plugins/bootstrapError')['renderBootstrapError']>
|
|
465
|
+
readonly reportProblem: UnwrapRef<typeof import('./src/plugins/recorder.plugin')['reportProblem']>
|
|
452
466
|
readonly request: UnwrapRef<typeof import('framework7/lite')['request']>
|
|
453
467
|
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
|
454
468
|
readonly seedPlugin: UnwrapRef<typeof import('./src/plugins/seed.plugin')['seedPlugin']>
|
|
@@ -575,6 +589,7 @@ declare module 'vue' {
|
|
|
575
589
|
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
|
|
576
590
|
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
|
|
577
591
|
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
|
|
592
|
+
readonly useNavigationGuard: UnwrapRef<typeof import('./src/shared/composables/useNavigationGuard')['useNavigationGuard']>
|
|
578
593
|
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
|
|
579
594
|
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
|
|
580
595
|
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
|
|
@@ -16,13 +16,16 @@
|
|
|
16
16
|
"@capacitor/android": "^8.5.0",
|
|
17
17
|
"@capacitor/app": "^8.0.0",
|
|
18
18
|
"@capacitor/core": "^8.0.0",
|
|
19
|
+
"@capacitor/filesystem": "^8.0.0",
|
|
19
20
|
"@capacitor/keyboard": "^8.0.0",
|
|
20
21
|
"@capacitor/preferences": "^8.0.0",
|
|
22
|
+
"@capacitor/share": "^8.0.0",
|
|
21
23
|
"@capacitor/splash-screen": "^8.0.0",
|
|
22
24
|
"@capacitor/status-bar": "^8.0.0",
|
|
23
|
-
"@cavulsqa/mobile-db": "^1.
|
|
24
|
-
"@cavulsqa/reactive-db": "^1.
|
|
25
|
-
"@cavulsqa/reactive-vue": "^1.
|
|
25
|
+
"@cavulsqa/mobile-db": "^1.4.0",
|
|
26
|
+
"@cavulsqa/reactive-db": "^1.4.0",
|
|
27
|
+
"@cavulsqa/reactive-vue": "^1.4.0",
|
|
28
|
+
"@cavulsqa/recorder": "^1.4.0",
|
|
26
29
|
"@sqlite.org/sqlite-wasm": "3.53.0-build1",
|
|
27
30
|
"@tailwindcss/vite": "^4.3.3",
|
|
28
31
|
"@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
|
});
|
|
@@ -9,6 +9,7 @@ import "./assets/css/app.css";
|
|
|
9
9
|
import App from "./App.vue";
|
|
10
10
|
import { i18n } from "./plugins/i18n.plugin";
|
|
11
11
|
import { renderBootstrapError } from "./plugins/bootstrapError";
|
|
12
|
+
import { recorderPlugin } from "./plugins/recorder.plugin";
|
|
12
13
|
import { seedPlugin } from "./plugins/seed.plugin";
|
|
13
14
|
import { sqlitePlugin } from "./plugins/sqlite.plugin";
|
|
14
15
|
|
|
@@ -31,6 +32,8 @@ async function bootstrap(): Promise<void> {
|
|
|
31
32
|
return;
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
void recorderPlugin();
|
|
36
|
+
|
|
34
37
|
// Components arrive through Framework7VueResolver, so nothing is registered by hand.
|
|
35
38
|
const app = createApp(App);
|
|
36
39
|
app.use(i18n);
|
|
@@ -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
|
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
webviewLikelyTooOld,
|
|
4
4
|
webviewVersion,
|
|
5
5
|
} from "@/shared/database/storage";
|
|
6
|
+
import { hideSplashScreen } from "@/plugins/capacitor/useSplashScreen";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* The screen shown when the app cannot start, which in practice means the database did not open.
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
* nothing they could act on.
|
|
16
17
|
*/
|
|
17
18
|
export function renderBootstrapError(error: unknown): void {
|
|
19
|
+
hideSplashScreen();
|
|
18
20
|
const root = document.getElementById("app");
|
|
19
21
|
if (!root) return;
|
|
20
22
|
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createConsoleTrack,
|
|
3
|
+
createDatabasesTrack,
|
|
4
|
+
createNetworkTrack,
|
|
5
|
+
createPerfTrack,
|
|
6
|
+
createRecorder,
|
|
7
|
+
createRrwebTrack,
|
|
8
|
+
type Recorder,
|
|
9
|
+
type TrackContext,
|
|
10
|
+
type TrackRecorder,
|
|
11
|
+
} from "@cavulsqa/recorder";
|
|
12
|
+
import { changeBus } from "@/shared/database/database";
|
|
13
|
+
import { createCapacitorSink } from "@/shared/recorder/capacitorSink";
|
|
14
|
+
|
|
15
|
+
const RRWEB_VERIFY_WINDOW_MS = 2000;
|
|
16
|
+
|
|
17
|
+
export type RecorderBackend = "memory" | "opfs" | null;
|
|
18
|
+
|
|
19
|
+
export interface RecorderDiagnostics {
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
recording: boolean;
|
|
22
|
+
backend: RecorderBackend;
|
|
23
|
+
rrwebAvailable: boolean;
|
|
24
|
+
lastError: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const recorderDiagnostics = ref<RecorderDiagnostics>({
|
|
28
|
+
enabled: false,
|
|
29
|
+
recording: false,
|
|
30
|
+
backend: null,
|
|
31
|
+
rrwebAvailable: true,
|
|
32
|
+
lastError: null,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
let recorder: Recorder | null = null;
|
|
36
|
+
|
|
37
|
+
function resolveFieldRecorderMode(): "on" | "off" {
|
|
38
|
+
const raw = import.meta.env.VITE_FIELD_RECORDER;
|
|
39
|
+
if (raw === "on" || raw === "off") return raw;
|
|
40
|
+
return import.meta.env.DEV ? "on" : "off";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isCapuError(data: unknown): boolean {
|
|
44
|
+
return typeof data === "object" && data !== null && "__capuError" in data;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Wraps `createRrwebTrack` with a runtime check: rrweb's published package resolves to an empty
|
|
49
|
+
* module outside a bundler, and a `record()` call against that module fails silently inside the
|
|
50
|
+
* track's own try/catch, so the recorder would otherwise report a healthy `rrweb` track that never
|
|
51
|
+
* emitted a frame. This watches the first events pushed and flips `rrwebAvailable` to `false` if
|
|
52
|
+
* nothing but a `__capuError` payload arrives within {@link RRWEB_VERIFY_WINDOW_MS}.
|
|
53
|
+
*/
|
|
54
|
+
function createVerifiedRrwebTrack(): TrackRecorder {
|
|
55
|
+
const inner = createRrwebTrack();
|
|
56
|
+
let sawRealFrame = false;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
name: "rrweb",
|
|
60
|
+
async start(ctx: TrackContext<unknown>) {
|
|
61
|
+
const wrapped: TrackContext<unknown> = {
|
|
62
|
+
...ctx,
|
|
63
|
+
push(data, wallMs) {
|
|
64
|
+
if (!sawRealFrame && !isCapuError(data)) sawRealFrame = true;
|
|
65
|
+
ctx.push(data, wallMs);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
await inner.start(wrapped);
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
recorderDiagnostics.value = { ...recorderDiagnostics.value, rrwebAvailable: sawRealFrame };
|
|
71
|
+
if (!sawRealFrame) {
|
|
72
|
+
console.error(
|
|
73
|
+
"[recorder] rrweb produced no frames; recording is running without screen replay.",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}, RRWEB_VERIFY_WINDOW_MS);
|
|
77
|
+
},
|
|
78
|
+
async stop() {
|
|
79
|
+
await inner.stop();
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function buildTracks(): TrackRecorder[] {
|
|
85
|
+
return [
|
|
86
|
+
createVerifiedRrwebTrack(),
|
|
87
|
+
createConsoleTrack(),
|
|
88
|
+
createNetworkTrack(),
|
|
89
|
+
createPerfTrack(),
|
|
90
|
+
createDatabasesTrack({ changeBus }),
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function syncDiagnostics(): void {
|
|
95
|
+
if (!recorder) return;
|
|
96
|
+
const status = recorder.status;
|
|
97
|
+
recorderDiagnostics.value = {
|
|
98
|
+
...recorderDiagnostics.value,
|
|
99
|
+
recording: status.recording,
|
|
100
|
+
backend: status.backend,
|
|
101
|
+
lastError: status.lastError,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Builds and starts the field recorder, gated by `VITE_FIELD_RECORDER` (`"on"` or `"off"`,
|
|
107
|
+
* defaulting to `on` in dev and `off` in a production build). Recovers a leftover session from a
|
|
108
|
+
* previous run before starting a new one. Never throws: every failure lands in
|
|
109
|
+
* {@link recorderDiagnostics}.`lastError` instead, so a broken recorder can never block `main.ts`
|
|
110
|
+
* from mounting the app.
|
|
111
|
+
*/
|
|
112
|
+
export async function recorderPlugin(): Promise<void> {
|
|
113
|
+
const mode = resolveFieldRecorderMode();
|
|
114
|
+
recorderDiagnostics.value = { ...recorderDiagnostics.value, enabled: mode === "on" };
|
|
115
|
+
if (mode === "off") return;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
recorder = createRecorder({
|
|
119
|
+
tracks: buildTracks(),
|
|
120
|
+
sink: createCapacitorSink(),
|
|
121
|
+
label: "field recording",
|
|
122
|
+
appId: __APP_NAME__,
|
|
123
|
+
appVersion: __APP_VERSION__,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await recorder.recover();
|
|
127
|
+
await recorder.start();
|
|
128
|
+
syncDiagnostics();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
recorderDiagnostics.value = {
|
|
131
|
+
...recorderDiagnostics.value,
|
|
132
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Finalizes the current buffer into a `.capu` and hands it to the sink without stopping the
|
|
139
|
+
* recorder, for the Settings "Report a problem" action. Returns `true` when the capture reached
|
|
140
|
+
* the sink with no error.
|
|
141
|
+
*/
|
|
142
|
+
export async function reportProblem(): Promise<boolean> {
|
|
143
|
+
if (!recorder) return false;
|
|
144
|
+
try {
|
|
145
|
+
await recorder.capture("user report");
|
|
146
|
+
} catch (error) {
|
|
147
|
+
recorderDiagnostics.value = {
|
|
148
|
+
...recorderDiagnostics.value,
|
|
149
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
150
|
+
};
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
syncDiagnostics();
|
|
154
|
+
return recorderDiagnostics.value.lastError === null;
|
|
155
|
+
}
|
|
@@ -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
|
|