create-syncular-app 0.15.44 → 0.15.45

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 CHANGED
@@ -7,24 +7,13 @@ bun create syncular-app my-app # prompts for the template
7
7
  bunx create-syncular-app my-app --template web
8
8
  ```
9
9
 
10
- > Naming (`@syncular/*`, bin `create-syncular-app`, CLI `syncular`) is not
11
- > final — package identity is TODO 6.3. Every user-visible name lives in
12
- > [`src/constants.ts`](./src/constants.ts) so a rename is mechanical: edit the
13
- > constants, regenerate the templates' generated files, done.
14
-
15
10
  ## Templates
16
11
 
17
12
  | Template | Shape |
18
13
  |---|---|
19
14
  | `minimal` | Server + a terminal two-client convergence demo (no browser) — migrations + manifest + `generate` wiring. Copy-evolved from `examples/quickstart`. The smallest honest starting point. |
20
15
  | `web` | Hono server + WebSocket realtime + a single-pane browser todo app whose whole client core runs in a Web Worker on OPFS. Derived from `apps/demo`, slimmed to one pane (no conflict simulator, no blob attachments) — the minimal browser app a real user starts from. |
21
- | `tauri` | One React codebase, web + desktop (RFC 0002 §4.1): the `web` template's server plus a shared React tree behind the `__TAURI_INTERNALS__` engine seam (`src/frontend/engine.ts`) — worker core on OPFS in the browser, native Rust core in a `src-tauri/` host (`tauri-plugin-syncular` from crates.io, `native-transport`). Derived from `bindings/tauri/example` + the [web+desktop guide](../../apps/docs/src/content/guide-web-desktop.md). |
22
-
23
- > **Next-template candidate: `react` (web-only).** [`apps/demo-react`](../../apps/demo-react)
24
- > is the ready-made source for a hooks-based web-only template — the `tauri`
25
- > template already carries the hook surface for the two-host story. Slim
26
- > demo-react the way `web` slims `apps/demo` when a web-only React template
27
- > is wanted. Not built yet — noted so the shape is on record.
16
+ | `tauri` | One React codebase, web + desktop: the `web` template's server plus a shared React tree behind the `__TAURI_INTERNALS__` engine seam (`src/frontend/engine.ts`) — worker core on OPFS in the browser, native Rust core in a `src-tauri/` host (`tauri-plugin-syncular` from crates.io, `native-transport`). Derived from `bindings/tauri/example` + the [web+desktop guide](../../apps/docs/src/content/guide-web-desktop.md). |
28
17
 
29
18
  Each template ships its own `README.md` (run steps, what to edit first),
30
19
  `.gitignore` (as `gitignore` — see below), a working `tsconfig.json`, and a
@@ -40,13 +29,8 @@ ranges:
40
29
  are the only ranges that resolve when the scaffolded app sits inside this
41
30
  repo's workspace.
42
31
  - **default** (a normal `bunx create-…` run): rewrite to
43
- `PUBLISHED_DEPENDENCY_RANGE` (`src/constants.ts`). **Today that constant is
44
- *also* `workspace:*`** because the workspace packages are unpublished and
45
- version-less (all `private`, no `version` — TODO 6.3), so there is no honest
46
- semver range yet. The CLI **warns loudly** in this case: a `bun install`
47
- outside the repo cannot resolve the deps until publishing lands. When the
48
- packages ship, flip that one constant to `^<version>` (or teach the CLI to
49
- read the published version) — a single edit.
32
+ `PUBLISHED_DEPENDENCY_RANGE` (`src/constants.ts`), derived from the
33
+ scaffolder's own version because all Syncular packages release in lockstep.
50
34
 
51
35
  `.gitignore` ships as `gitignore` (no dot) because npm strips real dotfiles
52
36
  from published tarballs; the scaffolder renames it on copy.
@@ -1,9 +1,7 @@
1
1
  /**
2
- * The ONE place naming lives (TODO 6.3 is a product decision — final package
3
- * identity is not settled). Every user-visible name the scaffolder emits comes
4
- * from here so a rename is mechanical: change these constants, regenerate the
5
- * templates' generated files, done. No name is hardcoded elsewhere in this
6
- * package — grep for the literals below and you should only find them here.
2
+ * The one place scaffolder naming lives. Every user-visible name the
3
+ * scaffolder emits comes from here so a rename is mechanical: change these
4
+ * constants and regenerate the templates' generated files.
7
5
  */
