@pylonsync/create-pylon 0.3.50 → 0.3.53
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/bin/create-pylon.js +292 -1157
- package/package.json +4 -3
- package/templates/_root/.env.example +9 -0
- package/templates/_root/README.md +43 -0
- package/templates/backend/barebones/apps/api/functions/createWidget.ts +22 -0
- package/templates/backend/barebones/apps/api/functions/listWidgets.ts +18 -0
- package/templates/backend/barebones/apps/api/package.json +20 -0
- package/templates/backend/barebones/apps/api/schema.ts +61 -0
- package/templates/backend/barebones/apps/api/tsconfig.json +13 -0
- package/templates/backend/todo/apps/api/functions/addTodo.ts +27 -0
- package/templates/backend/todo/apps/api/functions/deleteTodo.ts +14 -0
- package/templates/backend/todo/apps/api/functions/editTodo.ts +16 -0
- package/templates/backend/todo/apps/api/functions/listTodos.ts +24 -0
- package/templates/backend/todo/apps/api/functions/reorderTodo.ts +14 -0
- package/templates/backend/todo/apps/api/functions/toggleTodo.ts +13 -0
- package/templates/backend/todo/apps/api/package.json +20 -0
- package/templates/backend/todo/apps/api/schema.ts +85 -0
- package/templates/backend/todo/apps/api/tsconfig.json +13 -0
- package/templates/expo/barebones/apps/expo/App.tsx +166 -0
- package/templates/expo/barebones/apps/expo/app.json +31 -0
- package/templates/expo/barebones/apps/expo/babel.config.js +6 -0
- package/templates/expo/barebones/apps/expo/package.json +30 -0
- package/templates/expo/barebones/apps/expo/tsconfig.json +16 -0
- package/templates/expo/todo/apps/expo/App.tsx +287 -0
- package/templates/expo/todo/apps/expo/app.json +25 -0
- package/templates/expo/todo/apps/expo/babel.config.js +6 -0
- package/templates/expo/todo/apps/expo/package.json +30 -0
- package/templates/expo/todo/apps/expo/tsconfig.json +16 -0
- package/templates/mobile/barebones/apps/mobile/Package.swift +34 -0
- package/templates/mobile/barebones/apps/mobile/Sources/__APP_NAME_PASCAL__/ContentView.swift +98 -0
- package/templates/mobile/barebones/apps/mobile/Sources/__APP_NAME_PASCAL__/Models.swift +17 -0
- package/templates/mobile/barebones/apps/mobile/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +34 -0
- package/templates/mobile/barebones/apps/mobile/project.yml +42 -0
- package/templates/mobile/todo/apps/mobile/Package.swift +23 -0
- package/templates/mobile/todo/apps/mobile/Sources/__APP_NAME_PASCAL__/Models.swift +18 -0
- package/templates/mobile/todo/apps/mobile/Sources/__APP_NAME_PASCAL__/TodoListView.swift +230 -0
- package/templates/mobile/todo/apps/mobile/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +28 -0
- package/templates/mobile/todo/apps/mobile/project.yml +32 -0
- package/templates/ui/packages/ui/package.json +26 -0
- package/templates/ui/packages/ui/src/button.tsx +44 -0
- package/templates/ui/packages/ui/src/card.tsx +39 -0
- package/templates/ui/packages/ui/src/cn.ts +12 -0
- package/templates/ui/packages/ui/src/index.ts +4 -0
- package/templates/ui/packages/ui/src/input.tsx +19 -0
- package/templates/ui/packages/ui/tsconfig.json +15 -0
- package/templates/web/barebones/apps/web/next-env.d.ts +2 -0
- package/templates/web/barebones/apps/web/next.config.ts +40 -0
- package/templates/web/barebones/apps/web/package.json +29 -0
- package/templates/web/barebones/apps/web/postcss.config.mjs +4 -0
- package/templates/web/barebones/apps/web/src/app/components/WidgetList.tsx +81 -0
- package/templates/web/barebones/apps/web/src/app/globals.css +9 -0
- package/templates/web/barebones/apps/web/src/app/layout.tsx +21 -0
- package/templates/web/barebones/apps/web/src/app/page.tsx +43 -0
- package/templates/web/barebones/apps/web/src/lib/pylon.ts +14 -0
- package/templates/web/barebones/apps/web/tsconfig.json +26 -0
- package/templates/web/todo/apps/web/next-env.d.ts +2 -0
- package/templates/web/todo/apps/web/next.config.ts +24 -0
- package/templates/web/todo/apps/web/package.json +32 -0
- package/templates/web/todo/apps/web/postcss.config.mjs +3 -0
- package/templates/web/todo/apps/web/src/app/components/TodoList.tsx +310 -0
- package/templates/web/todo/apps/web/src/app/globals.css +6 -0
- package/templates/web/todo/apps/web/src/app/layout.tsx +21 -0
- package/templates/web/todo/apps/web/src/app/page.tsx +36 -0
- package/templates/web/todo/apps/web/src/lib/pylon.ts +5 -0
- package/templates/web/todo/apps/web/tsconfig.json +26 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pylonsync/create-pylon",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "Scaffold a new Pylon app — realtime backend +
|
|
3
|
+
"version": "0.3.53",
|
|
4
|
+
"description": "Scaffold a new Pylon app — realtime backend + web/mobile/expo frontends in one command. Run via `npm create @pylonsync/pylon@latest`.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"create-pylon": "./bin/create-pylon.js"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
|
-
"bin"
|
|
13
|
+
"bin",
|
|
14
|
+
"templates"
|
|
14
15
|
],
|
|
15
16
|
"engines": {
|
|
16
17
|
"node": ">=18"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Backend port the Pylon control plane listens on.
|
|
2
|
+
PYLON_PORT=4321
|
|
3
|
+
|
|
4
|
+
# Where the Next.js dev server can reach the control plane.
|
|
5
|
+
PYLON_TARGET=http://localhost:4321
|
|
6
|
+
|
|
7
|
+
# Cookie name the auth helpers look for.
|
|
8
|
+
# Pattern: `${app_name}_session` from the Pylon manifest.
|
|
9
|
+
PYLON_COOKIE_NAME=__APP_NAME_SNAKE___session
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# __APP_NAME__
|
|
2
|
+
|
|
3
|
+
Scaffolded by [@pylonsync/create-pylon](https://npmjs.com/@pylonsync/create-pylon).
|
|
4
|
+
|
|
5
|
+
## Layout
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
apps/
|
|
9
|
+
api/ Pylon backend — schema, policies, function handlers
|
|
10
|
+
web/ Next.js frontend (if you picked --platforms web)
|
|
11
|
+
mobile/ Swift / SwiftUI app (if you picked --platforms mobile)
|
|
12
|
+
expo/ Expo + React Native app (if you picked --platforms expo)
|
|
13
|
+
|
|
14
|
+
packages/
|
|
15
|
+
ui/ Shared shadcn-style React primitives (web only)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Getting started
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
# Install
|
|
22
|
+
bun install # or pnpm install / yarn / npm install
|
|
23
|
+
|
|
24
|
+
# Run everything (the API + every frontend you picked)
|
|
25
|
+
bun run dev
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- **api** on http://localhost:4321 — Pylon control plane
|
|
29
|
+
- **web** on http://localhost:3000 — Next.js (if scaffolded)
|
|
30
|
+
- **expo** runs Metro on a separate port (if scaffolded)
|
|
31
|
+
- **mobile** lives in `apps/mobile/` — open in Xcode or run `swift run`
|
|
32
|
+
|
|
33
|
+
## What to do next
|
|
34
|
+
|
|
35
|
+
- Edit `apps/api/schema.ts` to add entities + policies.
|
|
36
|
+
- Drop handlers into `apps/api/functions/` — auto-discovered by name.
|
|
37
|
+
- For web: components in `apps/web/src/app/components/`.
|
|
38
|
+
- For mobile: SwiftUI views in `apps/mobile/Sources/__APP_NAME_PASCAL__/`.
|
|
39
|
+
- For expo: screens in `apps/expo/src/screens/`.
|
|
40
|
+
|
|
41
|
+
## Docs
|
|
42
|
+
|
|
43
|
+
[pylonsync.com/docs](https://pylonsync.com/docs)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Insert a new Widget. Initializes count to 0 and stamps the time.
|
|
5
|
+
* Returns the inserted row so the client can render it without a
|
|
6
|
+
* follow-up read.
|
|
7
|
+
*/
|
|
8
|
+
export default mutation({
|
|
9
|
+
args: { name: v.string() },
|
|
10
|
+
async handler(ctx, args: { name: string }) {
|
|
11
|
+
const trimmed = args.name.trim();
|
|
12
|
+
if (!trimmed) {
|
|
13
|
+
throw ctx.error("EMPTY_NAME", "name cannot be empty");
|
|
14
|
+
}
|
|
15
|
+
const id = await ctx.db.insert("Widget", {
|
|
16
|
+
name: trimmed,
|
|
17
|
+
count: 0,
|
|
18
|
+
createdAt: new Date().toISOString(),
|
|
19
|
+
});
|
|
20
|
+
return await ctx.db.get("Widget", id);
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { query } from "@pylonsync/functions";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Live query — every Widget, newest first. Subscribed via the React
|
|
5
|
+
* `useQuery` / Swift `PylonQuery` / RN `useQuery` hooks; rerenders
|
|
6
|
+
* whenever a row is inserted, updated, or deleted.
|
|
7
|
+
*/
|
|
8
|
+
export default query({
|
|
9
|
+
args: {},
|
|
10
|
+
async handler(ctx) {
|
|
11
|
+
const rows = await ctx.db.query("Widget", {});
|
|
12
|
+
return [...rows].sort((a: any, b: any) => {
|
|
13
|
+
const ad = Date.parse(a.createdAt) || 0;
|
|
14
|
+
const bd = Date.parse(b.createdAt) || 0;
|
|
15
|
+
return bd - ad;
|
|
16
|
+
});
|
|
17
|
+
},
|
|
18
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@__APP_NAME_KEBAB__/api",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "pylon dev schema.ts --port 4321",
|
|
8
|
+
"build": "pylon codegen schema.ts --out pylon.manifest.json && pylon codegen client pylon.manifest.json --out pylon.client.ts",
|
|
9
|
+
"schema:push": "pylon schema push pylon.manifest.json --sqlite dev.db",
|
|
10
|
+
"schema:inspect": "pylon schema inspect --sqlite dev.db"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@pylonsync/sdk": "^__PYLON_VERSION__",
|
|
14
|
+
"@pylonsync/functions": "^__PYLON_VERSION__"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@pylonsync/cli": "^__PYLON_VERSION__",
|
|
18
|
+
"typescript": "^5.5.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
entity,
|
|
3
|
+
field,
|
|
4
|
+
query,
|
|
5
|
+
action,
|
|
6
|
+
policy,
|
|
7
|
+
buildManifest,
|
|
8
|
+
} from "@pylonsync/sdk";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Schema — one entity, two operations. The smallest thing that
|
|
12
|
+
// demonstrates the loop: declare → handler → live query.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
const Widget = entity("Widget", {
|
|
16
|
+
name: field.string(),
|
|
17
|
+
count: field.int(),
|
|
18
|
+
createdAt: field.datetime(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Function declarations — names only. Implementations live under
|
|
23
|
+
// functions/<name>.ts and are auto-discovered by the runtime.
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
const listWidgets = query("listWidgets");
|
|
27
|
+
|
|
28
|
+
const createWidget = action("createWidget", {
|
|
29
|
+
input: [{ name: "name", type: "string" }],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Policies — wide-open by default. Tighten for production.
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
const widgetPolicy = policy({
|
|
37
|
+
name: "widget_open",
|
|
38
|
+
entity: "Widget",
|
|
39
|
+
allowRead: "true",
|
|
40
|
+
allowInsert: "true",
|
|
41
|
+
allowUpdate: "true",
|
|
42
|
+
allowDelete: "true",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Manifest — pylon codegen reads this and emits pylon.manifest.json.
|
|
47
|
+
// pylon dev / pylon codegen run `bun run schema.ts` and read the
|
|
48
|
+
// manifest off stdout — every entry file ends with this console.log.
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
const manifest = buildManifest({
|
|
52
|
+
name: "__APP_NAME_SNAKE__",
|
|
53
|
+
version: "0.0.1",
|
|
54
|
+
entities: [Widget],
|
|
55
|
+
queries: [listWidgets],
|
|
56
|
+
actions: [createWidget],
|
|
57
|
+
policies: [widgetPolicy],
|
|
58
|
+
routes: [],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
console.log(JSON.stringify(manifest));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"allowSyntheticDefaultImports": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["schema.ts", "functions/**/*.ts"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Insert a new Todo. Seeds `position` to (max + 1024) so new rows
|
|
5
|
+
* land at the end of the drag-reorder list; the 1024 step leaves
|
|
6
|
+
* room for inserts-between without needing global renumber.
|
|
7
|
+
*/
|
|
8
|
+
export default mutation({
|
|
9
|
+
args: { title: v.string() },
|
|
10
|
+
async handler(ctx, args: { title: string }) {
|
|
11
|
+
const existing = await ctx.db.query("Todo", {});
|
|
12
|
+
const maxPos = existing.reduce((acc: number, row: any) => {
|
|
13
|
+
const p =
|
|
14
|
+
typeof row.position === "number"
|
|
15
|
+
? row.position
|
|
16
|
+
: Date.parse(row.createdAt) || 0;
|
|
17
|
+
return p > acc ? p : acc;
|
|
18
|
+
}, 0);
|
|
19
|
+
const id = await ctx.db.insert("Todo", {
|
|
20
|
+
title: args.title,
|
|
21
|
+
done: false,
|
|
22
|
+
createdAt: new Date().toISOString(),
|
|
23
|
+
position: maxPos + 1024,
|
|
24
|
+
});
|
|
25
|
+
return await ctx.db.get("Todo", id);
|
|
26
|
+
},
|
|
27
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Remove a Todo row. Returns the row as it existed pre-delete so
|
|
5
|
+
* the client can show a "todo removed" toast or animate it out.
|
|
6
|
+
*/
|
|
7
|
+
export default mutation({
|
|
8
|
+
args: { id: v.id("Todo") },
|
|
9
|
+
async handler(ctx, args: { id: string }) {
|
|
10
|
+
const snapshot = await ctx.db.get("Todo", args.id);
|
|
11
|
+
await ctx.db.delete("Todo", args.id);
|
|
12
|
+
return snapshot;
|
|
13
|
+
},
|
|
14
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Rename a Todo. Trims whitespace; rejects empty titles.
|
|
5
|
+
*/
|
|
6
|
+
export default mutation({
|
|
7
|
+
args: { id: v.id("Todo"), title: v.string() },
|
|
8
|
+
async handler(ctx, args: { id: string; title: string }) {
|
|
9
|
+
const trimmed = args.title.trim();
|
|
10
|
+
if (!trimmed) {
|
|
11
|
+
throw ctx.error("EMPTY_TITLE", "title cannot be empty");
|
|
12
|
+
}
|
|
13
|
+
await ctx.db.update("Todo", args.id, { title: trimmed });
|
|
14
|
+
return await ctx.db.get("Todo", args.id);
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { query } from "@pylonsync/functions";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Live query — every Todo, in user-controlled drag-reorder position.
|
|
5
|
+
* Rows without a `position` (legacy data) get sorted by createdAt as
|
|
6
|
+
* a fallback so the list stays deterministic.
|
|
7
|
+
*/
|
|
8
|
+
export default query({
|
|
9
|
+
args: {},
|
|
10
|
+
async handler(ctx) {
|
|
11
|
+
const rows = await ctx.db.query("Todo", {});
|
|
12
|
+
return [...rows].sort((a: any, b: any) => {
|
|
13
|
+
const ap =
|
|
14
|
+
typeof a.position === "number"
|
|
15
|
+
? a.position
|
|
16
|
+
: Date.parse(a.createdAt) || 0;
|
|
17
|
+
const bp =
|
|
18
|
+
typeof b.position === "number"
|
|
19
|
+
? b.position
|
|
20
|
+
: Date.parse(b.createdAt) || 0;
|
|
21
|
+
return ap - bp;
|
|
22
|
+
});
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Drag-reorder. Frontend computes `position` as the midpoint of the
|
|
5
|
+
* drop target's neighbors; we just write it. Floats give us ~52 inserts
|
|
6
|
+
* between any two rows before precision matters.
|
|
7
|
+
*/
|
|
8
|
+
export default mutation({
|
|
9
|
+
args: { id: v.id("Todo"), position: v.number() },
|
|
10
|
+
async handler(ctx, args: { id: string; position: number }) {
|
|
11
|
+
await ctx.db.update("Todo", args.id, { position: args.position });
|
|
12
|
+
return await ctx.db.get("Todo", args.id);
|
|
13
|
+
},
|
|
14
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { mutation, v } from "@pylonsync/functions";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Flip the `done` flag on a Todo. Mutation, not action — needs
|
|
5
|
+
* `ctx.db.update` which is only on writable ctx variants.
|
|
6
|
+
*/
|
|
7
|
+
export default mutation({
|
|
8
|
+
args: { id: v.id("Todo"), done: v.bool() },
|
|
9
|
+
async handler(ctx, args: { id: string; done: boolean }) {
|
|
10
|
+
await ctx.db.update("Todo", args.id, { done: args.done });
|
|
11
|
+
return await ctx.db.get("Todo", args.id);
|
|
12
|
+
},
|
|
13
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@__APP_NAME_KEBAB__/api",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "pylon dev schema.ts --port 4321",
|
|
8
|
+
"build": "pylon codegen schema.ts --out pylon.manifest.json && pylon codegen client pylon.manifest.json --out pylon.client.ts",
|
|
9
|
+
"schema:push": "pylon schema push pylon.manifest.json --sqlite dev.db",
|
|
10
|
+
"schema:inspect": "pylon schema inspect --sqlite dev.db"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@pylonsync/sdk": "^__PYLON_VERSION__",
|
|
14
|
+
"@pylonsync/functions": "^__PYLON_VERSION__"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@pylonsync/cli": "^__PYLON_VERSION__",
|
|
18
|
+
"typescript": "^5.5.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
entity,
|
|
3
|
+
field,
|
|
4
|
+
query,
|
|
5
|
+
action,
|
|
6
|
+
policy,
|
|
7
|
+
buildManifest,
|
|
8
|
+
} from "@pylonsync/sdk";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Schema
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
const Todo = entity("Todo", {
|
|
15
|
+
title: field.string(),
|
|
16
|
+
done: field.bool(),
|
|
17
|
+
createdAt: field.datetime(),
|
|
18
|
+
// Float position so drag-reorder can insert between two existing
|
|
19
|
+
// rows without renumbering the whole list. Frontend computes
|
|
20
|
+
// (prev.position + next.position) / 2 on drop. Optional for
|
|
21
|
+
// backwards compat with legacy rows.
|
|
22
|
+
position: field.float().optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Function declarations — names only. Implementations live under
|
|
27
|
+
// functions/<name>.ts and are auto-discovered by the runtime.
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
const listTodos = query("listTodos");
|
|
31
|
+
|
|
32
|
+
const addTodo = action("addTodo", {
|
|
33
|
+
input: [{ name: "title", type: "string" }],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const toggleTodo = action("toggleTodo", {
|
|
37
|
+
input: [{ name: "id", type: "id(Todo)" }, { name: "done", type: "bool" }],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const deleteTodo = action("deleteTodo", {
|
|
41
|
+
input: [{ name: "id", type: "id(Todo)" }],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const editTodo = action("editTodo", {
|
|
45
|
+
input: [
|
|
46
|
+
{ name: "id", type: "id(Todo)" },
|
|
47
|
+
{ name: "title", type: "string" },
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const reorderTodo = action("reorderTodo", {
|
|
52
|
+
input: [
|
|
53
|
+
{ name: "id", type: "id(Todo)" },
|
|
54
|
+
{ name: "position", type: "float" },
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Policies — wide-open by default. Tighten for production.
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
const todoPolicy = policy({
|
|
63
|
+
name: "todo_open",
|
|
64
|
+
entity: "Todo",
|
|
65
|
+
allowRead: "true",
|
|
66
|
+
allowInsert: "true",
|
|
67
|
+
allowUpdate: "true",
|
|
68
|
+
allowDelete: "true",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Manifest
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
const manifest = buildManifest({
|
|
76
|
+
name: "__APP_NAME_SNAKE__",
|
|
77
|
+
version: "0.0.1",
|
|
78
|
+
entities: [Todo],
|
|
79
|
+
queries: [listTodos],
|
|
80
|
+
actions: [addTodo, toggleTodo, deleteTodo, editTodo, reorderTodo],
|
|
81
|
+
policies: [todoPolicy],
|
|
82
|
+
routes: [],
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
console.log(JSON.stringify(manifest));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"allowSyntheticDefaultImports": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["schema.ts", "functions/**/*.ts"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
TextInput,
|
|
6
|
+
Pressable,
|
|
7
|
+
FlatList,
|
|
8
|
+
ActivityIndicator,
|
|
9
|
+
StyleSheet,
|
|
10
|
+
Platform,
|
|
11
|
+
} from "react-native";
|
|
12
|
+
import { StatusBar } from "expo-status-bar";
|
|
13
|
+
import { init, db, callFn } from "@pylonsync/react-native";
|
|
14
|
+
|
|
15
|
+
// In dev, point at the Pylon control plane on your machine. The iOS
|
|
16
|
+
// simulator can reach `localhost`; an Android emulator uses `10.0.2.2`;
|
|
17
|
+
// a physical device needs your LAN IP. Override via `PYLON_BASE_URL`
|
|
18
|
+
// in app.json `extra` or via an `.env`.
|
|
19
|
+
const PYLON_BASE_URL =
|
|
20
|
+
process.env.EXPO_PUBLIC_PYLON_BASE_URL ??
|
|
21
|
+
(Platform.OS === "android" ? "http://10.0.2.2:4321" : "http://localhost:4321");
|
|
22
|
+
|
|
23
|
+
type Widget = {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
count: number;
|
|
27
|
+
createdAt: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
let initPromise: Promise<void> | null = null;
|
|
31
|
+
function ensureInit() {
|
|
32
|
+
if (!initPromise) {
|
|
33
|
+
initPromise = init({
|
|
34
|
+
baseUrl: PYLON_BASE_URL,
|
|
35
|
+
appName: "__APP_NAME_SNAKE__",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return initPromise;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default function App() {
|
|
42
|
+
const [ready, setReady] = useState(false);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
ensureInit().then(() => setReady(true));
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
if (!ready) {
|
|
48
|
+
return (
|
|
49
|
+
<View style={[styles.screen, styles.center]}>
|
|
50
|
+
<ActivityIndicator />
|
|
51
|
+
</View>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return <Home />;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function Home() {
|
|
58
|
+
const { data: widgets, loading } = db.useQuery<Widget>("Widget", {});
|
|
59
|
+
const [name, setName] = useState("");
|
|
60
|
+
const [creating, setCreating] = useState(false);
|
|
61
|
+
|
|
62
|
+
async function create() {
|
|
63
|
+
const trimmed = name.trim();
|
|
64
|
+
if (!trimmed) return;
|
|
65
|
+
setCreating(true);
|
|
66
|
+
try {
|
|
67
|
+
await callFn("createWidget", { name: trimmed });
|
|
68
|
+
setName("");
|
|
69
|
+
} finally {
|
|
70
|
+
setCreating(false);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<View style={styles.screen}>
|
|
76
|
+
<StatusBar style="auto" />
|
|
77
|
+
<Text style={styles.title}>__APP_NAME__</Text>
|
|
78
|
+
<Text style={styles.subtitle}>
|
|
79
|
+
Pylon backend → Expo + React Native, live-updating list.
|
|
80
|
+
</Text>
|
|
81
|
+
|
|
82
|
+
<View style={styles.row}>
|
|
83
|
+
<TextInput
|
|
84
|
+
style={styles.input}
|
|
85
|
+
placeholder="Name a widget…"
|
|
86
|
+
value={name}
|
|
87
|
+
onChangeText={setName}
|
|
88
|
+
editable={!creating}
|
|
89
|
+
onSubmitEditing={create}
|
|
90
|
+
autoCorrect={false}
|
|
91
|
+
/>
|
|
92
|
+
<Pressable
|
|
93
|
+
onPress={create}
|
|
94
|
+
disabled={creating || !name.trim()}
|
|
95
|
+
style={({ pressed }) => [
|
|
96
|
+
styles.button,
|
|
97
|
+
(creating || !name.trim()) && styles.buttonDisabled,
|
|
98
|
+
pressed && styles.buttonPressed,
|
|
99
|
+
]}
|
|
100
|
+
>
|
|
101
|
+
<Text style={styles.buttonLabel}>Create</Text>
|
|
102
|
+
</Pressable>
|
|
103
|
+
</View>
|
|
104
|
+
|
|
105
|
+
{loading ? (
|
|
106
|
+
<ActivityIndicator style={{ marginTop: 24 }} />
|
|
107
|
+
) : (widgets ?? []).length === 0 ? (
|
|
108
|
+
<Text style={styles.empty}>No widgets yet. Create one above.</Text>
|
|
109
|
+
) : (
|
|
110
|
+
<FlatList
|
|
111
|
+
data={widgets ?? []}
|
|
112
|
+
keyExtractor={(w) => w.id}
|
|
113
|
+
contentContainerStyle={{ paddingTop: 16 }}
|
|
114
|
+
ItemSeparatorComponent={() => <View style={styles.separator} />}
|
|
115
|
+
renderItem={({ item }) => (
|
|
116
|
+
<View style={styles.item}>
|
|
117
|
+
<Text style={styles.itemName}>{item.name}</Text>
|
|
118
|
+
<Text style={styles.itemCount}>count: {item.count}</Text>
|
|
119
|
+
</View>
|
|
120
|
+
)}
|
|
121
|
+
/>
|
|
122
|
+
)}
|
|
123
|
+
</View>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const styles = StyleSheet.create({
|
|
128
|
+
screen: {
|
|
129
|
+
flex: 1,
|
|
130
|
+
paddingTop: 64,
|
|
131
|
+
paddingHorizontal: 20,
|
|
132
|
+
backgroundColor: "#fff",
|
|
133
|
+
},
|
|
134
|
+
center: { alignItems: "center", justifyContent: "center" },
|
|
135
|
+
title: { fontSize: 28, fontWeight: "600" },
|
|
136
|
+
subtitle: { color: "#666", marginTop: 4, marginBottom: 20 },
|
|
137
|
+
row: { flexDirection: "row", gap: 8 },
|
|
138
|
+
input: {
|
|
139
|
+
flex: 1,
|
|
140
|
+
borderWidth: 1,
|
|
141
|
+
borderColor: "#d4d4d8",
|
|
142
|
+
borderRadius: 6,
|
|
143
|
+
paddingHorizontal: 12,
|
|
144
|
+
paddingVertical: 8,
|
|
145
|
+
fontSize: 14,
|
|
146
|
+
},
|
|
147
|
+
button: {
|
|
148
|
+
backgroundColor: "#171717",
|
|
149
|
+
borderRadius: 6,
|
|
150
|
+
paddingHorizontal: 16,
|
|
151
|
+
justifyContent: "center",
|
|
152
|
+
},
|
|
153
|
+
buttonDisabled: { opacity: 0.5 },
|
|
154
|
+
buttonPressed: { opacity: 0.8 },
|
|
155
|
+
buttonLabel: { color: "#fff", fontWeight: "600" },
|
|
156
|
+
empty: { textAlign: "center", color: "#999", marginTop: 32 },
|
|
157
|
+
item: {
|
|
158
|
+
flexDirection: "row",
|
|
159
|
+
alignItems: "center",
|
|
160
|
+
justifyContent: "space-between",
|
|
161
|
+
paddingVertical: 12,
|
|
162
|
+
},
|
|
163
|
+
itemName: { fontSize: 15, fontWeight: "500" },
|
|
164
|
+
itemCount: { fontSize: 12, fontFamily: "Menlo", color: "#999" },
|
|
165
|
+
separator: { height: 1, backgroundColor: "#e5e5e5" },
|
|
166
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"expo": {
|
|
3
|
+
"name": "__APP_NAME__",
|
|
4
|
+
"slug": "__APP_NAME_KEBAB__",
|
|
5
|
+
"version": "0.0.1",
|
|
6
|
+
"orientation": "portrait",
|
|
7
|
+
"icon": "./assets/icon.png",
|
|
8
|
+
"userInterfaceStyle": "automatic",
|
|
9
|
+
"splash": {
|
|
10
|
+
"image": "./assets/splash.png",
|
|
11
|
+
"resizeMode": "contain",
|
|
12
|
+
"backgroundColor": "#ffffff"
|
|
13
|
+
},
|
|
14
|
+
"ios": {
|
|
15
|
+
"supportsTablet": true,
|
|
16
|
+
"bundleIdentifier": "com.example.__APP_NAME_SNAKE__",
|
|
17
|
+
"infoPlist": {
|
|
18
|
+
"NSAppTransportSecurity": {
|
|
19
|
+
"NSAllowsLocalNetworking": true
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"android": {
|
|
24
|
+
"package": "com.example.__APP_NAME_SNAKE__",
|
|
25
|
+
"usesCleartextTraffic": true
|
|
26
|
+
},
|
|
27
|
+
"web": {
|
|
28
|
+
"bundler": "metro"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|