@fragno-dev/create 0.0.3 → 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.3",
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
 
@@ -2,7 +2,7 @@
2
2
  "description": "A Fragno fragment",
3
3
  "version": "0.0.1",
4
4
  "files": ["dist"],
5
- "keywords": ["fragno", "typescript", "react", "vue", "svelte"],
5
+ "keywords": ["fragno", "typescript", "react", "vue", "svelte", "solidjs"],
6
6
  "exports": {
7
7
  ".": {
8
8
  "types": "./dist/node/index.d.ts",
@@ -32,6 +32,14 @@
32
32
  "types": "./dist/browser/client/svelte.d.ts",
33
33
  "default": "./dist/browser/client/svelte.js"
34
34
  },
35
+ "./solid": {
36
+ "development": {
37
+ "browser": "./dist/browser/client/solid.js",
38
+ "default": "./src/client/solid.ts"
39
+ },
40
+ "types": "./dist/browser/client/solid.d.ts",
41
+ "default": "./dist/browser/client/solid.js"
42
+ },
35
43
  "./vanilla": {
36
44
  "development": {
37
45
  "browser": "./dist/browser/client/vanilla.js",
@@ -48,18 +56,5 @@
48
56
  "types:check": "tsc --noEmit"
49
57
  },
50
58
  "type": "module",
51
- "dependencies": {
52
- "@fragno-dev/core": "^0.0.6",
53
- "zod": "^4.0.5"
54
- },
55
- "devDependencies": {
56
- "@types/node": "^20"
57
- },
58
- "private": true,
59
- "peerDependencies": {
60
- "typescript": "^5",
61
- "react": ">=18.0.0",
62
- "svelte": ">=4.0.0",
63
- "vue": ">=3.0.0"
64
- }
59
+ "private": true
65
60
  }
@@ -0,0 +1,7 @@
1
+ import { useFragno } from "@fragno-dev/core/solid";
2
+ import { createExampleFragmentClients } from "..";
3
+ import type { FragnoPublicClientConfig } from "@fragno-dev/core";
4
+
5
+ export function createExampleFragmentClient(config: FragnoPublicClientConfig = {}) {
6
+ return useFragno(createExampleFragmentClients(config));
7
+ }
@@ -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
@@ -9,6 +9,7 @@ export default defineConfig([
9
9
  "./src/index.ts",
10
10
  "./src/client/react.ts",
11
11
  "./src/client/svelte.ts",
12
+ "./src/client/solid.ts",
12
13
  "./src/client/vanilla.ts",
13
14
  "./src/client/vue.ts",
14
15
  ],
@@ -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) 22ms
7
- stdout | src/integration.test.ts > fragment with rspack
8
- temp /tmp/fragment-test-tsdown-1760971350638
9
-
10
- stdout | src/integration.test.ts > fragment with tsdown > package.json correctly templated
11
- temp /tmp/fragment-test-esbuild-1760971350646
12
-
13
- stdout | src/integration.test.ts > fragment with esbuild > package.json correctly templated
14
- temp /tmp/fragment-test-vite-1760971350646
15
-
16
- stdout | src/integration.test.ts > fragment with vite > package.json correctly templated
17
- temp /tmp/fragment-test-rollup-1760971350655
18
-
19
- stdout | src/integration.test.ts > fragment with rollup > package.json correctly templated
20
- temp /tmp/fragment-test-webpack-1760971350655
21
-
22
- stdout | src/integration.test.ts > fragment with webpack > package.json correctly templated
23
- temp /tmp/fragment-test-rspack-1760971350655
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 > fragment with esbuild > builds
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) 44063ms
44
- ✓ fragment with tsdown > installs  4713ms
45
- ✓ fragment with tsdown > compiles  10564ms
46
- ✓ fragment with tsdown > builds  22349ms
47
- ✓ fragment with esbuild > installs  4662ms
48
- ✓ fragment with esbuild > compiles  10549ms
49
- ✓ fragment with esbuild > builds  4265ms
50
- ✓ fragment with vite > installs  4586ms
51
- ✓ fragment with vite > compiles  10195ms
52
- ✓ fragment with vite > builds  15462ms
53
- ✓ fragment with rollup > installs  4570ms
54
- ✓ fragment with rollup > compiles  9853ms
55
- ✓ fragment with webpack > installs  4686ms
56
- ✓ fragment with webpack > compiles  10725ms
57
- ✓ fragment with webpack > builds  23445ms
58
- ✓ fragment with rspack > installs  5032ms
59
- ✓ fragment with rspack > compiles  11494ms
60
- ✓ fragment with rspack > builds  9897ms
61
-
62
-  Test Files  2 passed (2)
63
-  Tests  30 passed | 1 skipped (31)
64
-  Start at  14:42:27
65
-  Duration  46.59s (transform 1.38s, setup 0ms, collect 2.20s, tests 44.09s, environment 1ms, prepare 804ms)
66
-
@@ -1 +0,0 @@
1
- $ tsc --noEmit