@fragno-dev/create 0.0.4 → 0.1.0

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.0",
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: 40000 }, 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.1";
4
+ const fragnoDbVersion = "^0.1.1";
5
+ const unpluginFragnoVersion = "^0.0.2";
6
+ const fragnoCliVersion = "^0.1.2";
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
  };
@@ -24,6 +24,95 @@ Fragments follow a core pattern:
24
24
  2. **Client-side**: Auto-generated type-safe hooks for each route
25
25
  3. **Code splitting**: Server-only code (handlers, dependencies) is stripped from client bundles
26
26
 
27
+ ## Database Integration (Optional)
28
+
29
+ Some Fragments require persistent storage. Fragno provides an optional database layer via
30
+ `@fragno-dev/db` that integrates with your users' existing databases.
31
+
32
+ ### When to Use Database Integration
33
+
34
+ Use `defineFragmentWithDatabase()` when your Fragment needs to:
35
+
36
+ - Store persistent data (comments, likes, user preferences, etc.)
37
+ - Query structured data efficiently
38
+ - Maintain data integrity with indexes and constraints
39
+ - Provide users with full control over their data
40
+
41
+ ### Schema Definition
42
+
43
+ Database schemas are defined in a separate `schema.ts` file using the Fragno schema builder:
44
+
45
+ ```typescript
46
+ import { column, idColumn, schema } from "@fragno-dev/db/schema";
47
+
48
+ export const noteSchema = schema((s) => {
49
+ return s.addTable("note", (t) => {
50
+ return t
51
+ .addColumn("id", idColumn()) // Auto-generated ID
52
+ .addColumn("content", column("string"))
53
+ .addColumn("userId", column("string"))
54
+ .addColumn("createdAt", column("timestamp").defaultTo$("now"))
55
+ .createIndex("idx_note_user", ["userId"]); // Index for efficient queries
56
+ });
57
+ });
58
+ ```
59
+
60
+ **Key concepts**:
61
+
62
+ - **Append-only**: Schemas use an append-only log approach. Never modify existing operations -
63
+ always add new ones
64
+ - **Versioning**: Each schema operation increments the version number
65
+ - **Indexes**: Create indexes on columns you'll frequently query (e.g., foreign keys, user IDs)
66
+ - **Defaults**: Use `.defaultTo(value)` or `.defaultTo$("now")` for timestamps
67
+
68
+ ### Using the ORM
69
+
70
+ The ORM is available in both `withDependencies()` and `withServices()` via the `orm` parameter:
71
+
72
+ ```typescript
73
+ const fragmentDef = defineFragmentWithDatabase<Config>("my-fragment")
74
+ .withDatabase(mySchema)
75
+ .withServices(({ orm }) => {
76
+ return {
77
+ createNote: async (note) => {
78
+ const id = await orm.create("note", note);
79
+ return { id: id.toJSON(), ...note };
80
+ },
81
+ getNotesByUser: (userId: string) => {
82
+ // Use whereIndex for efficient indexed queries
83
+ return orm.find("note", (b) =>
84
+ b.whereIndex("idx_note_user", (eb) => eb("userId", "=", userId)),
85
+ );
86
+ },
87
+ };
88
+ });
89
+ ```
90
+
91
+ **ORM methods**:
92
+
93
+ - `orm.create(table, data)` - Insert a row, returns FragnoId
94
+ - `orm.find(table, builder)` - Query rows with filtering
95
+ - `orm.findOne(table, builder)` - Query a single row
96
+ - `orm.update(table, id, data)` - Update a row by ID
97
+ - `orm.delete(table, id)` - Delete a row by ID
98
+ - `.whereIndex(indexName, condition)` - Use indexes for efficient queries
99
+
100
+ ### Fragment Configuration
101
+
102
+ Database-enabled Fragments require `FragnoPublicConfigWithDatabase`:
103
+
104
+ ```typescript
105
+ export function createMyFragment(
106
+ config: MyFragmentConfig = {},
107
+ options: FragnoPublicConfigWithDatabase, // Enforces databaseAdapter requirement
108
+ ) {
109
+ return createFragment(fragmentDef, config, [], options);
110
+ }
111
+ ```
112
+
113
+ For complete database documentation, see:
114
+ https://fragno.dev/docs/for-library-authors/database-integration/overview.md
115
+
27
116
  ## File Structure & Core Concepts
28
117
 
29
118
  ### `src/index.ts` - Main Fragment Definition
@@ -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,12 @@
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("createdAt", column("timestamp").defaultTo$("now"))
10
+ .createIndex("idx_note_user", ["userId"]);
11
+ });
12
+ });
@@ -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