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 +3 -19
- package/dist/constants.d.ts +3 -5
- package/dist/constants.js +3 -5
- package/dist/scaffold.d.ts +2 -3
- package/dist/scaffold.js +2 -3
- package/package.json +22 -25
- package/src/constants.ts +3 -5
- package/src/scaffold.ts +2 -3
- package/template/tauri/src-tauri/Cargo.toml +1 -1
- package/template/web/README.md +5 -0
- package/template/web/src/frontend/index.html +4 -0
- package/template/web/src/frontend/main.ts +27 -0
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
|
|
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`)
|
|
44
|
-
|
|
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.
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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';
|
package/dist/scaffold.d.ts
CHANGED
|
@@ -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
|
|
20
|
-
*
|
|
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
|
|
41
|
-
*
|
|
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.
|
|
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
|
-
"
|
|
52
|
-
|
|
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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
59
|
-
*
|
|
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.
|
|
24
|
+
tauri-plugin-syncular = { version = "0.15.45", features = ["native-transport"] }
|
|
25
25
|
|
|
26
26
|
[features]
|
|
27
27
|
default = []
|
package/template/web/README.md
CHANGED
|
@@ -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> {
|