8
6
  /** The scoped npm namespace for the v2 packages, e.g. `@syncular`. */
9
7
  export declare const PACKAGE_SCOPE = "@syncular";
package/dist/constants.js CHANGED
@@ -1,10 +1,8 @@
1
1
  import { createRequire } from 'node:module';
2
2
  /**
3
- * The ONE place naming lives (TODO 6.3 is a product decision — final package
4
- * identity is not settled). Every user-visible name the scaffolder emits comes
5
- * from here so a rename is mechanical: change these constants, regenerate the
6
- * templates' generated files, done. No name is hardcoded elsewhere in this
7
- * package — grep for the literals below and you should only find them here.
3
+ * The one place scaffolder naming lives. Every user-visible name the
4
+ * scaffolder emits comes from here so a rename is mechanical: change these
5
+ * constants and regenerate the templates' generated files.
8
6
  */
9
7
  /** The scoped npm namespace for the v2 packages, e.g. `@syncular`. */
10
8
  export const PACKAGE_SCOPE = '@syncular';
@@ -16,9 +16,8 @@ export declare function packageNameFromDirectory(targetDir: string): string;
16
16
  * when the scaffolded app sits inside this repo's workspace (the in-tree
17
17
  * smoke test path, and `--local` for anyone hacking on the tree).
18
18
  * - `local: false` rewrites to {@link PUBLISHED_DEPENDENCY_RANGE}. Today that
19
- * is *also* `workspace:*` because the v2 packages are unpublished and
20
- * version-less (TODO 6.3); the CLI warns. It is one constant to flip the day
21
- * they publish.
19
+ * is derived from the scaffolder's version because the packages release in
20
+ * lockstep.
22
21
  */
