@kasperrt/smolter 0.0.0-stage → 0.0.1-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +470 -2
- package/dist/client.d.ts +2 -0
- package/dist/client.js +2 -0
- package/dist/core.d.ts +2 -0
- package/dist/core.js +2 -0
- package/dist/files.d.ts +2 -0
- package/dist/files.js +2 -0
- package/dist/internal/client/deserialize.d.ts +3 -0
- package/dist/internal/client/deserialize.js +11 -0
- package/dist/internal/client/index.d.ts +23 -0
- package/dist/internal/client/index.js +44 -0
- package/dist/internal/core/errors.d.ts +3 -0
- package/dist/internal/core/errors.js +6 -0
- package/dist/internal/core/index.d.ts +6 -0
- package/dist/internal/core/index.js +6 -0
- package/dist/internal/core/match.d.ts +5 -0
- package/dist/internal/core/match.js +27 -0
- package/dist/internal/core/router.d.ts +3 -0
- package/dist/internal/core/router.js +189 -0
- package/dist/internal/core/types.d.ts +119 -0
- package/dist/internal/core/types.js +5 -0
- package/dist/internal/core/url.d.ts +2 -0
- package/dist/internal/core/url.js +6 -0
- package/dist/internal/files/index.d.ts +11 -0
- package/dist/internal/files/index.js +77 -0
- package/dist/internal/preact/context.d.ts +18 -0
- package/dist/internal/preact/context.js +20 -0
- package/dist/internal/preact/fetcher.d.ts +22 -0
- package/dist/internal/preact/fetcher.js +28 -0
- package/dist/internal/preact/form.d.ts +17 -0
- package/dist/internal/preact/form.js +41 -0
- package/dist/internal/preact/hooks.d.ts +21 -0
- package/dist/internal/preact/hooks.js +35 -0
- package/dist/internal/preact/index.d.ts +9 -0
- package/dist/internal/preact/index.js +7 -0
- package/dist/internal/preact/link.d.ts +12 -0
- package/dist/internal/preact/link.js +28 -0
- package/dist/internal/preact/routes.d.ts +22 -0
- package/dist/internal/preact/routes.js +22 -0
- package/dist/internal/preact/runtime.d.ts +2 -0
- package/dist/internal/preact/runtime.js +1 -0
- package/dist/internal/react/context.d.ts +18 -0
- package/dist/internal/react/context.js +20 -0
- package/dist/internal/react/fetcher.d.ts +22 -0
- package/dist/internal/react/fetcher.js +28 -0
- package/dist/internal/react/form.d.ts +17 -0
- package/dist/internal/react/form.js +41 -0
- package/dist/internal/react/hooks.d.ts +21 -0
- package/dist/internal/react/hooks.js +35 -0
- package/dist/internal/react/index.d.ts +9 -0
- package/dist/internal/react/index.js +7 -0
- package/dist/internal/react/link.d.ts +12 -0
- package/dist/internal/react/link.js +28 -0
- package/dist/internal/react/routes.d.ts +22 -0
- package/dist/internal/react/routes.js +22 -0
- package/dist/internal/react/runtime.d.ts +2 -0
- package/dist/internal/react/runtime.js +2 -0
- package/dist/internal/server/index.d.ts +11 -0
- package/dist/internal/server/index.js +81 -0
- package/dist/internal/vite/index.d.ts +10 -0
- package/dist/internal/vite/index.js +132 -0
- package/dist/internal/vite/transport.d.ts +2 -0
- package/dist/internal/vite/transport.js +42 -0
- package/dist/preact.d.ts +2 -0
- package/dist/preact.js +2 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +2 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +2 -0
- package/dist/vite.d.ts +2 -0
- package/dist/vite.js +2 -0
- package/jsr.json +26 -0
- package/package.json +94 -4
- package/src/client.ts +2 -0
- package/src/core.ts +2 -0
- package/src/files.ts +2 -0
- package/src/internal/client/deserialize.ts +15 -0
- package/src/internal/client/index.ts +84 -0
- package/src/internal/core/errors.ts +13 -0
- package/src/internal/core/index.ts +7 -0
- package/src/internal/core/match.ts +45 -0
- package/src/internal/core/router.ts +251 -0
- package/src/internal/core/types.ts +151 -0
- package/src/internal/core/url.ts +6 -0
- package/src/internal/files/index.ts +112 -0
- package/src/internal/preact/context.ts +46 -0
- package/src/internal/preact/fetcher.ts +83 -0
- package/src/internal/preact/form.ts +79 -0
- package/src/internal/preact/hooks.ts +72 -0
- package/src/internal/preact/index.ts +13 -0
- package/src/internal/preact/link.ts +46 -0
- package/src/internal/preact/routes.ts +50 -0
- package/src/internal/preact/runtime.ts +15 -0
- package/src/internal/react/context.ts +46 -0
- package/src/internal/react/fetcher.ts +83 -0
- package/src/internal/react/form.ts +79 -0
- package/src/internal/react/hooks.ts +72 -0
- package/src/internal/react/index.ts +13 -0
- package/src/internal/react/link.ts +46 -0
- package/src/internal/react/routes.ts +50 -0
- package/src/internal/react/runtime.ts +18 -0
- package/src/internal/server/index.ts +120 -0
- package/src/internal/vite/index.ts +208 -0
- package/src/internal/vite/transport.ts +60 -0
- package/src/preact.ts +2 -0
- package/src/react.ts +2 -0
- package/src/server.ts +2 -0
- package/src/vite.ts +2 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { flatRoutes } from "../files/index.ts";
|
|
4
|
+
import { type Plugin, transformWithOxc } from "vite";
|
|
5
|
+
import { configureTransport } from "./transport.ts";
|
|
6
|
+
|
|
7
|
+
export interface FileRouterOptions {
|
|
8
|
+
directory?: string;
|
|
9
|
+
clientRoutes?: string;
|
|
10
|
+
serverRoutes?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Build-time flat route discovery and the optional server data transport. */
|
|
14
|
+
export function routerPlugin({
|
|
15
|
+
directory = "src/routes",
|
|
16
|
+
clientRoutes,
|
|
17
|
+
serverRoutes,
|
|
18
|
+
}: FileRouterOptions = {}): Plugin {
|
|
19
|
+
let root: string;
|
|
20
|
+
let projectRoot: string;
|
|
21
|
+
const publicId = "virtual:smolter/routes";
|
|
22
|
+
const virtualId = `\0${publicId}`;
|
|
23
|
+
const suffix = "?smolter-client";
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
name: "smolter-flat-routes",
|
|
27
|
+
enforce: "pre",
|
|
28
|
+
|
|
29
|
+
configResolved(config) {
|
|
30
|
+
projectRoot = config.root;
|
|
31
|
+
root = resolve(config.root, directory);
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
resolveId(id) {
|
|
35
|
+
if (id === publicId) return virtualId;
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
async load(id, options) {
|
|
39
|
+
if (id.endsWith(suffix)) {
|
|
40
|
+
const file = id.slice(0, -suffix.length);
|
|
41
|
+
const transformed = await transformWithOxc(
|
|
42
|
+
await readFile(file, "utf8"),
|
|
43
|
+
file,
|
|
44
|
+
);
|
|
45
|
+
const ast = this.parse(transformed.code);
|
|
46
|
+
const edits: [number, number][] = [];
|
|
47
|
+
|
|
48
|
+
for (const statement of ast.body) {
|
|
49
|
+
if (statement.type === "ExportAllDeclaration") {
|
|
50
|
+
return this.error(
|
|
51
|
+
`Use explicit named exports in ${file}; export * cannot be safely split between server and browser.`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (statement.type !== "ExportNamedDeclaration") continue;
|
|
56
|
+
|
|
57
|
+
const server = statement.specifiers.filter((specifier) => {
|
|
58
|
+
const name =
|
|
59
|
+
specifier.exported.type === "Identifier"
|
|
60
|
+
? specifier.exported.name
|
|
61
|
+
: specifier.exported.value;
|
|
62
|
+
|
|
63
|
+
return name === "loader" || name === "action";
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (!server.length) {
|
|
67
|
+
const declaration = statement.declaration;
|
|
68
|
+
const name =
|
|
69
|
+
declaration?.type === "FunctionDeclaration"
|
|
70
|
+
? declaration.id?.name
|
|
71
|
+
: undefined;
|
|
72
|
+
const variables =
|
|
73
|
+
declaration?.type === "VariableDeclaration"
|
|
74
|
+
? declaration.declarations
|
|
75
|
+
: [];
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
name === "loader" ||
|
|
79
|
+
name === "action" ||
|
|
80
|
+
variables.some(
|
|
81
|
+
(item) =>
|
|
82
|
+
item.id.type === "Identifier" &&
|
|
83
|
+
["loader", "action"].includes(item.id.name),
|
|
84
|
+
)
|
|
85
|
+
) {
|
|
86
|
+
return this.error(
|
|
87
|
+
`Re-export loader/action from separate files in ${file}; inline server handlers cannot enter a browser module.`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (
|
|
95
|
+
!statement.source ||
|
|
96
|
+
server.length !== statement.specifiers.length
|
|
97
|
+
) {
|
|
98
|
+
return this.error(
|
|
99
|
+
`Use separate export { loader } / export { action } statements in ${file}.`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
edits.push([statement.start, statement.end]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let code = transformed.code;
|
|
107
|
+
|
|
108
|
+
for (const [start, end] of edits.reverse())
|
|
109
|
+
code = code.slice(0, start) + code.slice(end);
|
|
110
|
+
|
|
111
|
+
return { code, map: null };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (id !== virtualId) return;
|
|
115
|
+
|
|
116
|
+
if (clientRoutes || serverRoutes) {
|
|
117
|
+
if (!clientRoutes || !serverRoutes)
|
|
118
|
+
return this.error(
|
|
119
|
+
"Specify both clientRoutes and serverRoutes for manual routing.",
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const entry = resolve(
|
|
123
|
+
projectRoot,
|
|
124
|
+
options?.ssr ? serverRoutes : clientRoutes,
|
|
125
|
+
).replaceAll("\\", "/");
|
|
126
|
+
|
|
127
|
+
return `export { routes as default } from ${JSON.stringify(entry)};`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const routes = await flatRoutes(root);
|
|
131
|
+
|
|
132
|
+
if (routes instanceof Error) return this.error(routes.message);
|
|
133
|
+
|
|
134
|
+
const imports: string[] = [];
|
|
135
|
+
const definitions: string[] = [];
|
|
136
|
+
|
|
137
|
+
for (const [index, route] of routes.entries()) {
|
|
138
|
+
this.addWatchFile(route.file);
|
|
139
|
+
|
|
140
|
+
const file = route.file.replaceAll("\\", "/");
|
|
141
|
+
const transformed = await transformWithOxc(
|
|
142
|
+
await readFile(file, "utf8"),
|
|
143
|
+
file,
|
|
144
|
+
);
|
|
145
|
+
const ast = this.parse(transformed.code);
|
|
146
|
+
const handlers: Record<string, boolean> = {};
|
|
147
|
+
|
|
148
|
+
for (const statement of ast.body) {
|
|
149
|
+
if (statement.type !== "ExportNamedDeclaration") continue;
|
|
150
|
+
|
|
151
|
+
for (const specifier of statement.specifiers) {
|
|
152
|
+
const name =
|
|
153
|
+
specifier.exported.type === "Identifier"
|
|
154
|
+
? specifier.exported.name
|
|
155
|
+
: specifier.exported.value;
|
|
156
|
+
|
|
157
|
+
if (name === "loader" || name === "action") handlers[name] = true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
imports.push(
|
|
162
|
+
`import * as route${index} from ${JSON.stringify(file + (options?.ssr ? "" : suffix))};`,
|
|
163
|
+
);
|
|
164
|
+
definitions.push(
|
|
165
|
+
options?.ssr || !Object.keys(handlers).length
|
|
166
|
+
? `{ ...route${index}, path: ${JSON.stringify(route.path)} }`
|
|
167
|
+
: `serverRoute(${JSON.stringify(route.path)}, route${index}, ${JSON.stringify(handlers)})`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (
|
|
172
|
+
!options?.ssr &&
|
|
173
|
+
definitions.some((definition) => definition.startsWith("serverRoute("))
|
|
174
|
+
)
|
|
175
|
+
imports.unshift('import { serverRoute } from "smolter/client";');
|
|
176
|
+
|
|
177
|
+
return `${imports.join("\n")}\nexport default [${definitions.join(",\n")}];\n`;
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
configureServer(server) {
|
|
181
|
+
server.watcher.add(root);
|
|
182
|
+
|
|
183
|
+
const refresh = (file: string) => {
|
|
184
|
+
if (!file.startsWith(`${root}/`)) return;
|
|
185
|
+
|
|
186
|
+
const module = server.moduleGraph.getModuleById(virtualId);
|
|
187
|
+
|
|
188
|
+
if (module) server.moduleGraph.invalidateModule(module);
|
|
189
|
+
|
|
190
|
+
server.ws.send({ type: "full-reload" });
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
server.watcher.on("add", refresh);
|
|
194
|
+
server.watcher.on("change", refresh);
|
|
195
|
+
server.watcher.on("unlink", refresh);
|
|
196
|
+
server.httpServer?.once("close", () => {
|
|
197
|
+
server.watcher.off("add", refresh);
|
|
198
|
+
server.watcher.off("change", refresh);
|
|
199
|
+
server.watcher.off("unlink", refresh);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
configureTransport(server, publicId);
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** File routing is the default; this alias keeps configuration explicit. */
|
|
208
|
+
export const fileRouter = routerPlugin;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ViteDevServer } from "vite";
|
|
2
|
+
|
|
3
|
+
export function configureTransport(
|
|
4
|
+
server: ViteDevServer,
|
|
5
|
+
publicId: string,
|
|
6
|
+
): void {
|
|
7
|
+
server.middlewares.use((incoming, outgoing, next) => {
|
|
8
|
+
if (!incoming.url?.startsWith("/_smolter?")) return next();
|
|
9
|
+
|
|
10
|
+
const dispatch = async () => {
|
|
11
|
+
const origin = `http://${incoming.headers.host}`;
|
|
12
|
+
const target = new URL(incoming.url!, origin).searchParams.get("url");
|
|
13
|
+
|
|
14
|
+
if (!target?.startsWith("/") || target.startsWith("//")) {
|
|
15
|
+
outgoing.statusCode = 400;
|
|
16
|
+
outgoing.end("Invalid route URL");
|
|
17
|
+
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const headers = new Headers();
|
|
22
|
+
|
|
23
|
+
for (const [name, value] of Object.entries(incoming.headers)) {
|
|
24
|
+
if (value)
|
|
25
|
+
headers.set(name, Array.isArray(value) ? value.join(", ") : value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const chunks: Uint8Array[] = [];
|
|
29
|
+
|
|
30
|
+
for await (const chunk of incoming) chunks.push(chunk);
|
|
31
|
+
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
|
|
34
|
+
outgoing.once("close", () => controller.abort());
|
|
35
|
+
|
|
36
|
+
const request = new Request(new URL(target, origin), {
|
|
37
|
+
signal: controller.signal,
|
|
38
|
+
method: incoming.method,
|
|
39
|
+
headers,
|
|
40
|
+
...(incoming.method !== "GET" && {
|
|
41
|
+
body: new Uint8Array(Buffer.concat(chunks)),
|
|
42
|
+
}),
|
|
43
|
+
});
|
|
44
|
+
const [{ default: routes }, { handleRequest }] = await Promise.all([
|
|
45
|
+
server.ssrLoadModule(publicId),
|
|
46
|
+
server.ssrLoadModule("smolter/server"),
|
|
47
|
+
]);
|
|
48
|
+
const response: Response = await handleRequest(routes, request);
|
|
49
|
+
|
|
50
|
+
outgoing.statusCode = response.status;
|
|
51
|
+
|
|
52
|
+
response.headers.forEach((value, name) =>
|
|
53
|
+
outgoing.setHeader(name, value),
|
|
54
|
+
);
|
|
55
|
+
outgoing.end(new Uint8Array(await response.arrayBuffer()));
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
void dispatch().catch(next);
|
|
59
|
+
});
|
|
60
|
+
}
|
package/src/preact.ts
ADDED
package/src/react.ts
ADDED
package/src/server.ts
ADDED
package/src/vite.ts
ADDED