@cavulsqa/create 2.5.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/lib/catalog.mjs +23 -0
- package/lib/pruneEngines.mjs +90 -0
- package/lib/scaffold.mjs +8 -0
- package/package.json +2 -2
- package/templates/f7-app/.env.example +4 -8
- 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/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
|
+
}
|
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}`);
|
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
|
|
@@ -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": "^1.
|
|
23
|
-
"@cavulsqa/reactive-db": "^1.
|
|
24
|
-
"@cavulsqa/reactive-vue": "^1.
|
|
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
|
+
};
|