@fragno-dev/create 0.0.4 → 0.1.1

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fragno-dev/create",
3
3
  "description": "A library for creating Fragno fragments",
4
- "version": "0.0.4",
4
+ "version": "0.1.1",
5
5
  "exports": {
6
6
  ".": {
7
7
  "bun": "./src/index.ts",
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { copy, merge } from "./utils.ts";
5
- import { buildToolPkg } from "./package-json.ts";
5
+ import { basePkg, buildToolPkg, databasePkg } from "./package-json.ts";
6
6
  import { z } from "zod";
7
7
 
8
8
  const templateTypesSchema = z.literal("fragment");
@@ -28,16 +28,22 @@ export const createOptionsSchema = z.object({
28
28
  name: z.string(),
29
29
  template: templateTypesSchema,
30
30
  agentDocs: agentDocsSchema,
31
+ withDatabase: z.boolean(),
31
32
  });
32
33
 
33
34
  type CreateOptions = z.infer<typeof createOptionsSchema>;
34
35
 
35
36
  export function create(options: CreateOptions) {
36
- let pkgOverride: Record<string, unknown> = { name: options.name };
37
+ let pkgOverride: Record<string, unknown> = merge(basePkg, { name: options.name });
37
38
 
38
39
  // Build tool pkg overrides
39
40
  pkgOverride = merge(pkgOverride, buildToolPkg[options.buildTool]);
40
41
 
42
+ // Database pkg overrides
43
+ if (options.withDatabase) {
44
+ pkgOverride = merge(pkgOverride, databasePkg);
45
+ }
46
+
41
47
  if (options.template == "fragment") {
42
48
  writeFragmentTemplate(options.path, pkgOverride);
43
49
  } else {
@@ -77,6 +83,11 @@ export function create(options: CreateOptions) {
77
83
  case "none":
78
84
  break;
79
85
  }
86
+
87
+ if (options.withDatabase) {
88
+ writeOptionalTemplate(options.path, "database/index.ts", "src/index.ts");
89
+ writeOptionalTemplate(options.path, "database/schema.ts", "src/schema.ts");
90
+ }
80
91
  }
81
92
 
82
93
  function getTemplateDir(): string {
@@ -19,9 +19,10 @@ async function createTempDir(name: string): Promise<string> {
19
19
  return dir;
20
20
  }
21
21
 
22
- describe.concurrent.each(["tsdown", "esbuild", "vite", "rollup", "webpack", "rspack"] as const)(
23
- "fragment with %s",
24
- (buildTool) => {
22
+ type BuildTool = "tsdown" | "esbuild" | "vite" | "rollup" | "webpack" | "rspack";
23
+
24
+ function createFragmentTestSuite(buildTool: BuildTool, withDatabase: boolean) {
25
+ return () => {
25
26
  let tempDir: string;
26
27
  const testConfig = {
27
28
  name: "@myorg/test",
@@ -31,16 +32,17 @@ describe.concurrent.each(["tsdown", "esbuild", "vite", "rollup", "webpack", "rsp
31
32
  };
32
33
 
33
34
  beforeAll(async () => {
34
- tempDir = await createTempDir(`fragment-test-${buildTool}`);
35
+ const suffix = withDatabase ? "db" : "no-db";
36
+ tempDir = await createTempDir(`fragment-test-${buildTool}-${suffix}`);
35
37
  console.log("temp", tempDir);
36
- create({ ...testConfig, path: tempDir });
38
+ create({ ...testConfig, path: tempDir, withDatabase });
37
39
  });
38
40
 
39
41
  afterAll(async () => {
40
42
  await fs.rm(tempDir, { recursive: true });
41
43
  });
42
44
 
43
- describe.sequential("", () => {
45
+ describe.sequential(buildTool, () => {
44
46
  test("package.json correctly templated", async () => {
45
47
  const pkg = path.join(tempDir, "package.json");
46
48
  const pkgContent = await fs.readFile(pkg, "utf8");
@@ -74,7 +76,7 @@ describe.concurrent.each(["tsdown", "esbuild", "vite", "rollup", "webpack", "rsp
74
76
  but somehow when running through vitest the module resolution mechanism changes causing
75
77
  the build to fail.
76
78
  */
77
- test.skipIf(buildTool == "rollup")("builds", { timeout: 40000 }, async () => {
79
+ test.skipIf(buildTool === "rollup")("builds", { timeout: 50000 }, async () => {
78
80
  const result = await execAsync("bun run build", {
79
81
  cwd: tempDir,
80
82
  encoding: "utf8",
@@ -99,5 +101,12 @@ describe.concurrent.each(["tsdown", "esbuild", "vite", "rollup", "webpack", "rsp
99
101
  }
100
102
  });
101
103
  });
102
- },
104
+ };
105
+ }
106
+
107
+ describe.concurrent.each(["tsdown", "esbuild", "vite", "rollup", "webpack", "rspack"] as const)(
108
+ "fragment with %s (with database)",
109
+ (buildTool) => createFragmentTestSuite(buildTool, true)(),
103
110
  );
111
+
112
+ describe("fragment with tsdown (without database)", createFragmentTestSuite("tsdown", false));
@@ -1,6 +1,33 @@
1
1
  import type { BuildTools } from "./index";
2
2
 
3
- const unpluginFragnoVersion = "^0.0.1";
3
+ const fragnoCoreVersion = "^0.1.2";
4
+ const fragnoDbVersion = "^0.1.2";
5
+ const unpluginFragnoVersion = "^0.0.2";
6
+ const fragnoCliVersion = "^0.1.3";
7
+
8
+ export const basePkg: Record<string, unknown> = {
9
+ dependencies: {
10
+ "@fragno-dev/core": fragnoCoreVersion,
11
+ zod: "^4.0.5",
12
+ },
13
+ devDependencies: {
14
+ "@fragno-dev/cli": fragnoCliVersion,
15
+ "@types/node": "^22",
16
+ },
17
+ peerDependencies: {
18
+ typescript: "^5",
19
+ react: ">=18.0.0",
20
+ svelte: ">=4.0.0",
21
+ "solid-js": ">=1.0.0",
22
+ vue: ">=3.0.0",
23
+ },
24
+ };
25
+
26
+ export const databasePkg: Record<string, unknown> = {
27
+ dependencies: {
28
+ "@fragno-dev/db": fragnoDbVersion,
29
+ },
30
+ };
4
31
 
5
32
  export const buildToolPkg: Record<BuildTools, Record<string, unknown>> = {
6
33
  none: {},
package/src/utils.ts CHANGED
@@ -5,7 +5,9 @@ export function mkdirp(dir: string): void {
5
5
  try {
6
6
  fs.mkdirSync(dir, { recursive: true });
7
7
  } catch (e: unknown) {
8
- if (e instanceof Error && "code" in e && e.code === "EEXIST") return;
8
+ if (e instanceof Error && "code" in e && e.code === "EEXIST") {
9
+ return;
10
+ }
9
11
  throw e;
10
12
  }
11
13
  }
@@ -54,7 +56,9 @@ export function copy(
54
56
  to: string,
55
57
  rename: (basename: string) => string = identity,
56
58
  ): void {
57
- if (!fs.existsSync(from)) return;
59
+ if (!fs.existsSync(from)) {
60
+ return;
61
+ }
58
62
 
59
63
  const stats = fs.statSync(from);
60
64
 
@@ -56,19 +56,5 @@
56
56
  "types:check": "tsc --noEmit"
57
57
  },
58
58
  "type": "module",
59
- "dependencies": {
60
- "@fragno-dev/core": "^0.0.7",
61
- "zod": "^4.0.5"
62
- },
63
- "devDependencies": {
64
- "@types/node": "^20"
65
- },
66
- "private": true,
67
- "peerDependencies": {
68
- "typescript": "^5",
69
- "react": ">=18.0.0",
70
- "svelte": ">=4.0.0",
71
- "solid-js": ">=1.0.0",
72
- "vue": ">=3.0.0"
73
- }
59
+ "private": true
74
60
  }
@@ -56,12 +56,12 @@ const exampleRoutesFactory = defineRoutes<ExampleConfig, ExampleDeps, ExampleSer
56
56
  );
57
57
 
58
58
  const exampleFragmentDefinition = defineFragment<ExampleConfig>("example-fragment")
59
- .withDependencies((config: ExampleConfig) => {
59
+ .withDependencies(({ config }) => {
60
60
  return {
61
61
  serverSideData: { value: config.initialData ?? "Hello World! This is a server-side data." },
62
62
  };
63
63
  })
64
- .withServices((_cfg, deps) => {
64
+ .withServices(({ deps }) => {
65
65
  return {
66
66
  getData: () => deps.serverSideData.value,
67
67
  };
@@ -14,7 +14,9 @@ provide:
14
14
  - Built-in state management with reactive stores (TanStack Query-style:
15
15
  `const {data, loading, error} = useData()`)
16
16
 
17
- **Documentation**: Full documentation is available at https://fragno.dev/docs
17
+ **Documentation**: https://fragno.dev/docs
18
+
19
+ All docs are also available with a `.md` extension: https://fragno.dev/docs.md
18
20
 
19
21
  ## Architecture
20
22
 
@@ -24,6 +26,22 @@ Fragments follow a core pattern:
24
26
  2. **Client-side**: Auto-generated type-safe hooks for each route
25
27
  3. **Code splitting**: Server-only code (handlers, dependencies) is stripped from client bundles
26
28
 
29
+ ### Fragment Configuration
30
+
31
+ Database-enabled Fragments require `FragnoPublicConfigWithDatabase`:
32
+
33
+ ```typescript
34
+ export function createMyFragment(
35
+ config: MyFragmentConfig = {},
36
+ options: FragnoPublicConfigWithDatabase, // Enforces databaseAdapter requirement
37
+ ) {
38
+ return createFragment(fragmentDef, config, [], options);
39
+ }
40
+ ```
41
+
42
+ For complete database documentation, see:
43
+ https://fragno.dev/docs/for-library-authors/database-integration/overview.md
44
+
27
45
  ## File Structure & Core Concepts
28
46
 
29
47
  ### `src/index.ts` - Main Fragment Definition
@@ -94,6 +112,107 @@ points:
94
112
  - Production uses built files from `dist/`
95
113
  - When adding new framework exports, add corresponding client files in `src/client/`
96
114
 
115
+ ## Database Integration (Optional)
116
+
117
+ Some Fragments require persistent storage. Fragno provides an optional database layer via
118
+ `@fragno-dev/db` that integrates with your users' existing databases.
119
+
120
+ ### When to Use Database Integration
121
+
122
+ Use `defineFragmentWithDatabase()` when your Fragment needs to:
123
+
124
+ - Store persistent data (comments, likes, user preferences, etc.)
125
+ - Query structured data efficiently
126
+ - Maintain data integrity with indexes and constraints
127
+ - Provide users with full control over their data
128
+
129
+ ### Schema Definition
130
+
131
+ Database schemas are defined in a separate `schema.ts` file using the Fragno schema builder:
132
+
133
+ ```typescript
134
+ import { column, idColumn, schema } from "@fragno-dev/db/schema";
135
+
136
+ export const noteSchema = schema((s) => {
137
+ return s.addTable("note", (t) => {
138
+ return t
139
+ .addColumn("id", idColumn()) // Auto-generated ID
140
+ .addColumn("content", column("string"))
141
+ .addColumn("userId", column("string"))
142
+ .addColumn("createdAt", column("timestamp").defaultTo$("now"))
143
+ .createIndex("idx_note_user", ["userId"]); // Index for efficient queries
144
+ });
145
+ });
146
+ ```
147
+
148
+ **Key concepts**:
149
+
150
+ - **Append-only**: Schemas use an append-only log approach. Never modify existing operations -
151
+ always add new ones
152
+ - **Versioning**: Each schema operation increments the version number
153
+ - **Indexes**: Create indexes on columns you'll frequently query (e.g., foreign keys, user IDs)
154
+ - **Defaults**: `.defaultTo(value)` or `.defaultTo((b) => b.now())` for DB defaults;
155
+ `.defaultTo$((b) => b.cuid())` for runtime defaults
156
+
157
+ ### Using the ORM
158
+
159
+ The ORM is available in both `withDependencies()` and `withServices()` via the `orm` parameter:
160
+
161
+ ```typescript
162
+ const fragmentDef = defineFragmentWithDatabase<Config>("my-fragment")
163
+ .withDatabase(mySchema)
164
+ .withServices(({ orm }) => {
165
+ return {
166
+ createNote: async (note) => {
167
+ const id = await orm.create("note", note);
168
+ return { id: id.toJSON(), ...note };
169
+ },
170
+ getNotesByUser: (userId: string) => {
171
+ // Use whereIndex for efficient indexed queries
172
+ return orm.find("note", (b) =>
173
+ b.whereIndex("idx_note_user", (eb) => eb("userId", "=", userId)),
174
+ );
175
+ },
176
+ };
177
+ });
178
+ ```
179
+
180
+ **ORM methods**:
181
+
182
+ - `orm.create(table, data)` - Insert a row, returns FragnoId
183
+ - `orm.find(table, builder)` - Query rows with filtering
184
+ - `orm.findOne(table, builder)` - Query a single row
185
+ - `orm.update(table, id, data)` - Update a row by ID
186
+ - `orm.delete(table, id)` - Delete a row by ID
187
+ - `.whereIndex(indexName, condition)` - Use indexes for efficient queries
188
+
189
+ ### Transactions (Unit of Work)
190
+
191
+ Two-phase pattern for atomic operations (optimistic concurrency control):
192
+
193
+ ```typescript
194
+ // Phase 1: Retrieve with version tracking
195
+ const uow = orm
196
+ .createUnitOfWork()
197
+ .find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", userId)))
198
+ .find("accounts", (b) => b.whereIndex("idx_user", (eb) => eb("userId", "=", userId)));
199
+ const [users, accounts] = await uow.executeRetrieve();
200
+
201
+ // Phase 2: Mutate atomically
202
+ uow.update("users", users[0].id, (b) => b.set({ lastLogin: new Date() }).check());
203
+ uow.update("accounts", accounts[0].id, (b) =>
204
+ b.set({ balance: accounts[0].balance + 100 }).check(),
205
+ );
206
+
207
+ const { success } = await uow.executeMutations();
208
+ if (!success) {
209
+ /* Version conflict - retry */
210
+ }
211
+ ```
212
+
213
+ **Notes**: `.check()` enables optimistic concurrency control; requires `FragnoId` objects (not
214
+ string IDs); use `uow.getCreatedIds()` for new record IDs
215
+
97
216
  ## Strategies for Building Fragments
98
217
 
99
218
  ### OpenAPI/Swagger Spec → Fragno Routes
@@ -0,0 +1,141 @@
1
+ import {
2
+ defineRoute,
3
+ defineRoutes,
4
+ createFragment,
5
+ type FragnoPublicClientConfig,
6
+ } from "@fragno-dev/core";
7
+ import { createClientBuilder } from "@fragno-dev/core/client";
8
+ import {
9
+ defineFragmentWithDatabase,
10
+ type FragnoPublicConfigWithDatabase,
11
+ } from "@fragno-dev/db/fragment";
12
+ import type { AbstractQuery, TableToInsertValues } from "@fragno-dev/db/query";
13
+ import { noteSchema } from "./schema";
14
+
15
+ // NOTE: We use zod here for defining schemas, but any StandardSchema library can be used!
16
+ // For a complete list see:
17
+ // https://github.com/standard-schema/standard-schema#what-schema-libraries-implement-the-spec
18
+ import { z } from "zod";
19
+
20
+ export interface ExampleConfig {
21
+ // Add any server-side configuration here if needed
22
+ }
23
+
24
+ type ExampleServices = {
25
+ createNote: (note: TableToInsertValues<typeof noteSchema.tables.note>) => Promise<{
26
+ id: string;
27
+ content: string;
28
+ userId: string;
29
+ createdAt: Date;
30
+ }>;
31
+ getNotes: () => Promise<
32
+ Array<{
33
+ id: string;
34
+ content: string;
35
+ userId: string;
36
+ createdAt: Date;
37
+ }>
38
+ >;
39
+ getNotesByUser: (userId: string) => Promise<
40
+ Array<{
41
+ id: string;
42
+ content: string;
43
+ userId: string;
44
+ createdAt: Date;
45
+ }>
46
+ >;
47
+ };
48
+
49
+ type ExampleDeps = {
50
+ orm: AbstractQuery<typeof noteSchema>;
51
+ };
52
+
53
+ const exampleRoutesFactory = defineRoutes<ExampleConfig, ExampleDeps, ExampleServices>().create(
54
+ ({ services }) => {
55
+ return [
56
+ defineRoute({
57
+ method: "GET",
58
+ path: "/notes",
59
+ queryParameters: ["userId"],
60
+ outputSchema: z.array(
61
+ z.object({
62
+ id: z.string(),
63
+ content: z.string(),
64
+ userId: z.string(),
65
+ createdAt: z.date(),
66
+ }),
67
+ ),
68
+ handler: async ({ query }, { json }) => {
69
+ const userId = query.get("userId");
70
+
71
+ if (userId) {
72
+ const notes = await services.getNotesByUser(userId);
73
+ return json(notes);
74
+ }
75
+
76
+ const notes = await services.getNotes();
77
+ return json(notes);
78
+ },
79
+ }),
80
+
81
+ defineRoute({
82
+ method: "POST",
83
+ path: "/notes",
84
+ inputSchema: z.object({ content: z.string(), userId: z.string() }),
85
+ outputSchema: z.object({
86
+ id: z.string(),
87
+ content: z.string(),
88
+ userId: z.string(),
89
+ createdAt: z.date(),
90
+ }),
91
+ errorCodes: [],
92
+ handler: async ({ input }, { json }) => {
93
+ const { content, userId } = await input.valid();
94
+
95
+ const note = await services.createNote({ content, userId });
96
+ return json(note);
97
+ },
98
+ }),
99
+ ];
100
+ },
101
+ );
102
+
103
+ const exampleFragmentDefinition = defineFragmentWithDatabase<ExampleConfig>("example-fragment")
104
+ .withDatabase(noteSchema)
105
+ .withServices(({ orm }) => {
106
+ return {
107
+ createNote: async (note: TableToInsertValues<typeof noteSchema.tables.note>) => {
108
+ const id = await orm.create("note", note);
109
+ return {
110
+ ...note,
111
+ id: id.toJSON(),
112
+ createdAt: note.createdAt ?? new Date(),
113
+ };
114
+ },
115
+ getNotes: () => {
116
+ return orm.find("note", (b) => b);
117
+ },
118
+ getNotesByUser: (userId: string) => {
119
+ return orm.find("note", (b) =>
120
+ b.whereIndex("idx_note_user", (eb) => eb("userId", "=", userId)),
121
+ );
122
+ },
123
+ };
124
+ });
125
+
126
+ export function createExampleFragment(
127
+ config: ExampleConfig = {},
128
+ fragnoConfig: FragnoPublicConfigWithDatabase,
129
+ ) {
130
+ return createFragment(exampleFragmentDefinition, config, [exampleRoutesFactory], fragnoConfig);
131
+ }
132
+
133
+ export function createExampleFragmentClients(fragnoConfig: FragnoPublicClientConfig) {
134
+ const b = createClientBuilder(exampleFragmentDefinition, fragnoConfig, [exampleRoutesFactory]);
135
+
136
+ return {
137
+ useNotes: b.createHook("/notes"),
138
+ useCreateNote: b.createMutator("POST", "/notes"),
139
+ };
140
+ }
141
+ export type { FragnoRouteConfig } from "@fragno-dev/core/api";
@@ -0,0 +1,15 @@
1
+ import { column, idColumn, schema } from "@fragno-dev/db/schema";
2
+
3
+ export const noteSchema = schema((s) => {
4
+ return s.addTable("note", (t) => {
5
+ return t
6
+ .addColumn("id", idColumn())
7
+ .addColumn("content", column("string"))
8
+ .addColumn("userId", column("string"))
9
+ .addColumn(
10
+ "createdAt",
11
+ column("timestamp").defaultTo((b) => b.now()),
12
+ )
13
+ .createIndex("idx_note_user", ["userId"]);
14
+ });
15
+ });
@@ -1,66 +0,0 @@
1
- $ vitest run
2
-
3
-  RUN  v3.2.4 /home/runner/work/fragno/fragno/packages/create
4
- Coverage enabled with istanbul
5
-
6
- ✓ src/utils.test.ts (1 test) 4ms
7
- stdout | src/integration.test.ts > fragment with rspack
8
- temp /tmp/fragment-test-tsdown-1760972394592
9
-
10
- stdout | src/integration.test.ts > fragment with tsdown > package.json correctly templated
11
- temp /tmp/fragment-test-esbuild-1760972394596
12
-
13
- stdout | src/integration.test.ts > fragment with esbuild > package.json correctly templated
14
- temp /tmp/fragment-test-vite-1760972394596
15
-
16
- stdout | src/integration.test.ts > fragment with vite > package.json correctly templated
17
- temp /tmp/fragment-test-rollup-1760972394596
18
-
19
- stdout | src/integration.test.ts > fragment with rollup > package.json correctly templated
20
- temp /tmp/fragment-test-webpack-1760972394597
21
-
22
- stdout | src/integration.test.ts > fragment with webpack > package.json correctly templated
23
- temp /tmp/fragment-test-rspack-1760972394597
24
-
25
- stdout | src/integration.test.ts > fragment with webpack > compiles
26
- 
27
-
28
- stdout | src/integration.test.ts > fragment with rspack > compiles
29
- 
30
-
31
- stdout | src/integration.test.ts > fragment with rollup
32
- 
33
-
34
- stdout | src/integration.test.ts
35
- 
36
-
37
- stdout | src/integration.test.ts
38
- 
39
-
40
- stdout | src/integration.test.ts > fragment with webpack > builds
41
- 
42
-
43
- ✓ src/integration.test.ts (30 tests | 1 skipped) 24967ms
44
- ✓ fragment with tsdown > installs  2596ms
45
- ✓ fragment with tsdown > compiles  7152ms
46
- ✓ fragment with tsdown > builds  9413ms
47
- ✓ fragment with esbuild > installs  2515ms
48
- ✓ fragment with esbuild > compiles  6488ms
49
- ✓ fragment with esbuild > builds  1739ms
50
- ✓ fragment with vite > installs  2517ms
51
- ✓ fragment with vite > compiles  6562ms
52
- ✓ fragment with vite > builds  6624ms
53
- ✓ fragment with rollup > installs  2570ms
54
- ✓ fragment with rollup > compiles  6173ms
55
- ✓ fragment with webpack > installs  2607ms
56
- ✓ fragment with webpack > compiles  6274ms
57
- ✓ fragment with webpack > builds  13005ms
58
- ✓ fragment with rspack > installs  3026ms
59
- ✓ fragment with rspack > compiles  5303ms
60
- ✓ fragment with rspack > builds  3715ms
61
-
62
-  Test Files  2 passed (2)
63
-  Tests  30 passed | 1 skipped (31)
64
-  Start at  14:59:53
65
-  Duration  25.62s (transform 325ms, setup 0ms, collect 498ms, tests 24.97s, environment 0ms, prepare 179ms)
66
-
@@ -1 +0,0 @@
1
- $ tsc --noEmit