@cavulsqa/create 2.4.0 → 2.7.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 +1 -1
- package/lib/catalog.mjs +23 -0
- package/lib/pruneEngines.mjs +90 -0
- package/lib/publishedVersions.mjs +40 -0
- package/lib/scaffold.mjs +8 -0
- package/lib/templateFingerprint.mjs +22 -8
- package/package.json +2 -2
- package/templates/f7-app/.env.example +4 -8
- package/templates/f7-app/CLAUDE.md +6 -6
- package/templates/f7-app/package.json +49 -5
- package/templates/f7-app/src/app/storage.config.ts +7 -4
- package/templates/f7-app/src/shared/database/candidates/capacitorSqlite.ts +47 -0
- package/templates/f7-app/src/shared/database/candidates/index.ts +1 -0
- package/templates/f7-app/src/shared/database/candidates/types.ts +1 -0
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ The `f7-app` template: a tabbed shell, a `domains` / `modules` / `shared` layout
|
|
|
44
44
|
sales demo over a six-table schema — dashboard aggregates, search, an order sheet, swipe actions,
|
|
45
45
|
a detail screen — with tests against real SQLite.
|
|
46
46
|
|
|
47
|
-
Underneath it, the data layer this repository publishes: `@cavulsqa/mobile-db` for
|
|
47
|
+
Underneath it, the data layer this repository publishes: `@cavulsqa/mobile-db` for OPFS SQLite
|
|
48
48
|
under Kysely, `@cavulsqa/reactive-db` for the change bus, and `@cavulsqa/reactive-vue` for
|
|
49
49
|
`useReactiveQuery`. A write announces the tables it touched and every query watching them refetches;
|
|
50
50
|
nothing in a screen asks for a refresh.
|
package/lib/catalog.mjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The workspace catalog, as a flat map of `name` to range.
|
|
6
|
+
*
|
|
7
|
+
* A deliberately small reader: the catalog is a block of `name: range` and nothing else.
|
|
8
|
+
*/
|
|
9
|
+
export function catalog(repoRoot) {
|
|
10
|
+
const entries = new Map();
|
|
11
|
+
let inside = false;
|
|
12
|
+
for (const line of readFileSync(join(repoRoot, "pnpm-workspace.yaml"), "utf8").split(/\r?\n/)) {
|
|
13
|
+
if (/^catalog:\s*$/.test(line)) {
|
|
14
|
+
inside = true;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (inside && /^\S/.test(line)) break;
|
|
18
|
+
if (!inside) continue;
|
|
19
|
+
const match = /^\s+"?([^":]+)"?:\s*(.+?)\s*$/.exec(line);
|
|
20
|
+
if (match) entries.set(match[1], match[2].replace(/^["']|["']$/g, ""));
|
|
21
|
+
}
|
|
22
|
+
return entries;
|
|
23
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Removes every storage engine the generated app did not ask for.
|
|
6
|
+
*
|
|
7
|
+
* Without this the choice is cosmetic: one template means one `package.json`, so picking OPFS still
|
|
8
|
+
* installed `@capacitor-community/sqlite` and put the native plugin in the APK for code that never
|
|
9
|
+
* runs - which is the whole reason the engine sits behind its own entry point in `mobile-db`.
|
|
10
|
+
*
|
|
11
|
+
* The template describes its own engines in `cavulsqa.engineModules`, so nothing here has a list of
|
|
12
|
+
* engine names in it. Each module is one candidate file, and every place that references it does so
|
|
13
|
+
* on a line of its own - a `STORAGE_IDS` entry, an `export` in the candidates barrel, an import
|
|
14
|
+
* specifier and an entry in `DEFAULT_ORDER`. Dropping whole lines is why this stays a few rules
|
|
15
|
+
* rather than a parser.
|
|
16
|
+
*/
|
|
17
|
+
export function pruneEngines(out, manifest, engine) {
|
|
18
|
+
const modules = manifest?.cavulsqa?.engineModules;
|
|
19
|
+
if (!modules || !engine) return { kept: null, dropped: [] };
|
|
20
|
+
|
|
21
|
+
const kept = Object.entries(modules).find(([, module]) => module.engines.includes(engine));
|
|
22
|
+
if (!kept) return { kept: null, dropped: [] };
|
|
23
|
+
|
|
24
|
+
const dropped = Object.entries(modules).filter(([name]) => name !== kept[0]);
|
|
25
|
+
if (dropped.length === 0) return { kept: kept[0], dropped: [] };
|
|
26
|
+
|
|
27
|
+
// A dependency two engines share is not removable, and neither is an engine id that survives.
|
|
28
|
+
const keptDeps = new Set(kept[1].dependencies ?? []);
|
|
29
|
+
const deadDeps = new Set();
|
|
30
|
+
const deadEngines = new Set();
|
|
31
|
+
const deadExports = new Set();
|
|
32
|
+
for (const [, module] of dropped) {
|
|
33
|
+
for (const dep of module.dependencies ?? []) if (!keptDeps.has(dep)) deadDeps.add(dep);
|
|
34
|
+
for (const id of module.engines) deadEngines.add(id);
|
|
35
|
+
for (const symbol of module.exports ?? []) deadExports.add(symbol);
|
|
36
|
+
const file = join(out, module.file);
|
|
37
|
+
if (existsSync(file)) rmSync(file);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
dropLines(join(out, "src/shared/database/candidates/types.ts"), (line) =>
|
|
41
|
+
[...deadEngines].some((id) => line.trim() === `"${id}",`),
|
|
42
|
+
);
|
|
43
|
+
dropLines(join(out, "src/shared/database/candidates/index.ts"), (line) =>
|
|
44
|
+
dropped.some(([, module]) => line.includes(`"./${basenameOf(module.file)}"`)),
|
|
45
|
+
);
|
|
46
|
+
dropLines(join(out, "src/app/storage.config.ts"), (line) =>
|
|
47
|
+
[...deadExports].some((symbol) => line.trim() === `${symbol},`),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// `.env.example` documents every engine, one line each. An app that keeps the line for an engine
|
|
51
|
+
// it no longer has is documentation that lies, and `VITE_STORAGE_ENGINE` would name a candidate
|
|
52
|
+
// the chain cannot offer.
|
|
53
|
+
dropLines(join(out, ".env.example"), (line) =>
|
|
54
|
+
[...deadEngines].some((id) => line.startsWith(`# ${id} `)),
|
|
55
|
+
);
|
|
56
|
+
rewriteLine(join(out, ".env.example"), /^VITE_STORAGE_ENGINE=/, `VITE_STORAGE_ENGINE=${engine}`);
|
|
57
|
+
|
|
58
|
+
pruneDependencies(join(out, "package.json"), deadDeps);
|
|
59
|
+
return { kept: kept[0], dropped: dropped.map(([name]) => name) };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function basenameOf(file) {
|
|
63
|
+
return file.split("/").pop().replace(/\.ts$/, "");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function dropLines(path, matches) {
|
|
67
|
+
if (!existsSync(path)) return;
|
|
68
|
+
const kept = readFileSync(path, "utf8")
|
|
69
|
+
.split("\n")
|
|
70
|
+
.filter((line) => !matches(line));
|
|
71
|
+
writeFileSync(path, kept.join("\n"));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function rewriteLine(path, matches, replacement) {
|
|
75
|
+
if (!existsSync(path)) return;
|
|
76
|
+
const lines = readFileSync(path, "utf8")
|
|
77
|
+
.split("\n")
|
|
78
|
+
.map((line) => (matches.test(line) ? replacement : line));
|
|
79
|
+
writeFileSync(path, lines.join("\n"));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function pruneDependencies(path, dead) {
|
|
83
|
+
if (!existsSync(path) || dead.size === 0) return;
|
|
84
|
+
const pkg = JSON.parse(readFileSync(path, "utf8"));
|
|
85
|
+
for (const group of ["dependencies", "devDependencies"]) {
|
|
86
|
+
if (!pkg[group]) continue;
|
|
87
|
+
for (const dep of dead) delete pkg[group][dep];
|
|
88
|
+
}
|
|
89
|
+
writeFileSync(path, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
90
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
/** Every publishable package under `packages/`, mapped to the version it currently declares. */
|
|
5
|
+
export function publishedVersions(repoRoot) {
|
|
6
|
+
const versions = new Map();
|
|
7
|
+
for (const dir of readdirSync(join(repoRoot, "packages")).sort()) {
|
|
8
|
+
const manifest = join(repoRoot, "packages", dir, "package.json");
|
|
9
|
+
if (!existsSync(manifest)) continue;
|
|
10
|
+
const pkg = JSON.parse(readFileSync(manifest, "utf8"));
|
|
11
|
+
if (!pkg.private) versions.set(pkg.name, pkg.version);
|
|
12
|
+
}
|
|
13
|
+
return versions;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The sibling packages the templates declare as `workspace:` deps, in the order they are named.
|
|
18
|
+
*
|
|
19
|
+
* These are the ones the bundler rewrites to a concrete `^` range, so they are the ones whose
|
|
20
|
+
* versions change what a published creator scaffolds.
|
|
21
|
+
*/
|
|
22
|
+
export function workspaceDependencies(repoRoot) {
|
|
23
|
+
const names = new Set();
|
|
24
|
+
const templates = join(repoRoot, "templates");
|
|
25
|
+
if (!existsSync(templates)) return [];
|
|
26
|
+
|
|
27
|
+
for (const name of readdirSync(templates).sort()) {
|
|
28
|
+
const manifest = join(templates, name, "package.json");
|
|
29
|
+
if (!existsSync(manifest)) continue;
|
|
30
|
+
const pkg = JSON.parse(readFileSync(manifest, "utf8"));
|
|
31
|
+
for (const group of [pkg.dependencies, pkg.devDependencies]) {
|
|
32
|
+
for (const [dep, range] of Object.entries(group ?? {})) {
|
|
33
|
+
if (typeof range === "string" && range.startsWith("workspace:")) names.add(dep);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Codepoint order, not locale order: this list is hashed, and the hash has to match on every
|
|
38
|
+
// machine that computes it.
|
|
39
|
+
return [...names].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
40
|
+
}
|
package/lib/scaffold.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { pruneEngines } from "./pruneEngines.mjs";
|
|
1
2
|
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { basename, join } from "node:path";
|
|
3
4
|
|
|
@@ -90,6 +91,13 @@ export function scaffold({ templateDir, out, name, appId, appName, engine, pragm
|
|
|
90
91
|
* and change, while the choice of engine for one deployment is configuration. `.env.example` is
|
|
91
92
|
* copied in by the template and documents every value.
|
|
92
93
|
*/
|
|
94
|
+
// Before the .env: picking an engine also removes the ones you did not pick, so the app installs
|
|
95
|
+
// only the plugin or wasm it actually reaches.
|
|
96
|
+
if (engine) {
|
|
97
|
+
const templateManifest = JSON.parse(readFileSync(join(templateDir, "package.json"), "utf8"));
|
|
98
|
+
pruneEngines(out, templateManifest, engine);
|
|
99
|
+
}
|
|
100
|
+
|
|
93
101
|
if (engine || pragmas) {
|
|
94
102
|
const lines = ["# Written by @cavulsqa/create. See .env.example for what these mean.", ""];
|
|
95
103
|
if (engine) lines.push(`VITE_STORAGE_ENGINE=${engine}`);
|
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
+
import { publishedVersions, workspaceDependencies } from "./publishedVersions.mjs";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* A content hash of
|
|
6
|
-
* was republished after
|
|
6
|
+
* A content hash of everything a published creator would bundle, used to prove that
|
|
7
|
+
* `@cavulsqa/create` was republished after any of it changed.
|
|
7
8
|
*
|
|
8
9
|
* The creator bundles the templates at pack time, so a template fix reaches nobody until a new
|
|
9
10
|
* version of the creator goes out. Nothing about that is visible in a diff, which is exactly the
|
|
10
11
|
* kind of coupling that gets forgotten - so it is checked rather than remembered.
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* Two inputs, because the bundle has two:
|
|
14
|
+
*
|
|
15
|
+
* - The tracked files under `templates/`. It asks git rather than walking the directory. Walking
|
|
16
|
+
* gave two different answers on a Windows working copy and on a CI checkout, because the disk
|
|
17
|
+
* holds whatever each machine happens to have: ignored files, build leftovers, and CRLF where
|
|
18
|
+
* the runner has LF. `git ls-files -s` reports the blob hash recorded in the index, which is
|
|
19
|
+
* normalised, platform-independent, and - the point - describes exactly what a `git clone` of
|
|
20
|
+
* this repository would produce.
|
|
21
|
+
* - The sibling versions the bundler pins those templates to. A template declares
|
|
22
|
+
* `workspace:*` and the bundler rewrites it to `^<current version>`, so bumping a library
|
|
23
|
+
* changes what the creator scaffolds while leaving every tracked file byte-identical. Hashing
|
|
24
|
+
* the files alone said "unchanged" and let the creator keep shipping ranges that no longer
|
|
25
|
+
* admitted the packages it was meant to install.
|
|
17
26
|
*/
|
|
18
27
|
export function fingerprintTemplates(repoRoot) {
|
|
19
28
|
const listing = execFileSync("git", ["ls-files", "-s", "--", "templates"], {
|
|
@@ -25,5 +34,10 @@ export function fingerprintTemplates(repoRoot) {
|
|
|
25
34
|
const lines = listing.split("\n").filter(Boolean).sort();
|
|
26
35
|
if (!lines.length) throw new Error("git reports no tracked files under templates/");
|
|
27
36
|
|
|
28
|
-
|
|
37
|
+
const versions = publishedVersions(repoRoot);
|
|
38
|
+
const pins = workspaceDependencies(repoRoot).map((name) => `${name}@${versions.get(name)}`);
|
|
39
|
+
|
|
40
|
+
return `sha256-${createHash("sha256")
|
|
41
|
+
.update([...lines, ...pins].join("\n"))
|
|
42
|
+
.digest("base64")}`;
|
|
29
43
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cavulsqa/create",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
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-FbgQOdRBk/8d4JoHOGyxK9/r8aLGGRY9ipzvKvfADNk=",
|
|
56
56
|
"scripts": {
|
|
57
57
|
"build": "node scripts/bundleTemplates.mjs",
|
|
58
58
|
"check": "vp check",
|
|
@@ -6,15 +6,11 @@
|
|
|
6
6
|
# The engine tried first. The rest of the chain in src/app/storage.config.ts still follows it as
|
|
7
7
|
# fallback, so this reorders rather than restricts.
|
|
8
8
|
#
|
|
9
|
-
# sqlite-wasm-opfs-sahpool official @sqlite.org/sqlite-wasm
|
|
10
|
-
#
|
|
11
|
-
# build. The default for those reasons.
|
|
12
|
-
# wa-sqlite-access-handle-pool wa-sqlite, OPFS pool. Faster at joins and scans on measured
|
|
13
|
-
# hardware; a single-maintainer project.
|
|
9
|
+
# sqlite-wasm-opfs-sahpool official @sqlite.org/sqlite-wasm on an OPFS pool. Fastest measured, the SQLite team's own build, and the default.
|
|
10
|
+
# wa-sqlite-access-handle-pool wa-sqlite on an OPFS pool. Faster at joins and scans on measured hardware; a single-maintainer project.
|
|
14
11
|
# wa-sqlite-opfs-async wa-sqlite over OPFS without the pool, on the Asyncify build.
|
|
15
|
-
# wa-sqlite-idb-batch-atomic SQLite pages in IndexedDB. The slowest, and the only
|
|
16
|
-
#
|
|
17
|
-
# between Chromium 86 and 108.
|
|
12
|
+
# wa-sqlite-idb-batch-atomic SQLite pages in IndexedDB. The slowest, and the only durable route on a WebView below Chromium 109.
|
|
13
|
+
# capacitor-sqlite the native plugin. Slower on every measured axis and it ships in the APK, but a read is not blocked by an in-flight write.
|
|
18
14
|
VITE_STORAGE_ENGINE=sqlite-wasm-opfs-sahpool
|
|
19
15
|
|
|
20
16
|
# PRAGMA profile. `safe` keeps SQLite's durability guarantee; `fast` trades it for speed by handing
|
|
@@ -10,15 +10,15 @@ SQLite — it does not become the thing screens read.
|
|
|
10
10
|
## Stack
|
|
11
11
|
|
|
12
12
|
Vue 3.5 `<script setup lang="ts">` · Framework7 9 (+ framework7-vue 8) · Capacitor 8 (Android
|
|
13
|
-
first) · Kysely over
|
|
13
|
+
first) · Kysely over SQLite in an OPFS worker · vue-i18n · Tailwind 4 · Vite+ (`vp`).
|
|
14
14
|
|
|
15
15
|
Data layer comes from three published packages, not from this repo:
|
|
16
16
|
|
|
17
|
-
| Package | What it gives you
|
|
18
|
-
| ------------------------ |
|
|
19
|
-
| `@cavulsqa/mobile-db` | The
|
|
20
|
-
| `@cavulsqa/reactive-db` | Change bus, result cache, visibility gate, query metrics
|
|
21
|
-
| `@cavulsqa/reactive-vue` | `useReactiveQuery` and the Framework7 page-visibility adapter
|
|
17
|
+
| Package | What it gives you |
|
|
18
|
+
| ------------------------ | ------------------------------------------------------------- |
|
|
19
|
+
| `@cavulsqa/mobile-db` | The OPFS worker dialect, migrations, transaction-aware writes |
|
|
20
|
+
| `@cavulsqa/reactive-db` | Change bus, result cache, visibility gate, query metrics |
|
|
21
|
+
| `@cavulsqa/reactive-vue` | `useReactiveQuery` and the Framework7 page-visibility adapter |
|
|
22
22
|
|
|
23
23
|
Do not vendor or fork them. If one is wrong, fix it there and bump the version.
|
|
24
24
|
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"check": "vp check"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
+
"@capacitor-community/sqlite": "^8.1.1",
|
|
15
16
|
"@capacitor/android": "^8.5.0",
|
|
16
17
|
"@capacitor/app": "^8.0.0",
|
|
17
18
|
"@capacitor/core": "^8.0.0",
|
|
@@ -19,9 +20,9 @@
|
|
|
19
20
|
"@capacitor/preferences": "^8.0.0",
|
|
20
21
|
"@capacitor/splash-screen": "^8.0.0",
|
|
21
22
|
"@capacitor/status-bar": "^8.0.0",
|
|
22
|
-
"@cavulsqa/mobile-db": "^
|
|
23
|
-
"@cavulsqa/reactive-db": "^
|
|
24
|
-
"@cavulsqa/reactive-vue": "^
|
|
23
|
+
"@cavulsqa/mobile-db": "^1.1.0",
|
|
24
|
+
"@cavulsqa/reactive-db": "^1.1.0",
|
|
25
|
+
"@cavulsqa/reactive-vue": "^1.1.0",
|
|
25
26
|
"@sqlite.org/sqlite-wasm": "3.53.0-build1",
|
|
26
27
|
"@tailwindcss/vite": "^4.3.3",
|
|
27
28
|
"@vueuse/core": "^14.4.0",
|
|
@@ -53,7 +54,50 @@
|
|
|
53
54
|
"sqlite-wasm-opfs-sahpool",
|
|
54
55
|
"wa-sqlite-access-handle-pool",
|
|
55
56
|
"wa-sqlite-opfs-async",
|
|
56
|
-
"wa-sqlite-idb-batch-atomic"
|
|
57
|
-
|
|
57
|
+
"wa-sqlite-idb-batch-atomic",
|
|
58
|
+
"capacitor-sqlite"
|
|
59
|
+
],
|
|
60
|
+
"engineModules": {
|
|
61
|
+
"opfsSahPool": {
|
|
62
|
+
"file": "src/shared/database/candidates/opfsSahPool.ts",
|
|
63
|
+
"engines": [
|
|
64
|
+
"sqlite-wasm-opfs-sahpool"
|
|
65
|
+
],
|
|
66
|
+
"exports": [
|
|
67
|
+
"opfsSahPool"
|
|
68
|
+
],
|
|
69
|
+
"dependencies": [
|
|
70
|
+
"@sqlite.org/sqlite-wasm"
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
"waSqlite": {
|
|
74
|
+
"file": "src/shared/database/candidates/waSqlite.ts",
|
|
75
|
+
"engines": [
|
|
76
|
+
"wa-sqlite-access-handle-pool",
|
|
77
|
+
"wa-sqlite-opfs-async",
|
|
78
|
+
"wa-sqlite-idb-batch-atomic"
|
|
79
|
+
],
|
|
80
|
+
"exports": [
|
|
81
|
+
"waAccessHandlePool",
|
|
82
|
+
"waOriginPrivateFileSystem",
|
|
83
|
+
"waIdbBatchAtomic"
|
|
84
|
+
],
|
|
85
|
+
"dependencies": [
|
|
86
|
+
"wa-sqlite"
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
"capacitorSqlite": {
|
|
90
|
+
"file": "src/shared/database/candidates/capacitorSqlite.ts",
|
|
91
|
+
"engines": [
|
|
92
|
+
"capacitor-sqlite"
|
|
93
|
+
],
|
|
94
|
+
"exports": [
|
|
95
|
+
"capacitorSqlite"
|
|
96
|
+
],
|
|
97
|
+
"dependencies": [
|
|
98
|
+
"@capacitor-community/sqlite"
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
}
|
|
58
102
|
}
|
|
59
103
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
capacitorSqlite,
|
|
2
3
|
isStorageId,
|
|
3
4
|
opfsSahPool,
|
|
4
5
|
waAccessHandlePool,
|
|
@@ -10,11 +11,12 @@ import {
|
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* The knob. Walked in order, first candidate that opens wins, and the rest are never imported - so a
|
|
13
|
-
* chain that stops at the first entry never downloads
|
|
14
|
+
* chain that stops at the first entry never downloads the others' wasm.
|
|
14
15
|
*
|
|
15
|
-
* Reorder freely to trade speed for reach:
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* Reorder freely to trade speed for reach: the entries lower down reach older WebViews, and each
|
|
17
|
+
* one states its own `tradeoff` and whether its `evidence` came from a device or a vendor's README.
|
|
18
|
+
* Read the list rather than this comment - `@cavulsqa/create` removes the engines an app did not
|
|
19
|
+
* ask for, so what is below is what this app actually has.
|
|
18
20
|
*
|
|
19
21
|
* No in-memory entry on purpose: a chain that ends somewhere data is not kept is worse than one that
|
|
20
22
|
* fails and names every attempt.
|
|
@@ -24,6 +26,7 @@ export const DEFAULT_ORDER: StorageCandidate[] = [
|
|
|
24
26
|
waAccessHandlePool,
|
|
25
27
|
waOriginPrivateFileSystem,
|
|
26
28
|
waIdbBatchAtomic,
|
|
29
|
+
capacitorSqlite,
|
|
27
30
|
];
|
|
28
31
|
|
|
29
32
|
/**
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Capacitor } from "@capacitor/core";
|
|
2
|
+
import type { StorageCandidate, StorageProbe } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `@capacitor-community/sqlite`: a real file behind a native bridge.
|
|
6
|
+
*
|
|
7
|
+
* Slower than the OPFS engine on every axis measured on a device - batched writes about 2.4x, the
|
|
8
|
+
* app's own screen queries about 1.3x - and it needs the plugin in the APK. It is here for the one
|
|
9
|
+
* thing the worker cannot do: the worker is serial, so a read waits behind an in-flight write,
|
|
10
|
+
* where this dialect keeps reads outside the write lock. Choose it for an app that writes
|
|
11
|
+
* continuously while the UI reads, or that needs SQLCipher or native access to the file.
|
|
12
|
+
*/
|
|
13
|
+
function probeNativePlatform(): StorageProbe {
|
|
14
|
+
if (!Capacitor.isNativePlatform()) {
|
|
15
|
+
return {
|
|
16
|
+
supported: false,
|
|
17
|
+
reason: "the SQLite plugin is native-only; on web it needs jeep-sqlite and a separate store",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
return { supported: true };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const capacitorSqlite: StorageCandidate = {
|
|
24
|
+
id: "capacitor-sqlite",
|
|
25
|
+
label: "Capacitor SQLite · native plugin",
|
|
26
|
+
tradeoff:
|
|
27
|
+
"Needs the native plugin in the APK, and every statement crosses the bridge. Slower than the " +
|
|
28
|
+
"worker engines, but reads are not blocked by an in-flight write.",
|
|
29
|
+
durable: true,
|
|
30
|
+
evidence: "measured",
|
|
31
|
+
probe: probeNativePlatform,
|
|
32
|
+
createDialect: async () => {
|
|
33
|
+
const [{ CapacitorSQLite, SQLiteConnection }, { SharedConnectionSQLiteDialect }] =
|
|
34
|
+
await Promise.all([
|
|
35
|
+
import("@capacitor-community/sqlite"),
|
|
36
|
+
import("@cavulsqa/mobile-db/capacitor"),
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const sqlite = new SQLiteConnection(CapacitorSQLite);
|
|
40
|
+
const name = "app.sqlite3";
|
|
41
|
+
await sqlite.createConnection(name, false, "no-encryption", 1, false);
|
|
42
|
+
const database = await sqlite.retrieveConnection(name, false);
|
|
43
|
+
await database.open();
|
|
44
|
+
|
|
45
|
+
return new SharedConnectionSQLiteDialect({ database, sqlite, name, serializeAccess: true });
|
|
46
|
+
},
|
|
47
|
+
};
|