@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 +9 -0
- package/README.md +46 -0
- package/bin/bionicjs.js +20 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +88 -0
- package/dist/router/index.d.ts +22 -0
- package/dist/router/index.js +187 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.js +11 -0
- package/package.json +86 -0
- package/src/api-client.ts +19 -0
- package/src/config.ts +13 -0
- package/src/entry-client.tsx +10 -0
- package/src/index.ts +16 -0
- package/src/plugin-utils.ts +116 -0
- package/src/router/adapter.tsx +14 -0
- package/src/router/conventions.ts +28 -0
- package/src/router/generator.ts +113 -0
- package/src/router/index.ts +4 -0
- package/src/router/parser.ts +80 -0
- package/src/router/types.ts +11 -0
- package/src/router.tsx +23 -0
- package/src/server.ts +7 -0
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>.
|
package/bin/bionicjs.js
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
};
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
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,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
|
+
}
|
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;
|