@bionicjs/core 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/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bionicjs contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # bionicjs
2
+
3
+ The core package of the BionicJS meta-framework: a fullstack framework where
4
+ TypeScript owns the web (Vite + React + filesystem routing) and Python owns the
5
+ intelligence (AI and background jobs).
6
+
7
+ ## Exports
8
+
9
+ - `defineConfig` — typed entry for `bionicjs.config.ts`
10
+ - `createAuthPlugin`, `createDatabasePlugin`, `createAiPlugin`,
11
+ `createJobsPlugin` — factories the `@bionicjs/*` packages build on
12
+ - `api` — client-side RPC helper
13
+ - `BionicJSRouter` — mounts the app's filesystem routes
14
+ - `@bionicjs/core/server` — generated server exports (`auth`, `db`, `ai`, `jobs`)
15
+ - `@bionicjs/core/router` — route parser, generator, and layout adapter
16
+ - `runDevServer` — starts Vite + Nitro programmatically
17
+
18
+ ## Config
19
+
20
+ ```ts
21
+ // bionicjs.config.ts
22
+ import { defineConfig } from "@bionicjs/core";
23
+ import { betterAuth } from "@bionicjs/better-auth";
24
+ import { drizzle } from "@bionicjs/drizzle";
25
+
26
+ export default defineConfig({
27
+ auth: betterAuth({ emailAndPassword: { enabled: true } }),
28
+ database: drizzle({ provider: "sqlite", url: "file:./db.sqlite" }),
29
+ });
30
+ ```
31
+
32
+ ## Runtime
33
+
34
+ ```ts
35
+ // client
36
+ import { api } from "@bionicjs/core";
37
+ const result = await api.users.sayHello({ name: "bionicjs" });
38
+
39
+ // server
40
+ import { auth, db, ai, jobs } from "@bionicjs/core/server";
41
+ ```
42
+
43
+ ## Development
44
+
45
+ The package has no standalone test suite; its behavior is exercised through the
46
+ end-to-end scaffolding tests in <code>create-bionicjs-app</code>.
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createJiti } from "jiti";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const jiti = createJiti(fileURLToPath(import.meta.url));
7
+
8
+ const command = process.argv[2];
9
+
10
+ if (command === "dev" || !command) {
11
+ const { runDevServer } = await jiti.import("@bionicjs/dev");
12
+ runDevServer().catch((err) => {
13
+ console.error("Error starting BionicJS dev server:", err);
14
+ process.exit(1);
15
+ });
16
+ } else {
17
+ console.log(`Unknown command: ${command}`);
18
+ console.log("Usage: bionicjs dev");
19
+ process.exit(1);
20
+ }
@@ -0,0 +1,67 @@
1
+ import React from 'react';
2
+ export { runDevServer } from '@bionicjs/dev';
3
+
4
+ declare function BionicJSRouter(): React.JSX.Element;
5
+
6
+ declare const api: any;
7
+
8
+ interface BionicJSPlugin<TName extends string = string, TExports = any> {
9
+ name: TName;
10
+ setup: (app: any) => void | Promise<void>;
11
+ generateExports?: () => string | Promise<string>;
12
+ }
13
+ type BionicJSConfig = {
14
+ [key: string]: BionicJSPlugin | any;
15
+ };
16
+ declare function defineConfig(config: BionicJSConfig): BionicJSConfig;
17
+
18
+ /**
19
+ * bionicjs plugin factory utilities
20
+ *
21
+ * Inspired by Farm.js integrations — each provider package stays thin.
22
+ * Shared logic lives here so every integration has a consistent shape.
23
+ *
24
+ * Separation:
25
+ * Config = "what" and "which" (provider, model, path, feature flags)
26
+ * Code = "how" (real handler, query builder, agent logic — lives in server/)
27
+ */
28
+
29
+ interface BionicJSContext {
30
+ cwd: string;
31
+ mode: "development" | "production";
32
+ }
33
+ interface AuthPluginOptions<TOptions = unknown> {
34
+ /** e.g. "better-auth", "clerk" */
35
+ name: string;
36
+ options: TOptions;
37
+ /** Mount auth HTTP handler at this path pattern */
38
+ path?: string;
39
+ /** Called once at server startup */
40
+ setup?: (ctx: BionicJSContext, options: TOptions) => void | Promise<void>;
41
+ /** Returns runtime export code injected into .bionicjs/server.ts */
42
+ generateExports?: (options: TOptions) => string;
43
+ }
44
+ declare function createAuthPlugin<TOptions>(def: AuthPluginOptions<TOptions>): BionicJSPlugin;
45
+ interface DatabasePluginOptions<TOptions = unknown> {
46
+ name: string;
47
+ options: TOptions;
48
+ setup?: (ctx: BionicJSContext, options: TOptions) => void | Promise<void>;
49
+ generateExports?: (options: TOptions) => string;
50
+ }
51
+ declare function createDatabasePlugin<TOptions>(def: DatabasePluginOptions<TOptions>): BionicJSPlugin;
52
+ interface AiPluginOptions<TOptions = unknown> {
53
+ name: string;
54
+ options: TOptions;
55
+ setup?: (ctx: BionicJSContext, options: TOptions) => void | Promise<void>;
56
+ generateExports?: (options: TOptions) => string;
57
+ }
58
+ declare function createAiPlugin<TOptions>(def: AiPluginOptions<TOptions>): BionicJSPlugin;
59
+ interface JobsPluginOptions<TOptions = unknown> {
60
+ name: string;
61
+ options: TOptions;
62
+ setup?: (ctx: BionicJSContext, options: TOptions) => void | Promise<void>;
63
+ generateExports?: (options: TOptions) => string;
64
+ }
65
+ declare function createJobsPlugin<TOptions>(def: JobsPluginOptions<TOptions>): BionicJSPlugin;
66
+
67
+ export { type AiPluginOptions, type AuthPluginOptions, type BionicJSConfig, type BionicJSContext, type BionicJSPlugin, BionicJSRouter, type DatabasePluginOptions, type JobsPluginOptions, api, createAiPlugin, createAuthPlugin, createDatabasePlugin, createJobsPlugin, defineConfig };
package/dist/index.js ADDED
@@ -0,0 +1,88 @@
1
+ // src/router.tsx
2
+ import { lazy, Suspense } from "react";
3
+ import { createBrowserRouter, RouterProvider } from "react-router";
4
+ import { jsx } from "react/jsx-runtime";
5
+ var LazyRoutes = lazy(
6
+ () => (
7
+ // @ts-expect-error Virtual module provided by bionicjsRoutesPlugin
8
+ import("virtual:bionicjs-routes").then((mod) => ({
9
+ default: function BionicJSRoutes() {
10
+ const router = createBrowserRouter(mod.routes);
11
+ return /* @__PURE__ */ jsx(RouterProvider, { router });
12
+ }
13
+ }))
14
+ )
15
+ );
16
+ function BionicJSRouter() {
17
+ return /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(LazyRoutes, {}) });
18
+ }
19
+
20
+ // src/api-client.ts
21
+ var api = new Proxy({}, {
22
+ get(_, path) {
23
+ return new Proxy({}, {
24
+ get(_2, method) {
25
+ return async (...args) => {
26
+ const res = await fetch(`/api/${String(path)}`, {
27
+ method: "POST",
28
+ headers: { "Content-Type": "application/json" },
29
+ body: JSON.stringify(args[0])
30
+ });
31
+ return res.json();
32
+ };
33
+ }
34
+ });
35
+ }
36
+ });
37
+
38
+ // src/index.ts
39
+ import { runDevServer } from "@bionicjs/dev";
40
+
41
+ // src/config.ts
42
+ function defineConfig(config) {
43
+ return config;
44
+ }
45
+
46
+ // src/plugin-utils.ts
47
+ function createAuthPlugin(def) {
48
+ return {
49
+ name: "auth",
50
+ setup: def.setup ? (app) => def.setup(app, def.options) : () => {
51
+ },
52
+ generateExports: def.generateExports ? () => def.generateExports(def.options) : void 0
53
+ };
54
+ }
55
+ function createDatabasePlugin(def) {
56
+ return {
57
+ name: "db",
58
+ setup: def.setup ? (app) => def.setup(app, def.options) : () => {
59
+ },
60
+ generateExports: def.generateExports ? () => def.generateExports(def.options) : void 0
61
+ };
62
+ }
63
+ function createAiPlugin(def) {
64
+ return {
65
+ name: "ai",
66
+ setup: def.setup ? (app) => def.setup(app, def.options) : () => {
67
+ },
68
+ generateExports: def.generateExports ? () => def.generateExports(def.options) : void 0
69
+ };
70
+ }
71
+ function createJobsPlugin(def) {
72
+ return {
73
+ name: "jobs",
74
+ setup: def.setup ? (app) => def.setup(app, def.options) : () => {
75
+ },
76
+ generateExports: def.generateExports ? () => def.generateExports(def.options) : void 0
77
+ };
78
+ }
79
+ export {
80
+ BionicJSRouter,
81
+ api,
82
+ createAiPlugin,
83
+ createAuthPlugin,
84
+ createDatabasePlugin,
85
+ createJobsPlugin,
86
+ defineConfig,
87
+ runDevServer
88
+ };
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+
3
+ interface RouteManifestNode {
4
+ segment: string;
5
+ path?: string;
6
+ page?: string;
7
+ layout?: string;
8
+ children: RouteManifestNode[];
9
+ }
10
+
11
+ declare function parseRoutes(files: string[]): RouteManifestNode;
12
+
13
+ declare function generateRouteModule(manifest: RouteManifestNode): string;
14
+
15
+ interface BionicJSLayoutAdapterProps {
16
+ Layout: React.ComponentType<{
17
+ children?: React.ReactNode;
18
+ }>;
19
+ }
20
+ declare function BionicJSLayoutAdapter({ Layout }: BionicJSLayoutAdapterProps): React.JSX.Element;
21
+
22
+ export { BionicJSLayoutAdapter, type RouteManifestNode, generateRouteModule, parseRoutes };
@@ -0,0 +1,187 @@
1
+ // src/router/conventions.ts
2
+ var PAGE_FILE = "page.tsx";
3
+ var LAYOUT_FILE = "layout.tsx";
4
+ function isPageFile(filename) {
5
+ return filename === PAGE_FILE || filename.endsWith(`/${PAGE_FILE}`);
6
+ }
7
+ function isLayoutFile(filename) {
8
+ return filename === LAYOUT_FILE || filename.endsWith(`/${LAYOUT_FILE}`);
9
+ }
10
+ function isDynamicSegment(segment) {
11
+ return segment.startsWith("[") && segment.endsWith("]") && !segment.startsWith("[...");
12
+ }
13
+ function isCatchAllSegment(segment) {
14
+ return segment.startsWith("[...") && segment.endsWith("]");
15
+ }
16
+ function formatSegmentToRoutePath(segment) {
17
+ if (isCatchAllSegment(segment)) {
18
+ return "*";
19
+ }
20
+ if (isDynamicSegment(segment)) {
21
+ return `:${segment.slice(1, -1)}`;
22
+ }
23
+ return segment;
24
+ }
25
+
26
+ // src/router/parser.ts
27
+ function parseRoutes(files) {
28
+ const root = {
29
+ segment: "",
30
+ children: []
31
+ };
32
+ const getOrCreateNode = (segments) => {
33
+ let current = root;
34
+ for (const segment of segments) {
35
+ const formattedPath = formatSegmentToRoutePath(segment);
36
+ const existingConflict = current.children.find((c) => {
37
+ if (c.segment === segment) return false;
38
+ if (isDynamicSegment(segment) && isDynamicSegment(c.segment)) return true;
39
+ if (isCatchAllSegment(segment) && isCatchAllSegment(c.segment)) return true;
40
+ return c.path === formattedPath;
41
+ });
42
+ if (existingConflict) {
43
+ throw new Error(
44
+ `BionicJS Route Conflict: Conflicting segments "${segment}" and "${existingConflict.segment}" at the same directory level.`
45
+ );
46
+ }
47
+ let child = current.children.find((c) => c.segment === segment);
48
+ if (!child) {
49
+ child = {
50
+ segment,
51
+ path: formattedPath,
52
+ children: []
53
+ };
54
+ current.children.push(child);
55
+ }
56
+ current = child;
57
+ }
58
+ return current;
59
+ };
60
+ for (const rawFile of files) {
61
+ let normalized = rawFile.replace(/\\/g, "/");
62
+ normalized = normalized.replace(/^(\.\/|\/)?(app\/|\/)?/, "");
63
+ const parts = normalized.split("/").filter(Boolean);
64
+ if (parts.length === 0) continue;
65
+ const filename = parts[parts.length - 1];
66
+ if (!isPageFile(filename) && !isLayoutFile(filename)) {
67
+ continue;
68
+ }
69
+ const dirSegments = parts.slice(0, parts.length - 1);
70
+ const targetNode = dirSegments.length === 0 ? root : getOrCreateNode(dirSegments);
71
+ const fullVitePath = rawFile.startsWith("/") ? rawFile : `/${rawFile}`;
72
+ if (isPageFile(filename)) {
73
+ targetNode.page = fullVitePath;
74
+ } else if (isLayoutFile(filename)) {
75
+ targetNode.layout = fullVitePath;
76
+ }
77
+ }
78
+ return root;
79
+ }
80
+
81
+ // src/router/generator.ts
82
+ function generateNodeCode(node, isRoot) {
83
+ const parts = [];
84
+ if (node.layout) {
85
+ const layoutLazy = `lazy: async () => {
86
+ const mod = await import("${node.layout}");
87
+ return {
88
+ Component: () => React.createElement(BionicJSLayoutAdapter, { Layout: mod.default })
89
+ };
90
+ }`;
91
+ const childrenCode = [];
92
+ if (node.page) {
93
+ childrenCode.push(`{
94
+ index: true,
95
+ lazy: async () => {
96
+ const mod = await import("${node.page}");
97
+ return { Component: mod.default };
98
+ }
99
+ }`);
100
+ }
101
+ for (const child of node.children) {
102
+ childrenCode.push(generateNodeCode(child, false));
103
+ }
104
+ if (isRoot) {
105
+ return `{
106
+ ${layoutLazy},
107
+ children: [
108
+ ${childrenCode.join(",\n")}
109
+ ]
110
+ }`;
111
+ } else {
112
+ return `{
113
+ ${node.path ? `path: "${node.path}",` : ""}
114
+ ${layoutLazy},
115
+ children: [
116
+ ${childrenCode.join(",\n")}
117
+ ]
118
+ }`;
119
+ }
120
+ }
121
+ if (isRoot) {
122
+ const rootChildren = [];
123
+ if (node.page) {
124
+ rootChildren.push(`{
125
+ index: true,
126
+ lazy: async () => {
127
+ const mod = await import("${node.page}");
128
+ return { Component: mod.default };
129
+ }
130
+ }`);
131
+ }
132
+ for (const child of node.children) {
133
+ rootChildren.push(generateNodeCode(child, false));
134
+ }
135
+ return rootChildren.join(",\n");
136
+ }
137
+ const childCodes = node.children.map((c) => generateNodeCode(c, false));
138
+ if (node.page) {
139
+ const pageLazy = `lazy: async () => {
140
+ const mod = await import("${node.page}");
141
+ return { Component: mod.default };
142
+ }`;
143
+ if (childCodes.length > 0) {
144
+ return `{
145
+ path: "${node.path}",
146
+ ${pageLazy},
147
+ children: [
148
+ ${childCodes.join(",\n")}
149
+ ]
150
+ }`;
151
+ } else {
152
+ return `{
153
+ path: "${node.path}",
154
+ ${pageLazy}
155
+ }`;
156
+ }
157
+ } else {
158
+ return `{
159
+ path: "${node.path}",
160
+ children: [
161
+ ${childCodes.join(",\n")}
162
+ ]
163
+ }`;
164
+ }
165
+ }
166
+ function generateRouteModule(manifest) {
167
+ const routesCode = generateNodeCode(manifest, true);
168
+ return `import React from "react";
169
+ import { BionicJSLayoutAdapter } from "@bionicjs/core/router";
170
+
171
+ export const routes = [
172
+ ${routesCode}
173
+ ];
174
+ `;
175
+ }
176
+
177
+ // src/router/adapter.tsx
178
+ import { Outlet } from "react-router";
179
+ import { jsx } from "react/jsx-runtime";
180
+ function BionicJSLayoutAdapter({ Layout }) {
181
+ return /* @__PURE__ */ jsx(Layout, { children: /* @__PURE__ */ jsx(Outlet, {}) });
182
+ }
183
+ export {
184
+ BionicJSLayoutAdapter,
185
+ generateRouteModule,
186
+ parseRoutes
187
+ };
@@ -0,0 +1,6 @@
1
+ declare const auth: undefined;
2
+ declare const db: undefined;
3
+ declare const ai: undefined;
4
+ declare const jobs: undefined;
5
+
6
+ export { ai, auth, db, jobs };
package/dist/server.js ADDED
@@ -0,0 +1,11 @@
1
+ // src/server.ts
2
+ var auth = void 0;
3
+ var db = void 0;
4
+ var ai = void 0;
5
+ var jobs = void 0;
6
+ export {
7
+ ai,
8
+ auth,
9
+ db,
10
+ jobs
11
+ };
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "@bionicjs/core",
3
+ "version": "0.1.0",
4
+ "description": "The full-stack React framework for the AI era: React UI and Python intelligence in one codebase",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./server": {
15
+ "types": "./dist/server.d.ts",
16
+ "import": "./dist/server.js",
17
+ "default": "./dist/server.js"
18
+ },
19
+ "./router": {
20
+ "types": "./dist/router/index.d.ts",
21
+ "import": "./dist/router/index.js",
22
+ "default": "./dist/router/index.js"
23
+ },
24
+ "./entry-client": "./src/entry-client.tsx",
25
+ "./package.json": "./package.json"
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "src",
30
+ "bin",
31
+ "README.md",
32
+ "!src/**/*.test.ts"
33
+ ],
34
+ "imports": {
35
+ "#bionicjs-api": "./src/api-client.ts"
36
+ },
37
+ "bin": {
38
+ "bionicjs": "./bin/bionicjs.js"
39
+ },
40
+ "dependencies": {
41
+ "@bionicjs/dev": "0.1.0",
42
+ "jiti": "^2.7.0",
43
+ "react-router": "^7"
44
+ },
45
+ "peerDependencies": {
46
+ "react": "^19",
47
+ "react-dom": "^19"
48
+ },
49
+ "devDependencies": {
50
+ "vite": "^8.3.0",
51
+ "tsup": "^8.5.1",
52
+ "typescript": "^5.9.3",
53
+ "tsx": "^4.23.15",
54
+ "@types/node": "^22",
55
+ "react": "^19",
56
+ "react-dom": "^19",
57
+ "@types/react": "^19",
58
+ "@types/react-dom": "^19"
59
+ },
60
+ "license": "MIT",
61
+ "keywords": [
62
+ "bionicjs",
63
+ "framework",
64
+ "react",
65
+ "python",
66
+ "vite",
67
+ "nitro"
68
+ ],
69
+ "repository": {
70
+ "type": "git",
71
+ "url": "git+https://github.com/bionicjs/bionicjs.git",
72
+ "directory": "packages/bionicjs"
73
+ },
74
+ "bugs": {
75
+ "url": "https://github.com/bionicjs/bionicjs/issues"
76
+ },
77
+ "homepage": "https://github.com/bionicjs/bionicjs#readme",
78
+ "engines": {
79
+ "node": ">=20"
80
+ },
81
+ "scripts": {
82
+ "build": "tsup src/router/index.ts --format esm --dts --out-dir dist/router && tsup src/index.ts src/server.ts --format esm --target node20 --dts --external virtual:bionicjs-routes",
83
+ "typecheck": "tsc --noEmit",
84
+ "test": "tsx src/router/router.test.ts && tsx src/router/integration.test.ts && tsx src/router/vite-e2e.test.ts"
85
+ }
86
+ }
@@ -0,0 +1,19 @@
1
+ // AUTO-GENERATED by BionicJS — do not edit
2
+ // This is a stub. The real client is generated into .bionicjs/api-client.ts at dev/build time.
3
+ // The Vite plugin resolves "#bionicjs-api" to .bionicjs/api-client.ts.
4
+ export const api = new Proxy({} as any, {
5
+ get(_, path) {
6
+ return new Proxy({} as any, {
7
+ get(_, method) {
8
+ return async (...args: any[]) => {
9
+ const res = await fetch(`/api/${String(path)}`, {
10
+ method: "POST",
11
+ headers: { "Content-Type": "application/json" },
12
+ body: JSON.stringify(args[0]),
13
+ });
14
+ return res.json();
15
+ };
16
+ },
17
+ });
18
+ },
19
+ });
package/src/config.ts ADDED
@@ -0,0 +1,13 @@
1
+ export interface BionicJSPlugin<TName extends string = string, TExports = any> {
2
+ name: TName;
3
+ setup: (app: any) => void | Promise<void>;
4
+ generateExports?: () => string | Promise<string>;
5
+ }
6
+
7
+ export type BionicJSConfig = {
8
+ [key: string]: BionicJSPlugin | any;
9
+ };
10
+
11
+ export function defineConfig(config: BionicJSConfig): BionicJSConfig {
12
+ return config;
13
+ }
@@ -0,0 +1,10 @@
1
+ import React, { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { BionicJSRouter } from "./router";
4
+ import "/app/globals.css"; // Ensure user's globals.css is imported
5
+
6
+ createRoot(document.getElementById("root")!).render(
7
+ <StrictMode>
8
+ <BionicJSRouter />
9
+ </StrictMode>
10
+ );
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ export { BionicJSRouter } from "./router";
2
+ export { api } from "./api-client";
3
+ export { runDevServer } from "@bionicjs/dev";
4
+
5
+ export { defineConfig, type BionicJSConfig, type BionicJSPlugin } from "./config";
6
+ export {
7
+ createAuthPlugin,
8
+ createDatabasePlugin,
9
+ createAiPlugin,
10
+ createJobsPlugin,
11
+ type BionicJSContext,
12
+ type AuthPluginOptions,
13
+ type DatabasePluginOptions,
14
+ type AiPluginOptions,
15
+ type JobsPluginOptions,
16
+ } from "./plugin-utils";
@@ -0,0 +1,116 @@
1
+ /**
2
+ * bionicjs plugin factory utilities
3
+ *
4
+ * Inspired by Farm.js integrations — each provider package stays thin.
5
+ * Shared logic lives here so every integration has a consistent shape.
6
+ *
7
+ * Separation:
8
+ * Config = "what" and "which" (provider, model, path, feature flags)
9
+ * Code = "how" (real handler, query builder, agent logic — lives in server/)
10
+ */
11
+
12
+ import type { BionicJSPlugin } from "./config.ts";
13
+
14
+ // ─── Shared app context ───────────────────────────────────────────────────────
15
+
16
+ export interface BionicJSContext {
17
+ cwd: string;
18
+ mode: "development" | "production";
19
+ }
20
+
21
+ // ─── Auth ─────────────────────────────────────────────────────────────────────
22
+
23
+ export interface AuthPluginOptions<TOptions = unknown> {
24
+ /** e.g. "better-auth", "clerk" */
25
+ name: string;
26
+ options: TOptions;
27
+ /** Mount auth HTTP handler at this path pattern */
28
+ path?: string;
29
+ /** Called once at server startup */
30
+ setup?: (ctx: BionicJSContext, options: TOptions) => void | Promise<void>;
31
+ /** Returns runtime export code injected into .bionicjs/server.ts */
32
+ generateExports?: (options: TOptions) => string;
33
+ }
34
+
35
+ export function createAuthPlugin<TOptions>(
36
+ def: AuthPluginOptions<TOptions>,
37
+ ): BionicJSPlugin {
38
+ return {
39
+ name: "auth",
40
+ setup: def.setup
41
+ ? (app: any) => def.setup!(app as BionicJSContext, def.options)
42
+ : () => {},
43
+ generateExports: def.generateExports
44
+ ? () => def.generateExports!(def.options)
45
+ : undefined,
46
+ };
47
+ }
48
+
49
+ // ─── Database ─────────────────────────────────────────────────────────────────
50
+
51
+ export interface DatabasePluginOptions<TOptions = unknown> {
52
+ name: string;
53
+ options: TOptions;
54
+ setup?: (ctx: BionicJSContext, options: TOptions) => void | Promise<void>;
55
+ generateExports?: (options: TOptions) => string;
56
+ }
57
+
58
+ export function createDatabasePlugin<TOptions>(
59
+ def: DatabasePluginOptions<TOptions>,
60
+ ): BionicJSPlugin {
61
+ return {
62
+ name: "db",
63
+ setup: def.setup
64
+ ? (app: any) => def.setup!(app as BionicJSContext, def.options)
65
+ : () => {},
66
+ generateExports: def.generateExports
67
+ ? () => def.generateExports!(def.options)
68
+ : undefined,
69
+ };
70
+ }
71
+
72
+ // ─── AI ───────────────────────────────────────────────────────────────────────
73
+
74
+ export interface AiPluginOptions<TOptions = unknown> {
75
+ name: string;
76
+ options: TOptions;
77
+ setup?: (ctx: BionicJSContext, options: TOptions) => void | Promise<void>;
78
+ generateExports?: (options: TOptions) => string;
79
+ }
80
+
81
+ export function createAiPlugin<TOptions>(
82
+ def: AiPluginOptions<TOptions>,
83
+ ): BionicJSPlugin {
84
+ return {
85
+ name: "ai",
86
+ setup: def.setup
87
+ ? (app: any) => def.setup!(app as BionicJSContext, def.options)
88
+ : () => {},
89
+ generateExports: def.generateExports
90
+ ? () => def.generateExports!(def.options)
91
+ : undefined,
92
+ };
93
+ }
94
+
95
+ // ─── Jobs ─────────────────────────────────────────────────────────────────────
96
+
97
+ export interface JobsPluginOptions<TOptions = unknown> {
98
+ name: string;
99
+ options: TOptions;
100
+ setup?: (ctx: BionicJSContext, options: TOptions) => void | Promise<void>;
101
+ generateExports?: (options: TOptions) => string;
102
+ }
103
+
104
+ export function createJobsPlugin<TOptions>(
105
+ def: JobsPluginOptions<TOptions>,
106
+ ): BionicJSPlugin {
107
+ return {
108
+ name: "jobs",
109
+ setup: def.setup
110
+ ? (app: any) => def.setup!(app as BionicJSContext, def.options)
111
+ : () => {},
112
+ generateExports: def.generateExports
113
+ ? () => def.generateExports!(def.options)
114
+ : undefined,
115
+ };
116
+ }
@@ -0,0 +1,14 @@
1
+ import React from "react";
2
+ import { Outlet } from "react-router";
3
+
4
+ export interface BionicJSLayoutAdapterProps {
5
+ Layout: React.ComponentType<{ children?: React.ReactNode }>;
6
+ }
7
+
8
+ export function BionicJSLayoutAdapter({ Layout }: BionicJSLayoutAdapterProps) {
9
+ return (
10
+ <Layout>
11
+ <Outlet />
12
+ </Layout>
13
+ );
14
+ }
@@ -0,0 +1,28 @@
1
+ export const PAGE_FILE = "page.tsx";
2
+ export const LAYOUT_FILE = "layout.tsx";
3
+
4
+ export function isPageFile(filename: string): boolean {
5
+ return filename === PAGE_FILE || filename.endsWith(`/${PAGE_FILE}`);
6
+ }
7
+
8
+ export function isLayoutFile(filename: string): boolean {
9
+ return filename === LAYOUT_FILE || filename.endsWith(`/${LAYOUT_FILE}`);
10
+ }
11
+
12
+ export function isDynamicSegment(segment: string): boolean {
13
+ return segment.startsWith("[") && segment.endsWith("]") && !segment.startsWith("[...");
14
+ }
15
+
16
+ export function isCatchAllSegment(segment: string): boolean {
17
+ return segment.startsWith("[...") && segment.endsWith("]");
18
+ }
19
+
20
+ export function formatSegmentToRoutePath(segment: string): string {
21
+ if (isCatchAllSegment(segment)) {
22
+ return "*";
23
+ }
24
+ if (isDynamicSegment(segment)) {
25
+ return `:${segment.slice(1, -1)}`;
26
+ }
27
+ return segment;
28
+ }
@@ -0,0 +1,113 @@
1
+ import { RouteManifestNode } from "./types";
2
+
3
+ function generateNodeCode(node: RouteManifestNode, isRoot: boolean): string {
4
+ const parts: string[] = [];
5
+
6
+ // Handle Layout wrapper if node has a layout file
7
+ if (node.layout) {
8
+ const layoutLazy = `lazy: async () => {
9
+ const mod = await import("${node.layout}");
10
+ return {
11
+ Component: () => React.createElement(BionicJSLayoutAdapter, { Layout: mod.default })
12
+ };
13
+ }`;
14
+
15
+ const childrenCode: string[] = [];
16
+
17
+ // Add page as index route under layout if page exists
18
+ if (node.page) {
19
+ childrenCode.push(`{
20
+ index: true,
21
+ lazy: async () => {
22
+ const mod = await import("${node.page}");
23
+ return { Component: mod.default };
24
+ }
25
+ }`);
26
+ }
27
+
28
+ // Process child subdirectories
29
+ for (const child of node.children) {
30
+ childrenCode.push(generateNodeCode(child, false));
31
+ }
32
+
33
+ if (isRoot) {
34
+ return `{
35
+ ${layoutLazy},
36
+ children: [
37
+ ${childrenCode.join(",\n")}
38
+ ]
39
+ }`;
40
+ } else {
41
+ return `{
42
+ ${node.path ? `path: "${node.path}",` : ""}
43
+ ${layoutLazy},
44
+ children: [
45
+ ${childrenCode.join(",\n")}
46
+ ]
47
+ }`;
48
+ }
49
+ }
50
+
51
+ // Node does NOT have a layout file
52
+ if (isRoot) {
53
+ const rootChildren: string[] = [];
54
+ if (node.page) {
55
+ rootChildren.push(`{
56
+ index: true,
57
+ lazy: async () => {
58
+ const mod = await import("${node.page}");
59
+ return { Component: mod.default };
60
+ }
61
+ }`);
62
+ }
63
+ for (const child of node.children) {
64
+ rootChildren.push(generateNodeCode(child, false));
65
+ }
66
+ return rootChildren.join(",\n");
67
+ }
68
+
69
+ // Non-root node without layout
70
+ const childCodes = node.children.map((c) => generateNodeCode(c, false));
71
+
72
+ if (node.page) {
73
+ const pageLazy = `lazy: async () => {
74
+ const mod = await import("${node.page}");
75
+ return { Component: mod.default };
76
+ }`;
77
+
78
+ if (childCodes.length > 0) {
79
+ return `{
80
+ path: "${node.path}",
81
+ ${pageLazy},
82
+ children: [
83
+ ${childCodes.join(",\n")}
84
+ ]
85
+ }`;
86
+ } else {
87
+ return `{
88
+ path: "${node.path}",
89
+ ${pageLazy}
90
+ }`;
91
+ }
92
+ } else {
93
+ // Transparent directory segment without layout or page
94
+ return `{
95
+ path: "${node.path}",
96
+ children: [
97
+ ${childCodes.join(",\n")}
98
+ ]
99
+ }`;
100
+ }
101
+ }
102
+
103
+ export function generateRouteModule(manifest: RouteManifestNode): string {
104
+ const routesCode = generateNodeCode(manifest, true);
105
+
106
+ return `import React from "react";
107
+ import { BionicJSLayoutAdapter } from "@bionicjs/core/router";
108
+
109
+ export const routes = [
110
+ ${routesCode}
111
+ ];
112
+ `;
113
+ }
@@ -0,0 +1,4 @@
1
+ export { parseRoutes } from "./parser";
2
+ export { generateRouteModule } from "./generator";
3
+ export { BionicJSLayoutAdapter } from "./adapter";
4
+ export type { RouteManifestNode } from "./types";
@@ -0,0 +1,80 @@
1
+ import { RouteManifestNode } from "./types";
2
+ import {
3
+ formatSegmentToRoutePath,
4
+ isPageFile,
5
+ isLayoutFile,
6
+ isDynamicSegment,
7
+ isCatchAllSegment,
8
+ } from "./conventions";
9
+
10
+ export function parseRoutes(files: string[]): RouteManifestNode {
11
+ const root: RouteManifestNode = {
12
+ segment: "",
13
+ children: [],
14
+ };
15
+
16
+ const getOrCreateNode = (segments: string[]): RouteManifestNode => {
17
+ let current = root;
18
+ for (const segment of segments) {
19
+ const formattedPath = formatSegmentToRoutePath(segment);
20
+
21
+ // Check for route conflicts:
22
+ // 1. Multiple dynamic parameter segments at the same level (e.g. [id] vs [userId])
23
+ // 2. Multiple catch-all segments at the same level
24
+ const existingConflict = current.children.find((c) => {
25
+ if (c.segment === segment) return false;
26
+ if (isDynamicSegment(segment) && isDynamicSegment(c.segment)) return true;
27
+ if (isCatchAllSegment(segment) && isCatchAllSegment(c.segment)) return true;
28
+ return c.path === formattedPath;
29
+ });
30
+
31
+ if (existingConflict) {
32
+ throw new Error(
33
+ `BionicJS Route Conflict: Conflicting segments "${segment}" and "${existingConflict.segment}" at the same directory level.`
34
+ );
35
+ }
36
+
37
+ let child = current.children.find((c) => c.segment === segment);
38
+ if (!child) {
39
+ child = {
40
+ segment,
41
+ path: formattedPath,
42
+ children: [],
43
+ };
44
+ current.children.push(child);
45
+ }
46
+ current = child;
47
+ }
48
+ return current;
49
+ };
50
+
51
+ for (const rawFile of files) {
52
+ // Normalize path to remove leading dot/slash and /app prefix
53
+ let normalized = rawFile.replace(/\\/g, "/");
54
+ normalized = normalized.replace(/^(\.\/|\/)?(app\/|\/)?/, "");
55
+
56
+ const parts = normalized.split("/").filter(Boolean);
57
+ if (parts.length === 0) continue;
58
+
59
+ const filename = parts[parts.length - 1];
60
+
61
+ // Only process page.tsx and layout.tsx files
62
+ if (!isPageFile(filename) && !isLayoutFile(filename)) {
63
+ continue;
64
+ }
65
+
66
+ const dirSegments = parts.slice(0, parts.length - 1);
67
+ const targetNode = dirSegments.length === 0 ? root : getOrCreateNode(dirSegments);
68
+
69
+ // Ensure page/layout paths retain standard Vite root format (e.g. /app/...)
70
+ const fullVitePath = rawFile.startsWith("/") ? rawFile : `/${rawFile}`;
71
+
72
+ if (isPageFile(filename)) {
73
+ targetNode.page = fullVitePath;
74
+ } else if (isLayoutFile(filename)) {
75
+ targetNode.layout = fullVitePath;
76
+ }
77
+ }
78
+
79
+ return root;
80
+ }
@@ -0,0 +1,11 @@
1
+ export interface RouteManifestNode {
2
+ segment: string;
3
+ path?: string;
4
+ page?: string;
5
+ layout?: string;
6
+ children: RouteManifestNode[];
7
+ }
8
+
9
+ export interface ParseOptions {
10
+ appDir?: string;
11
+ }
package/src/router.tsx ADDED
@@ -0,0 +1,23 @@
1
+ import React, { lazy, Suspense } from "react";
2
+ import { createBrowserRouter, RouterProvider } from "react-router";
3
+
4
+ // virtual:bionicjs-routes is provided by the bionicjs-routes Vite plugin. It is loaded
5
+ // lazily so that importing @bionicjs/core never touches the virtual module outside
6
+ // a browser (Node-side config loading, plugin packages, etc. stay crash-free).
7
+ const LazyRoutes = lazy(() =>
8
+ // @ts-expect-error Virtual module provided by bionicjsRoutesPlugin
9
+ import("virtual:bionicjs-routes").then((mod) => ({
10
+ default: function BionicJSRoutes() {
11
+ const router = createBrowserRouter(mod.routes);
12
+ return <RouterProvider router={router} />;
13
+ },
14
+ })),
15
+ );
16
+
17
+ export function BionicJSRouter() {
18
+ return (
19
+ <Suspense fallback={null}>
20
+ <LazyRoutes />
21
+ </Suspense>
22
+ );
23
+ }
package/src/server.ts ADDED
@@ -0,0 +1,7 @@
1
+ // AUTO-GENERATED by BionicJS — do not edit
2
+ // This file re-exports server-only helpers wired through the plugin system.
3
+ // Import these from "@bionicjs/core/server" in your server-side code.
4
+ export const auth = undefined;
5
+ export const db = undefined;
6
+ export const ai = undefined;
7
+ export const jobs = undefined;