@cavulsqa/create 2.7.0 → 2.9.2

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/scaffold.mjs CHANGED
@@ -1,6 +1,7 @@
1
+ import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, join } from "node:path";
3
+ import { listTemplateFiles } from "./templateFiles.mjs";
1
4
  import { pruneEngines } from "./pruneEngines.mjs";
2
- import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
3
- import { basename, join } from "node:path";
4
5
 
5
6
  /** Files whose contents carry the app's identity and have to be rewritten, not copied. */
6
7
  function personalise(entry, text, { name, appName }) {
@@ -38,26 +39,12 @@ function manifest(source, { name, appName, templateName }) {
38
39
  )}\n`;
39
40
  }
40
41
 
41
- /**
42
- * Installed, generated or platform-specific: none of it belongs in a generated app.
43
- *
44
- * A bundled template never contains these, but `--from` points at a live working copy that usually
45
- * does - and `node_modules` is full of directory symlinks, which `isDirectory()` reports as files and
46
- * `readFileSync` then rejects with EISDIR. `--from` could not generate anything at all.
47
- */
48
- const SKIP = new Set(["node_modules", "dist", "android", "ios", ".git", "pnpm-lock.yaml"]);
49
-
50
42
  function copyTree(from, to, transform) {
51
- mkdirSync(to, { recursive: true });
52
- for (const entry of readdirSync(from, { withFileTypes: true })) {
53
- if (SKIP.has(entry.name)) continue;
54
- const source = join(from, entry.name);
55
- const target = join(to, entry.name);
56
- if (entry.isDirectory()) {
57
- copyTree(source, target, transform);
58
- continue;
59
- }
60
- const rewritten = transform(entry.name, source);
43
+ for (const file of listTemplateFiles(from)) {
44
+ const source = join(from, file);
45
+ const target = join(to, file);
46
+ mkdirSync(dirname(target), { recursive: true });
47
+ const rewritten = transform(basename(file), source);
61
48
  if (rewritten === null) cpSync(source, target);
62
49
  else writeFileSync(target, rewritten);
63
50
  }
@@ -0,0 +1,62 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, readdirSync } from "node:fs";
3
+ import { join, relative, sep } from "node:path";
4
+
5
+ /**
6
+ * Generated, installed or platform-specific: none of it belongs in a template.
7
+ *
8
+ * Only consulted when git cannot answer - a bundled template carries no repository.
9
+ */
10
+ const SKIP = new Set(["node_modules", "dist", "android", "ios", ".git", "pnpm-lock.yaml"]);
11
+
12
+ /**
13
+ * The files a template consists of, as paths relative to `dir`.
14
+ *
15
+ * Asks git first, and falls back to walking only when git cannot answer. A name blocklist is the
16
+ * wrong instrument for a working copy: it lets through anything nobody thought to name, and a
17
+ * maintainer's stray `build/` and a deploy log both reached a generated app that way. Git already
18
+ * knows what belongs - it is the same list the template fingerprint is computed from, so what ships
19
+ * and what is hashed cannot drift apart.
20
+ */
21
+ export function listTemplateFiles(dir) {
22
+ const tracked = trackedFiles(dir);
23
+ return tracked ?? walk(dir, dir);
24
+ }
25
+
26
+ function trackedFiles(dir) {
27
+ if (!existsSync(join(dir, ".git")) && !insideRepository(dir)) return null;
28
+ try {
29
+ const listing = execFileSync("git", ["ls-files", "-z"], {
30
+ cwd: dir,
31
+ encoding: "utf8",
32
+ maxBuffer: 64 * 1024 * 1024,
33
+ });
34
+ const files = listing
35
+ .split("\0")
36
+ .filter(Boolean)
37
+ .map((file) => file.split("/").join(sep));
38
+ return files.length ? files.filter((file) => !SKIP.has(file.split(sep)[0])) : null;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ function insideRepository(dir) {
45
+ try {
46
+ execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: dir, stdio: "ignore" });
47
+ return true;
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ function walk(root, dir) {
54
+ const found = [];
55
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
56
+ if (SKIP.has(entry.name)) continue;
57
+ const full = join(dir, entry.name);
58
+ if (entry.isDirectory()) found.push(...walk(root, full));
59
+ else found.push(relative(root, full));
60
+ }
61
+ return found;
62
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cavulsqa/create",
3
- "version": "2.7.0",
3
+ "version": "2.9.2",
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-FbgQOdRBk/8d4JoHOGyxK9/r8aLGGRY9ipzvKvfADNk=",
55
+ "templatesFingerprint": "sha256-w5ItqHFEFJCJwe7V3W0FEhJf7foJkanjYLY85j/Wbd8=",
56
56
  "scripts": {
57
57
  "build": "node scripts/bundleTemplates.mjs",
58
58
  "check": "vp check",
@@ -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.1.0",
24
- "@cavulsqa/reactive-db": "^1.1.0",
25
- "@cavulsqa/reactive-vue": "^1.1.0",
23
+ "@cavulsqa/mobile-db": "^1.2.0",
24
+ "@cavulsqa/reactive-db": "^1.2.0",
25
+ "@cavulsqa/reactive-vue": "^1.2.0",
26
26
  "@sqlite.org/sqlite-wasm": "3.53.0-build1",
27
27
  "@tailwindcss/vite": "^4.3.3",
28
28
  "@vueuse/core": "^14.4.0",
@@ -37,8 +37,17 @@ export const DEFAULT_ORDER: StorageCandidate[] = [
37
37
  * A typo is reported and ignored rather than fatal: it should not brick the build's output, but a
38
38
  * setting that was silently dropped looks exactly like one that was applied and did nothing.
39
39
  */
40
+ /**
41
+ * What `VITE_STORAGE_ENGINE` was at build time, or undefined.
42
+ *
43
+ * Exported so the open can report it. Vite inlines this when it bundles, so an empty value means
44
+ * the variable never reached the build - a `.env` written as UTF-16 by PowerShell's `>` parses as
45
+ * garbage and is dropped without a word.
46
+ */
47
+ export const preferredEngine = import.meta.env.VITE_STORAGE_ENGINE;
48
+
40
49
  function preferredFirst(candidates: StorageCandidate[]): StorageCandidate[] {
41
- const preferred = import.meta.env.VITE_STORAGE_ENGINE;
50
+ const preferred = preferredEngine;
42
51
  if (!preferred) return candidates;
43
52
 
44
53
  if (!isStorageId(preferred)) {
@@ -117,7 +117,6 @@
117
117
  "footer": "Change these screens in src/modules/settings/.",
118
118
  "storage": "Storage",
119
119
  "engine": "Database engine",
120
- "engineHint": "Every query reaches SQLite compiled to WebAssembly, against a real file in the Origin Private File System, in a worker - no native bridge. The same engine in a browser as on a phone.",
121
120
  "webview": "WebView",
122
121
  "notDurable": "Storage is not persisted"
123
122
  },
@@ -139,7 +138,7 @@
139
138
  "sqlite": {
140
139
  "title": "SQLite",
141
140
  "subtitle": "Kysely, offline-first",
142
- "text": "Writes take turns on the one native connection while reads run in parallel."
141
+ "text": "SQLite compiled to WebAssembly, in a worker, against a real file in OPFS - no native plugin."
143
142
  },
144
143
  "reactive": {
145
144
  "title": "Reactive queries",
@@ -117,7 +117,6 @@
117
117
  "footer": "Modifiez ces écrans dans src/modules/settings/.",
118
118
  "storage": "Stockage",
119
119
  "engine": "Moteur de base de données",
120
- "engineHint": "Chaque requête atteint SQLite compilé en WebAssembly, sur un vrai fichier dans l'Origin Private File System, dans un worker - sans pont natif. Le même moteur dans un navigateur que sur un téléphone.",
121
120
  "webview": "WebView",
122
121
  "notDurable": "Le stockage n'est pas persistant"
123
122
  },
@@ -139,7 +138,7 @@
139
138
  "sqlite": {
140
139
  "title": "SQLite",
141
140
  "subtitle": "Kysely, hors-ligne d'abord",
142
- "text": "Les écritures se relaient sur la connexion native, les lectures restent parallèles."
141
+ "text": "SQLite compilé en WebAssembly, dans un worker, sur un vrai fichier OPFS - sans plugin natif."
143
142
  },
144
143
  "reactive": {
145
144
  "title": "Requêtes réactives",
@@ -52,7 +52,7 @@
52
52
  </F7ListItem>
53
53
  </F7List>
54
54
  <F7Block class="mt-0!">
55
- <p class="m-0 text-[13px] opacity-60">{{ t("settings.engineHint") }}</p>
55
+ <p class="m-0 text-[13px] opacity-60">{{ tradeoff }}</p>
56
56
  </F7Block>
57
57
 
58
58
  <F7BlockTitle>{{ t("settings.tryIt") }}</F7BlockTitle>
@@ -30,18 +30,7 @@ export const capacitorSqlite: StorageCandidate = {
30
30
  evidence: "measured",
31
31
  probe: probeNativePlatform,
32
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 });
33
+ const { createCapacitorDialect } = await import("@cavulsqa/mobile-db/capacitor");
34
+ return await createCapacitorDialect({ name: "app.sqlite3" });
46
35
  },
47
36
  };
@@ -3,7 +3,7 @@ import { Migrator } from "kysely/migration";
3
3
  import { runWrite, type MobileDatabase } from "@cavulsqa/mobile-db";
4
4
  import { createChangeBus, createReactiveDb } from "@cavulsqa/reactive-db";
5
5
  import { pragmaProfile, pragmasFor } from "@/app/pragmas.config";
6
- import { storageChain } from "@/app/storage.config";
6
+ import { preferredEngine, storageChain } from "@/app/storage.config";
7
7
  import { isStorageId, type StorageAttempt, type StorageCandidate } from "./candidates";
8
8
  import { migrations } from "./migrations";
9
9
  import { describeOpenFailure } from "./storage";
@@ -155,6 +155,14 @@ async function tryEveryCandidate(): Promise<MobileDatabase<Database>> {
155
155
  applied = opened.pragmas;
156
156
  chosen = candidate;
157
157
  attempts.push({ id: candidate.id, outcome: "opened" });
158
+ // Every launch names its engine. A preference that never arrived - a `.env` the build could
159
+ // not parse, a typo - otherwise looks exactly like one that was applied, and the app reports
160
+ // an engine nobody chose.
161
+ console.info(
162
+ `[storage] opened on ${candidate.id}` +
163
+ (preferredEngine ? ` (VITE_STORAGE_ENGINE=${preferredEngine})` : " (no preference set)"),
164
+ attempts,
165
+ );
158
166
  return database;
159
167
  } catch (error) {
160
168
  attempts.push({ id: candidate.id, outcome: "failed", detail: describeOpenFailure(error) });
@@ -91,5 +91,13 @@ export default defineConfig({
91
91
  server: { port: 5173 },
92
92
  build: { target: "esnext" },
93
93
  lint: { options: { typeAware: false } },
94
- fmt: {},
94
+ /**
95
+ * The unplugin declaration files are committed - vue-tsc has no idea what `ref` or an F7
96
+ * component is without them - but the generator rewrites them on every run, and what it emits is
97
+ * not what oxfmt emits. Formatting them means a build and a commit hook take turns rewriting a
98
+ * file no human wrote and no human can keep formatted.
99
+ */
100
+ fmt: {
101
+ ignorePatterns: ["**/auto-imports.d.ts", "**/components.d.ts"],
102
+ },
95
103
  });