23
22
  export declare function rewriteTemplatePackageJson(source: string, options: {
24
23
  packageName: string;
package/dist/scaffold.js CHANGED
@@ -37,9 +37,8 @@ function isWorkspacePackage(dependencyName) {
37
37
  * when the scaffolded app sits inside this repo's workspace (the in-tree
38
38
  * smoke test path, and `--local` for anyone hacking on the tree).
39
39
  * - `local: false` rewrites to {@link PUBLISHED_DEPENDENCY_RANGE}. Today that
40
- * is *also* `workspace:*` because the v2 packages are unpublished and
41
- * version-less (TODO 6.3); the CLI warns. It is one constant to flip the day
42
- * they publish.
40
+ * is derived from the scaffolder's version because the packages release in
41
+ * lockstep.
43
42
  */
44
43
  export function rewriteTemplatePackageJson(source, options) {
45
44
  const pkg = JSON.parse(source);
package/package.json CHANGED
@@ -1,33 +1,12 @@
1
1
  {
2
2
  "name": "create-syncular-app",
3
- "version": "0.15.44",
4
- "description": "Scaffold a new Syncular app: `create-syncular-app`",
5
- "license": "Apache-2.0",
3
+ "version": "0.15.45",
6
4
  "author": "Benjamin Kniffler",
7
- "homepage": "https://syncular.dev",
8
5
  "repository": {
9
6
  "type": "git",
10
7
  "url": "https://github.com/syncular/syncular.git",
11
8
  "directory": "packages/create-app"
12
9
  },
13
- "bugs": {
14
- "url": "https://github.com/syncular/syncular/issues"
15
- },
16
- "keywords": [
17
- "sync",
18
- "offline-first",
19
- "realtime",
20
- "database",
21
- "typescript"
22
- ],
23
- "type": "module",
24
- "sideEffects": false,
25
- "publishConfig": {
26
- "access": "public"
27
- },
28
- "bin": {
29
- "create-syncular-app": "./dist/cli.js"
30
- },
31
10
  "exports": {
32
11
  ".": {
33
12
  "bun": "./src/index.ts",
@@ -38,6 +17,13 @@
38
17
  }
39
18
  }
40
19
  },
20
+ "bin": {
21
+ "create-syncular-app": "./dist/cli.js"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/syncular/syncular/issues"
25
+ },
26
+ "description": "Scaffold a new Syncular app: `create-syncular-app`",
41
27
  "files": [
42
28
  "dist",
43
29
  "src",
@@ -48,7 +34,18 @@
48
34
  "!dist/**/*.test.js",
49
35
  "!dist/**/*.test.d.ts"
50
36
  ],
51
- "devDependencies": {
52
- "@syncular/typegen": "0.15.44"
53
- }
37
+ "homepage": "https://syncular.dev",
38
+ "keywords": [
39
+ "sync",
40
+ "offline-first",
41
+ "realtime",
42
+ "database",
43
+ "typescript"
44
+ ],
45
+ "license": "Apache-2.0",
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "sideEffects": false,
50
+ "type": "module"
54
51
  }
package/src/constants.ts CHANGED
@@ -1,11 +1,9 @@
1
1
  import { createRequire } from 'node:module';
2
2
 
3
3
  /**
4
- * The ONE place naming lives (TODO 6.3 is a product decision — final package
5
- * identity is not settled). Every user-visible name the scaffolder emits comes
6
- * from here so a rename is mechanical: change these constants, regenerate the
7
- * templates' generated files, done. No name is hardcoded elsewhere in this
8
- * package — grep for the literals below and you should only find them here.
4
+ * The one place scaffolder naming lives. Every user-visible name the
5
+ * scaffolder emits comes from here so a rename is mechanical: change these
6
+ * constants and regenerate the templates' generated files.
9
7
  */
10
8
 
11
9
  /** The scoped npm namespace for the v2 packages, e.g. `@syncular`. */
package/src/scaffold.ts CHANGED
@@ -55,9 +55,8 @@ function isWorkspacePackage(dependencyName: string): boolean {
55
55
  * when the scaffolded app sits inside this repo's workspace (the in-tree
56
56
  * smoke test path, and `--local` for anyone hacking on the tree).
57
57
  * - `local: false` rewrites to {@link PUBLISHED_DEPENDENCY_RANGE}. Today that
58
- * is *also* `workspace:*` because the v2 packages are unpublished and
59
- * version-less (TODO 6.3); the CLI warns. It is one constant to flip the day
60
- * they publish.
58
+ * is derived from the scaffolder's version because the packages release in
59
+ * lockstep.
61
60
  */
62
61
  export function rewriteTemplatePackageJson(
63
62
  source: string,
@@ -21,7 +21,7 @@ tauri = { version = "2", features = [] }
21
21
  # The syncular plugin from crates.io. `native-transport` gives the core its
22
22
  # real HTTP + WebSocket transport (ureq + tungstenite) inside the host
23
23
  # process — the webview never talks to the sync server directly.
24
- tauri-plugin-syncular = { version = "0.15.44", features = ["native-transport"] }
24
+ tauri-plugin-syncular = { version = "0.15.45", features = ["native-transport"] }
25
25
 
26
26
  [features]
27
27
  default = []
@@ -17,6 +17,11 @@ Open the URL, add todos, toggle "Go offline" and keep editing — the outbox
17
17
  counter grows and drains with idempotent retry on reconnect. Open a second
18
18
  browser window on the same URL to watch them converge over the realtime socket.
19
19
 
20
+ The page checks whether the origin has eviction-resistant storage and offers a
21
+ `Protect offline data` action while it remains best effort. If permission is
22
+ not granted, a non-empty outbox displays a warning because browser eviction can
23
+ remove the OPFS database and its unsynced writes.
24
+
20
25
  `bun test` runs a server-level convergence smoke test (the browser UI is
21
26
  covered by `tsc`).
22
27
 
@@ -11,6 +11,7 @@
11
11
  header h1 { font-size: 1.2rem; margin: 0; }
12
12
  .badge { font-size: 0.7rem; padding: 0.1rem 0.45rem; border-radius: 999px; border: 1px solid currentColor; opacity: 0.7; }
13
13
  #status { font-size: 0.8rem; opacity: 0.7; min-height: 1.1em; margin: 0 0 1rem; }
14
+ #storage-warning { font-size: 0.8rem; border: 1px solid currentColor; padding: 0.6rem; }
14
15
  form { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
15
16
  form input { flex: 1; padding: 0.4rem 0.6rem; }
16
17
  ul { list-style: none; margin: 0; padding: 0; }
@@ -24,9 +25,12 @@
24
25
  <header>
25
26
  <h1>__PROJECT_NAME__</h1>
26
27
  <span id="outbox" class="badge">outbox 0</span>
28
+ <span id="storage" class="badge">storage checking</span>
29
+ <button id="storage-btn" hidden>Protect offline data</button>
27
30
  <button id="offline-btn">Go offline</button>
28
31
  </header>
29
32
  <p id="status">starting…</p>
33
+ <p id="storage-warning" role="alert" hidden></p>
30
34
  <form id="add-form">
31
35
  <input id="add-input" placeholder="Add a todo…" autocomplete="off" />
32
36
  <button type="submit">Add</button>
@@ -14,11 +14,14 @@
14
14
  */
15
15
  import {
16
16
  browserConnectivitySignal,
17
+ type BrowserStoragePersistence,
18
+ checkBrowserStoragePersistence,
17
19
  ClientSyncError,
18
20
  createSyncClientHandle,
19
21
  documentLifecycleSignal,
20
22
  installRealtimeSupervisor,
21
23
  realtimeSupervisorSnapshot,
24
+ requestBrowserStoragePersistence,
22
25
  type SqlRow,
23
26
  subscribeRealtimeSupervisor,
24
27
  } from '@syncular/client';
@@ -39,6 +42,11 @@ interface Todo {
39
42
  const statusEl = document.getElementById('status') as HTMLElement;
40
43
  const listEl = document.getElementById('todos') as HTMLUListElement;
41
44
  const outboxEl = document.getElementById('outbox') as HTMLElement;
45
+ const storageEl = document.getElementById('storage') as HTMLElement;
46
+ const storageWarningEl = document.getElementById(
47
+ 'storage-warning',
48
+ ) as HTMLElement;
49
+ const storageBtn = document.getElementById('storage-btn') as HTMLButtonElement;
42
50
  const form = document.getElementById('add-form') as HTMLFormElement;
43
51
  const input = document.getElementById('add-input') as HTMLInputElement;
44
52
  const offlineBtn = document.getElementById('offline-btn') as HTMLButtonElement;
@@ -48,6 +56,8 @@ function setStatus(text: string): void {
48
56
  }
49
57
 
50
58
  async function main(): Promise<void> {
59
+ let storagePersistence: BrowserStoragePersistence =
60
+ await checkBrowserStoragePersistence();
51
61
  const handle = await createSyncClientHandle({
52
62
  worker: () => new Worker('/worker.js', { type: 'module' }),
53
63
  schema,
@@ -93,6 +103,13 @@ async function main(): Promise<void> {
93
103
  renderRealtimeStatus();
94
104
  await refresh();
95
105
 
106
+ storageBtn.addEventListener('click', async () => {
107
+ storageBtn.disabled = true;
108
+ storagePersistence = await requestBrowserStoragePersistence();
109
+ storageBtn.disabled = false;
110
+ await refresh();
111
+ });
112
+
96
113
  let offline = false;
97
114
  offlineBtn.addEventListener('click', async () => {
98
115
  offline = !offline;
@@ -142,6 +159,16 @@ async function main(): Promise<void> {
142
159
  )) as unknown as Todo[];
143
160
  const pending = (await handle.pendingCommits()).length;
144
161
  outboxEl.textContent = `outbox ${pending}`;
162
+ storageEl.textContent =
163
+ storagePersistence.state === 'persistent'
164
+ ? 'storage persistent'
165
+ : 'storage best effort';
166
+ storageBtn.hidden = storagePersistence.state === 'persistent';
167
+ storageWarningEl.hidden =
168
+ storagePersistence.state === 'persistent' || pending === 0;
169
+ storageWarningEl.textContent =
170
+ 'Unsynced changes can disappear if the browser evicts this site. ' +
171
+ 'Keep this tab open and reconnect, or protect offline data.';
145
172
  listEl.replaceChildren(...rows.map((row) => renderRow(row)));
146
173
 
147
174
  async function toggle(row: Todo): Promise<void> {