hono-preact 0.1.0 → 0.2.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/README.md +2 -1
- package/dist/adapter-cloudflare.d.ts +1 -0
- package/dist/adapter-cloudflare.d.ts.map +1 -0
- package/dist/adapter-cloudflare.js +2 -0
- package/dist/adapter-node.d.ts +1 -0
- package/dist/adapter-node.d.ts.map +1 -0
- package/dist/adapter-node.js +2 -0
- package/dist/internal.d.ts +1 -1
- package/dist/internal.js +1 -1
- package/dist/iso/action.d.ts +10 -14
- package/dist/iso/action.js +57 -21
- package/dist/iso/define-app.d.ts +7 -0
- package/dist/iso/define-app.js +3 -0
- package/dist/iso/define-loader.d.ts +19 -0
- package/dist/iso/define-loader.js +4 -0
- package/dist/iso/define-middleware.d.ts +43 -0
- package/dist/iso/define-middleware.js +6 -0
- package/dist/iso/define-page.d.ts +7 -2
- package/dist/iso/define-page.js +1 -1
- package/dist/iso/define-routes.d.ts +24 -1
- package/dist/iso/define-routes.js +34 -0
- package/dist/iso/define-stream-observer.d.ts +20 -0
- package/dist/iso/define-stream-observer.js +3 -0
- package/dist/iso/index.d.ts +10 -5
- package/dist/iso/index.js +5 -3
- package/dist/iso/internal/contexts.d.ts +0 -2
- package/dist/iso/internal/contexts.js +0 -1
- package/dist/iso/internal/loader-fetch.js +37 -7
- package/dist/iso/internal/loader-runner.js +105 -8
- package/dist/iso/internal/middleware-runner.d.ts +22 -0
- package/dist/iso/internal/middleware-runner.js +79 -0
- package/dist/iso/internal/page-middleware-host.d.ts +13 -0
- package/dist/iso/internal/page-middleware-host.js +119 -0
- package/dist/iso/internal/route-boundary.d.ts +1 -0
- package/dist/iso/internal/route-boundary.js +16 -0
- package/dist/iso/internal/stream-observer-runner.d.ts +13 -0
- package/dist/iso/internal/stream-observer-runner.js +48 -0
- package/dist/iso/internal/use-partitioner.d.ts +9 -0
- package/dist/iso/internal/use-partitioner.js +11 -0
- package/dist/iso/internal/use-types.d.ts +7 -0
- package/dist/iso/internal/use-types.js +1 -0
- package/dist/iso/internal.d.ts +5 -4
- package/dist/iso/internal.js +8 -6
- package/dist/iso/outcomes.d.ts +38 -0
- package/dist/iso/outcomes.js +56 -0
- package/dist/iso/page-only.d.ts +5 -0
- package/dist/iso/page-only.js +20 -0
- package/dist/iso/page.d.ts +3 -3
- package/dist/iso/page.js +3 -3
- package/dist/page.d.ts +1 -0
- package/dist/page.d.ts.map +1 -0
- package/dist/page.js +8 -0
- package/dist/server/actions-handler.d.ts +20 -6
- package/dist/server/actions-handler.js +83 -47
- package/dist/server/context.js +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/loaders-handler.d.ts +16 -0
- package/dist/server/loaders-handler.js +94 -17
- package/dist/server/render.d.ts +2 -0
- package/dist/server/render.js +104 -33
- package/dist/server/route-server-modules.d.ts +42 -1
- package/dist/server/route-server-modules.js +184 -0
- package/dist/server/sse.d.ts +24 -1
- package/dist/server/sse.js +56 -4
- package/dist/vite/adapter-cloudflare.d.ts +2 -0
- package/dist/vite/adapter-cloudflare.js +25 -0
- package/dist/vite/adapter-node.d.ts +2 -0
- package/dist/vite/adapter-node.js +49 -0
- package/dist/vite/adapter.d.ts +29 -0
- package/dist/vite/adapter.js +1 -0
- package/dist/vite/client-shim.js +5 -4
- package/dist/vite/guard-strip.js +52 -27
- package/dist/vite/hono-preact.d.ts +6 -6
- package/dist/vite/hono-preact.js +48 -77
- package/dist/vite/index.d.ts +2 -1
- package/dist/vite/index.js +1 -1
- package/dist/vite/node-dev-server.d.ts +4 -0
- package/dist/vite/node-dev-server.js +121 -0
- package/dist/vite/server-entry.d.ts +30 -7
- package/dist/vite/server-entry.js +161 -78
- package/dist/vite/server-exports-contract.d.ts +6 -0
- package/dist/vite/server-exports-contract.js +43 -0
- package/dist/vite/server-loader-validation.js +36 -9
- package/dist/vite/server-loaders-parser.d.ts +17 -1
- package/dist/vite/server-loaders-parser.js +41 -0
- package/dist/vite/server-only.js +20 -2
- package/package.json +32 -4
|
@@ -2,10 +2,18 @@ import * as path from 'node:path';
|
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import { parse } from '@babel/parser';
|
|
4
4
|
import { BABEL_PARSER_PLUGINS } from './parser-options.js';
|
|
5
|
-
export function
|
|
6
|
-
const { layoutAbsPath, routesAbsPath, apiAbsPath } = opts;
|
|
5
|
+
export function generateCoreAppModule(opts) {
|
|
6
|
+
const { layoutAbsPath, routesAbsPath, apiAbsPath, appConfigAbsPath } = opts;
|
|
7
7
|
const apiImport = apiAbsPath ? `import userApp from '${apiAbsPath}';\n` : '';
|
|
8
8
|
const apiMount = apiAbsPath ? ` .route('/', userApp)\n` : '';
|
|
9
|
+
// appConfig is optional: when no app-config.ts file exists, fall back to
|
|
10
|
+
// an empty config so the handler chain composition (root -> page -> unit)
|
|
11
|
+
// still works without the user authoring anything. The default-export
|
|
12
|
+
// shape mirrors the `import appConfig from './app-config'` convention so
|
|
13
|
+
// consumers can adopt the file later without other entry changes.
|
|
14
|
+
const appConfigImport = appConfigAbsPath
|
|
15
|
+
? `import appConfig from '${appConfigAbsPath}';\n`
|
|
16
|
+
: `const appConfig = { use: [] };\n`;
|
|
9
17
|
// The generated source is loaded as a virtual module, which Vite/esbuild
|
|
10
18
|
// treats as plain JS by default. Use h() to construct vnodes rather than
|
|
11
19
|
// JSX so the source compiles without a TSX loader hint.
|
|
@@ -16,25 +24,32 @@ export function generateServerEntrySource(opts) {
|
|
|
16
24
|
`import {\n` +
|
|
17
25
|
` actionsHandler,\n` +
|
|
18
26
|
` loadersHandler,\n` +
|
|
27
|
+
` makePageUseResolvers,\n` +
|
|
19
28
|
` renderPage,\n` +
|
|
20
29
|
` routeServerModules,\n` +
|
|
21
30
|
`} from 'hono-preact/server';\n` +
|
|
22
31
|
`import Layout from '${layoutAbsPath}';\n` +
|
|
23
32
|
`import routes from '${routesAbsPath}';\n` +
|
|
24
33
|
apiImport +
|
|
34
|
+
appConfigImport +
|
|
25
35
|
`\n` +
|
|
26
36
|
`env.current = 'server';\n` +
|
|
37
|
+
`const dev = import.meta.env.DEV;\n` +
|
|
27
38
|
`const serverModules = routeServerModules(routes);\n` +
|
|
28
|
-
`const
|
|
39
|
+
`const pageUseResolvers = makePageUseResolvers(routes.serverRoutes, { dev });\n` +
|
|
29
40
|
`\n` +
|
|
30
41
|
`export const app = new Hono()\n` +
|
|
31
|
-
` .post('/__loaders', loadersHandler(serverModules, handlerOpts))\n` +
|
|
32
|
-
` .post('/__actions', actionsHandler(serverModules, handlerOpts))\n` +
|
|
33
42
|
apiMount +
|
|
34
|
-
` .
|
|
43
|
+
` .post('/__loaders', loadersHandler(serverModules, { dev, appConfig, resolvePageUse: pageUseResolvers.byPath }))\n` +
|
|
44
|
+
` .post('/__actions', actionsHandler(serverModules, { dev, appConfig, resolvePageUse: pageUseResolvers.byModuleKey }))\n` +
|
|
45
|
+
` .get('*', (c) => renderPage(c, h(Layout, null, h(LocationProvider, null, h(Routes, { routes }))), { appConfig }));\n` +
|
|
35
46
|
`\n` +
|
|
36
47
|
`export default app;\n`);
|
|
37
48
|
}
|
|
49
|
+
// Framework-reserved request paths. A literal registration of either in
|
|
50
|
+
// api.ts shadows the framework's RPC handler now that the user app mounts
|
|
51
|
+
// ahead of them.
|
|
52
|
+
const RESERVED_PATHS = new Set(['/__loaders', '/__actions']);
|
|
38
53
|
const HONO_METHODS = new Set([
|
|
39
54
|
'get',
|
|
40
55
|
'post',
|
|
@@ -57,8 +72,8 @@ const FUNCTION_BODY_PARENTS = new Set([
|
|
|
57
72
|
'ObjectMethod',
|
|
58
73
|
'ClassMethod',
|
|
59
74
|
]);
|
|
60
|
-
export function
|
|
61
|
-
const
|
|
75
|
+
export function findApiShadowingRoutes(source) {
|
|
76
|
+
const found = [];
|
|
62
77
|
let ast;
|
|
63
78
|
try {
|
|
64
79
|
ast = parse(source, {
|
|
@@ -70,23 +85,23 @@ export function findApiCatchAllRoutes(source) {
|
|
|
70
85
|
catch (err) {
|
|
71
86
|
// If api.ts won't parse, the build will fail elsewhere with a clearer
|
|
72
87
|
// error. Surface a note so the framework user can correlate a missing
|
|
73
|
-
//
|
|
88
|
+
// shadowing warning with a parse-time syntax issue rather than wondering
|
|
74
89
|
// why nothing was reported.
|
|
75
90
|
const msg = err instanceof Error ? err.message : String(err);
|
|
76
|
-
console.warn(`[hono-preact] Failed to parse api.ts for
|
|
91
|
+
console.warn(`[hono-preact] Failed to parse api.ts for shadowing-route detection: ${msg}. ` +
|
|
77
92
|
`The build will surface the real syntax error; this warning explains why ` +
|
|
78
|
-
`route-overlap
|
|
79
|
-
return
|
|
93
|
+
`route-overlap diagnostics may be missing.`);
|
|
94
|
+
return found;
|
|
80
95
|
}
|
|
81
|
-
walk(ast.program,
|
|
82
|
-
return
|
|
96
|
+
walk(ast.program, found);
|
|
97
|
+
return found;
|
|
83
98
|
}
|
|
84
|
-
function walk(node,
|
|
99
|
+
function walk(node, found) {
|
|
85
100
|
if (!node || typeof node !== 'object')
|
|
86
101
|
return;
|
|
87
102
|
if (Array.isArray(node)) {
|
|
88
103
|
for (const child of node)
|
|
89
|
-
walk(child,
|
|
104
|
+
walk(child, found);
|
|
90
105
|
return;
|
|
91
106
|
}
|
|
92
107
|
const n = node;
|
|
@@ -97,19 +112,32 @@ function walk(node, warnings) {
|
|
|
97
112
|
const method = n.callee.property.name;
|
|
98
113
|
const line = n.loc?.start?.line;
|
|
99
114
|
if (method === 'notFound') {
|
|
100
|
-
|
|
115
|
+
found.push({ kind: 'notFound', line, severity: 'warning' });
|
|
101
116
|
}
|
|
102
117
|
else if (HONO_METHODS.has(method)) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
118
|
+
// `app.on(method, path, ...)` puts the path at argument index 1;
|
|
119
|
+
// every other Hono routing method takes the path as argument 0.
|
|
120
|
+
const pathArg = n.arguments?.[method === 'on' ? 1 : 0];
|
|
121
|
+
if (pathArg?.type === 'StringLiteral' &&
|
|
122
|
+
typeof pathArg.value === 'string') {
|
|
123
|
+
if (WILDCARD_PATTERNS.has(pathArg.value)) {
|
|
124
|
+
found.push({
|
|
125
|
+
kind: 'wildcard',
|
|
126
|
+
method,
|
|
127
|
+
pattern: pathArg.value,
|
|
128
|
+
line,
|
|
129
|
+
severity: 'error',
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
else if (RESERVED_PATHS.has(pathArg.value)) {
|
|
133
|
+
found.push({
|
|
134
|
+
kind: 'reserved',
|
|
135
|
+
method,
|
|
136
|
+
pattern: pathArg.value,
|
|
137
|
+
line,
|
|
138
|
+
severity: 'error',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
113
141
|
}
|
|
114
142
|
}
|
|
115
143
|
}
|
|
@@ -121,81 +149,136 @@ function walk(node, warnings) {
|
|
|
121
149
|
continue;
|
|
122
150
|
if (isFunctionParent && key === 'body')
|
|
123
151
|
continue;
|
|
124
|
-
walk(node[key],
|
|
152
|
+
walk(node[key], found);
|
|
125
153
|
}
|
|
126
154
|
}
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
export
|
|
141
|
-
|
|
155
|
+
// Both generated files live in the Vite cache dir. The wrapper keeps the
|
|
156
|
+
// `server-entry.tsx` name because that is the file the adapter's build/dev
|
|
157
|
+
// plugins (and wrangler.jsonc `main`) point at; the core app module is a
|
|
158
|
+
// separate file the wrapper imports.
|
|
159
|
+
export const GENERATED_CORE_APP_RELATIVE = 'node_modules/.vite/hono-preact/core-app.tsx';
|
|
160
|
+
export const GENERATED_ENTRY_WRAPPER_RELATIVE = 'node_modules/.vite/hono-preact/server-entry.tsx';
|
|
161
|
+
export function generatedCoreAppAbsPath(cwd = process.cwd()) {
|
|
162
|
+
return path.resolve(cwd, GENERATED_CORE_APP_RELATIVE);
|
|
163
|
+
}
|
|
164
|
+
export function generatedEntryWrapperAbsPath(cwd = process.cwd()) {
|
|
165
|
+
return path.resolve(cwd, GENERATED_ENTRY_WRAPPER_RELATIVE);
|
|
166
|
+
}
|
|
167
|
+
// Returns true if the parsed program contains a top-level
|
|
168
|
+
// `export default ...`. The app-config diagnostic uses this to detect a
|
|
169
|
+
// common mistake: writing `export const appConfig = defineApp(...)`
|
|
170
|
+
// instead of `export default defineApp(...)`. Without a default the
|
|
171
|
+
// generated `import appConfig from '...'` binds to undefined and the
|
|
172
|
+
// app-level middleware chain silently never runs.
|
|
173
|
+
function hasDefaultExport(source) {
|
|
174
|
+
let ast;
|
|
175
|
+
try {
|
|
176
|
+
ast = parse(source, {
|
|
177
|
+
sourceType: 'module',
|
|
178
|
+
plugins: BABEL_PARSER_PLUGINS,
|
|
179
|
+
errorRecovery: true,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// Fall back to "true" on parse failure so we don't pile a misleading
|
|
184
|
+
// app-config error on top of an obvious syntax error elsewhere in the
|
|
185
|
+
// file. The real parse error surfaces from the Vite build itself.
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
for (const node of ast.program.body) {
|
|
189
|
+
if (node.type === 'ExportDefaultDeclaration')
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
142
193
|
}
|
|
143
194
|
export function serverEntryPlugin(opts) {
|
|
144
|
-
// Paths resolved during configResolved (cheap) — actual disk write
|
|
145
|
-
// happens in buildStart so config-only Vite invocations (IDE probes,
|
|
146
|
-
// typecheck-only runs, dependency-optimizer cold runs) don't side-effect
|
|
147
|
-
// the cache directory. Writing on every config resolution was a tax that
|
|
148
|
-
// showed up when integration tests loaded vite.config.ts to inspect the
|
|
149
|
-
// plugin chain.
|
|
150
|
-
let layoutAbsPath;
|
|
151
|
-
let routesAbsPath;
|
|
152
195
|
let apiAbsPath;
|
|
196
|
+
let appConfigAbsPath;
|
|
153
197
|
return {
|
|
154
198
|
name: 'hono-preact:server-entry',
|
|
155
199
|
enforce: 'pre',
|
|
156
|
-
|
|
157
|
-
|
|
200
|
+
// Write generated files in `config` -- the earliest hook -- so the entry
|
|
201
|
+
// wrapper exists before @cloudflare/vite-plugin's own `config` hook does
|
|
202
|
+
// fs.existsSync on wrangler.jsonc `main`.
|
|
203
|
+
config(userConfig) {
|
|
204
|
+
const root = userConfig.root
|
|
205
|
+
? path.resolve(userConfig.root)
|
|
206
|
+
: process.cwd();
|
|
207
|
+
const layoutAbsPath = path.isAbsolute(opts.layout)
|
|
158
208
|
? opts.layout
|
|
159
|
-
: path.resolve(
|
|
160
|
-
routesAbsPath = path.isAbsolute(opts.routes)
|
|
209
|
+
: path.resolve(root, opts.layout);
|
|
210
|
+
const routesAbsPath = path.isAbsolute(opts.routes)
|
|
161
211
|
? opts.routes
|
|
162
|
-
: path.resolve(
|
|
212
|
+
: path.resolve(root, opts.routes);
|
|
163
213
|
const candidateApi = path.isAbsolute(opts.api)
|
|
164
214
|
? opts.api
|
|
165
|
-
: path.resolve(
|
|
215
|
+
: path.resolve(root, opts.api);
|
|
166
216
|
apiAbsPath = fs.existsSync(candidateApi) ? candidateApi : undefined;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
const source = generateServerEntrySource({
|
|
217
|
+
const candidateAppConfig = path.isAbsolute(opts.appConfig)
|
|
218
|
+
? opts.appConfig
|
|
219
|
+
: path.resolve(root, opts.appConfig);
|
|
220
|
+
appConfigAbsPath = fs.existsSync(candidateAppConfig)
|
|
221
|
+
? candidateAppConfig
|
|
222
|
+
: undefined;
|
|
223
|
+
const source = generateCoreAppModule({
|
|
176
224
|
layoutAbsPath,
|
|
177
225
|
routesAbsPath,
|
|
178
226
|
apiAbsPath,
|
|
227
|
+
appConfigAbsPath,
|
|
228
|
+
});
|
|
229
|
+
fs.mkdirSync(path.dirname(opts.coreAppPath), { recursive: true });
|
|
230
|
+
fs.writeFileSync(opts.coreAppPath, source, 'utf8');
|
|
231
|
+
const wrapper = opts.adapter.wrapEntry({
|
|
232
|
+
root,
|
|
233
|
+
coreAppModuleId: opts.coreAppPath,
|
|
234
|
+
entryWrapperId: opts.entryWrapperPath,
|
|
235
|
+
apiModuleId: apiAbsPath,
|
|
179
236
|
});
|
|
180
|
-
fs.
|
|
181
|
-
|
|
237
|
+
fs.writeFileSync(opts.entryWrapperPath, wrapper, 'utf8');
|
|
238
|
+
},
|
|
239
|
+
buildStart() {
|
|
240
|
+
// The api.ts shadowing diagnostic stays in buildStart: it needs
|
|
241
|
+
// this.warn / this.error, which the `config` hook context lacks.
|
|
242
|
+
// The app-config default-export diagnostic lives here for the same
|
|
243
|
+
// reason.
|
|
244
|
+
if (appConfigAbsPath) {
|
|
245
|
+
const appConfigSource = fs.readFileSync(appConfigAbsPath, 'utf8');
|
|
246
|
+
if (!hasDefaultExport(appConfigSource)) {
|
|
247
|
+
this.error(`[hono-preact] ${appConfigAbsPath}: app-config.ts must default-export ` +
|
|
248
|
+
`the result of defineApp(...) (e.g. ` +
|
|
249
|
+
`\`export default defineApp({ use: [...] })\`). The generated entry ` +
|
|
250
|
+
`does \`import appConfig from '...'\`; without a default export the ` +
|
|
251
|
+
`import resolves to undefined and the app-level middleware chain ` +
|
|
252
|
+
`silently never runs.`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
182
255
|
if (!apiAbsPath)
|
|
183
256
|
return;
|
|
184
257
|
const apiSource = fs.readFileSync(apiAbsPath, 'utf8');
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
`
|
|
258
|
+
const shadowing = findApiShadowingRoutes(apiSource);
|
|
259
|
+
const errors = [];
|
|
260
|
+
for (const r of shadowing) {
|
|
261
|
+
const where = `${apiAbsPath}${r.line != null ? `:${r.line}` : ''}`;
|
|
262
|
+
if (r.kind === 'notFound') {
|
|
263
|
+
this.warn(`[hono-preact] ${where}: app.notFound(...) will not fire — the ` +
|
|
264
|
+
`framework's renderPage handler matches every unmatched request. ` +
|
|
265
|
+
`Move the behavior to a specific path, or accept that it won't fire.`);
|
|
266
|
+
}
|
|
267
|
+
else if (r.kind === 'wildcard') {
|
|
268
|
+
errors.push(`${where}: app.${r.method}('${r.pattern}', ...) is a catch-all route`);
|
|
192
269
|
}
|
|
193
270
|
else {
|
|
194
|
-
|
|
195
|
-
`
|
|
196
|
-
`handler. Move it to a more specific path, or accept that it won't fire.`);
|
|
271
|
+
errors.push(`${where}: app.${r.method}('${r.pattern}', ...) registers the ` +
|
|
272
|
+
`framework-reserved path '${r.pattern}'`);
|
|
197
273
|
}
|
|
198
274
|
}
|
|
275
|
+
if (errors.length > 0) {
|
|
276
|
+
this.error(`[hono-preact] api.ts registers routes that shadow framework handlers:\n` +
|
|
277
|
+
errors.map((e) => ` - ${e}`).join('\n') +
|
|
278
|
+
`\nThe framework mounts your app ahead of its reserved paths ` +
|
|
279
|
+
`(/__loaders, /__actions) and the SSR handler, so these routes break ` +
|
|
280
|
+
`loaders/actions and/or page rendering. Use specific, non-wildcard paths.`);
|
|
281
|
+
}
|
|
199
282
|
},
|
|
200
283
|
};
|
|
201
284
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const RECOGNIZED_SERVER_EXPORTS: readonly ["serverActions", "serverLoaders", "pageUse", "loaderUse", "actionUse"];
|
|
2
|
+
export type RecognizedServerExport = (typeof RECOGNIZED_SERVER_EXPORTS)[number];
|
|
3
|
+
export declare const RECOGNIZED_SERVER_EXPORTS_SET: ReadonlySet<string>;
|
|
4
|
+
export declare const RECOGNIZED_USE_EXPORTS: readonly ["pageUse", "loaderUse", "actionUse"];
|
|
5
|
+
export type RecognizedUseExport = (typeof RECOGNIZED_USE_EXPORTS)[number];
|
|
6
|
+
export declare const RECOGNIZED_USE_EXPORTS_SET: ReadonlySet<string>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Single source of truth for the recognized named exports a `.server.*`
|
|
2
|
+
// file may declare. Three plugins enforce this contract and used to keep
|
|
3
|
+
// independent copies of the list, which drifted silently when a new name
|
|
4
|
+
// was added:
|
|
5
|
+
//
|
|
6
|
+
// - `server-only.ts` rejects unknown specifiers when client code imports
|
|
7
|
+
// from a `.server.*` module.
|
|
8
|
+
// - `server-loader-validation.ts` runs at build time on the `.server.*`
|
|
9
|
+
// file itself and rejects unknown top-level named exports.
|
|
10
|
+
// - `server-loaders-parser.ts` documents the middleware-carrying subset
|
|
11
|
+
// (`pageUse`/`loaderUse`/`actionUse`) used by the route map builder.
|
|
12
|
+
//
|
|
13
|
+
// Centralizing the list here forces all three to agree by import.
|
|
14
|
+
//
|
|
15
|
+
// Status of each entry:
|
|
16
|
+
// - `serverLoaders` / `serverActions`: the two value-bearing exports the
|
|
17
|
+
// handlers read at runtime.
|
|
18
|
+
// - `pageUse`: page-layer middleware chain composed into the resolver map
|
|
19
|
+
// in `route-server-modules.ts`. Load-bearing.
|
|
20
|
+
// - `loaderUse` / `actionUse`: reserved names for future per-file
|
|
21
|
+
// middleware. The handlers do not yet read them; per-unit middleware
|
|
22
|
+
// rides on `defineLoader({ use })` / `defineAction({ use })`. Plan
|
|
23
|
+
// item E7 may drop them in a follow-up; until then they're recognized
|
|
24
|
+
// so users who write them get an array stub on the client and a
|
|
25
|
+
// passing build instead of an opaque "unknown export" error.
|
|
26
|
+
export const RECOGNIZED_SERVER_EXPORTS = [
|
|
27
|
+
'serverActions',
|
|
28
|
+
'serverLoaders',
|
|
29
|
+
'pageUse',
|
|
30
|
+
'loaderUse',
|
|
31
|
+
'actionUse',
|
|
32
|
+
];
|
|
33
|
+
export const RECOGNIZED_SERVER_EXPORTS_SET = new Set(RECOGNIZED_SERVER_EXPORTS);
|
|
34
|
+
// Subset of RECOGNIZED_SERVER_EXPORTS that names a middleware-carrying
|
|
35
|
+
// array. Used by the validation plugin to enforce that values are
|
|
36
|
+
// `ArrayExpression` literals (so a typo like `pageUse = singleMw` fails
|
|
37
|
+
// the build instead of silently disabling the gate at runtime).
|
|
38
|
+
export const RECOGNIZED_USE_EXPORTS = [
|
|
39
|
+
'pageUse',
|
|
40
|
+
'loaderUse',
|
|
41
|
+
'actionUse',
|
|
42
|
+
];
|
|
43
|
+
export const RECOGNIZED_USE_EXPORTS_SET = new Set(RECOGNIZED_USE_EXPORTS);
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import { parse } from '@babel/parser';
|
|
2
2
|
import { BABEL_PARSER_PLUGINS } from './parser-options.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
'serverLoaders',
|
|
7
|
-
]);
|
|
8
|
-
const ALLOWED_NAMED_EXPORTS_LIST = [...ALLOWED_NAMED_EXPORTS]
|
|
9
|
-
.map((n) => `'${n}'`)
|
|
10
|
-
.join(', ');
|
|
3
|
+
import { RECOGNIZED_SERVER_EXPORTS, RECOGNIZED_SERVER_EXPORTS_SET, } from './server-exports-contract.js';
|
|
4
|
+
import { findUseExports } from './server-loaders-parser.js';
|
|
5
|
+
const ALLOWED_NAMED_EXPORTS_LIST = RECOGNIZED_SERVER_EXPORTS.map((n) => `'${n}'`).join(', ');
|
|
11
6
|
export function serverLoaderValidationPlugin() {
|
|
12
7
|
return {
|
|
13
8
|
name: 'server-loader-validation',
|
|
@@ -51,7 +46,39 @@ export function serverLoaderValidationPlugin() {
|
|
|
51
46
|
}
|
|
52
47
|
}
|
|
53
48
|
}
|
|
54
|
-
|
|
49
|
+
// F3: pageUse / loaderUse / actionUse must resolve to an array at
|
|
50
|
+
// runtime so the route-map builder and dispatcher receive a real
|
|
51
|
+
// ReadonlyArray. We can't statically prove that an identifier (e.g.
|
|
52
|
+
// `pageUse = requireSession` re-exporting an array from another
|
|
53
|
+
// module) holds an array, so we reject only the obviously-wrong
|
|
54
|
+
// literal shapes here and let the runtime guard in
|
|
55
|
+
// `makePageUseResolvers` catch indirect non-array values at first
|
|
56
|
+
// request. The literal denylist still catches the canonical typo
|
|
57
|
+
// case (`pageUse = singleMwObject`). findUseExports is shared with
|
|
58
|
+
// server-loaders-parser so the recognized-name list stays in one
|
|
59
|
+
// place.
|
|
60
|
+
const REJECTED_LITERAL_TYPES = new Set([
|
|
61
|
+
'ObjectExpression',
|
|
62
|
+
'NumericLiteral',
|
|
63
|
+
'StringLiteral',
|
|
64
|
+
'BooleanLiteral',
|
|
65
|
+
'NullLiteral',
|
|
66
|
+
'RegExpLiteral',
|
|
67
|
+
'TemplateLiteral',
|
|
68
|
+
'BigIntLiteral',
|
|
69
|
+
]);
|
|
70
|
+
for (const useExport of findUseExports(ast.program)) {
|
|
71
|
+
if (useExport.init == null)
|
|
72
|
+
continue;
|
|
73
|
+
if (REJECTED_LITERAL_TYPES.has(useExport.init.type)) {
|
|
74
|
+
errors.push(`${id}: \`${useExport.name}\` must be an array literal or an ` +
|
|
75
|
+
`identifier that points to an array ` +
|
|
76
|
+
`(e.g. \`export const ${useExport.name} = [requireAuth]\` or ` +
|
|
77
|
+
`\`export const ${useExport.name} = requireSession\`). ` +
|
|
78
|
+
`A non-array value silently disables the middleware at runtime.`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const disallowedExports = namedExports.filter((n) => !RECOGNIZED_SERVER_EXPORTS_SET.has(n));
|
|
55
82
|
if (disallowedExports.length > 0) {
|
|
56
83
|
errors.push(`${id}: .server files may only export ${ALLOWED_NAMED_EXPORTS_LIST} as named exports (found: ${disallowedExports.join(', ')}). ` +
|
|
57
84
|
`Export the server loader as the default export only.`);
|
|
@@ -1,4 +1,20 @@
|
|
|
1
|
-
import type { Program, CallExpression, ObjectExpression } from '@babel/types';
|
|
1
|
+
import type { Program, CallExpression, ObjectExpression, Expression } from '@babel/types';
|
|
2
|
+
export declare const RECOGNIZED_USE_EXPORTS: ReadonlySet<string>;
|
|
3
|
+
export declare function hasNamedUseExport(program: Program, name: string): boolean;
|
|
4
|
+
export type ParsedUseExport = {
|
|
5
|
+
/** The export name -- one of pageUse / loaderUse / actionUse. */
|
|
6
|
+
name: string;
|
|
7
|
+
/** The initializer expression, or null if `export const foo;` with no init. */
|
|
8
|
+
init: Expression | null;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Walk a parsed program for top-level `export const <use> = ...` declarations
|
|
12
|
+
* where `<use>` is one of the recognized middleware-carrying names
|
|
13
|
+
* (`pageUse` / `loaderUse` / `actionUse`). Returns each one with its
|
|
14
|
+
* initializer expression so callers can validate the shape (e.g. require
|
|
15
|
+
* an ArrayExpression literal).
|
|
16
|
+
*/
|
|
17
|
+
export declare function findUseExports(program: Program): ParsedUseExport[];
|
|
2
18
|
export type ParsedLoaderEntry = {
|
|
3
19
|
/** Loader name (the key in serverLoaders, e.g. "summary"). */
|
|
4
20
|
name: string;
|
|
@@ -1,3 +1,44 @@
|
|
|
1
|
+
import { RECOGNIZED_USE_EXPORTS_SET } from './server-exports-contract.js';
|
|
2
|
+
// Re-exported for backward compatibility. The canonical list now lives in
|
|
3
|
+
// `server-exports-contract.ts` so the server-only stub plugin, the
|
|
4
|
+
// validation plugin, and this parser all agree by import. New consumers
|
|
5
|
+
// should import directly from the contract file.
|
|
6
|
+
export const RECOGNIZED_USE_EXPORTS = RECOGNIZED_USE_EXPORTS_SET;
|
|
7
|
+
export function hasNamedUseExport(program, name) {
|
|
8
|
+
for (const stmt of program.body) {
|
|
9
|
+
if (stmt.type !== 'ExportNamedDeclaration' ||
|
|
10
|
+
stmt.declaration?.type !== 'VariableDeclaration')
|
|
11
|
+
continue;
|
|
12
|
+
for (const decl of stmt.declaration.declarations) {
|
|
13
|
+
if (decl.id.type === 'Identifier' && decl.id.name === name)
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Walk a parsed program for top-level `export const <use> = ...` declarations
|
|
21
|
+
* where `<use>` is one of the recognized middleware-carrying names
|
|
22
|
+
* (`pageUse` / `loaderUse` / `actionUse`). Returns each one with its
|
|
23
|
+
* initializer expression so callers can validate the shape (e.g. require
|
|
24
|
+
* an ArrayExpression literal).
|
|
25
|
+
*/
|
|
26
|
+
export function findUseExports(program) {
|
|
27
|
+
const found = [];
|
|
28
|
+
for (const stmt of program.body) {
|
|
29
|
+
if (stmt.type !== 'ExportNamedDeclaration' ||
|
|
30
|
+
stmt.declaration?.type !== 'VariableDeclaration')
|
|
31
|
+
continue;
|
|
32
|
+
for (const decl of stmt.declaration.declarations) {
|
|
33
|
+
if (decl.id.type !== 'Identifier')
|
|
34
|
+
continue;
|
|
35
|
+
if (!RECOGNIZED_USE_EXPORTS_SET.has(decl.id.name))
|
|
36
|
+
continue;
|
|
37
|
+
found.push({ name: decl.id.name, init: decl.init ?? null });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return found;
|
|
41
|
+
}
|
|
1
42
|
/**
|
|
2
43
|
* Walk a parsed program for `export const serverLoaders = { name: defineLoader(fn, opts?), ... }`.
|
|
3
44
|
* Returns one entry per object property whose value is a defineLoader(...) call.
|
package/dist/vite/server-only.js
CHANGED
|
@@ -5,6 +5,11 @@ import MagicString from 'magic-string';
|
|
|
5
5
|
import { deriveModuleKey } from './module-key.js';
|
|
6
6
|
import { parseServerLoaders, readParamsOpt } from './server-loaders-parser.js';
|
|
7
7
|
import { BABEL_PARSER_PLUGINS } from './parser-options.js';
|
|
8
|
+
import { RECOGNIZED_SERVER_EXPORTS } from './server-exports-contract.js';
|
|
9
|
+
// The unknown-specifier rejection message lists every recognized server
|
|
10
|
+
// export so a user can immediately see the valid set. The list is derived
|
|
11
|
+
// from the shared contract so it cannot drift from the validation plugin.
|
|
12
|
+
const ALLOWED_SPECIFIERS_LIST = RECOGNIZED_SERVER_EXPORTS.join(', ');
|
|
8
13
|
// Symbol-keyed accessor used by unit tests to verify `configResolved` fires
|
|
9
14
|
// and captures the root. Hidden behind a Symbol so it does not appear in IDE
|
|
10
15
|
// autocomplete for the public Plugin surface.
|
|
@@ -184,13 +189,26 @@ export function serverOnlyPlugin() {
|
|
|
184
189
|
}
|
|
185
190
|
else if (specifier.type === 'ImportSpecifier' &&
|
|
186
191
|
specifier.imported.type === 'Identifier' &&
|
|
187
|
-
specifier.imported.name === '
|
|
192
|
+
(specifier.imported.name === 'pageUse' ||
|
|
193
|
+
specifier.imported.name === 'loaderUse' ||
|
|
194
|
+
specifier.imported.name === 'actionUse')) {
|
|
195
|
+
// Middleware-carrying named exports never run on the client; the
|
|
196
|
+
// client only needs the array to exist so user imports don't
|
|
197
|
+
// crash. The real `use` arrays live on the server side and are
|
|
198
|
+
// wired into the dispatcher via pageUse resolvers.
|
|
188
199
|
stubs.push(`const ${specifier.local.name} = [];`);
|
|
189
200
|
}
|
|
190
201
|
else if (specifier.type === 'ImportSpecifier' &&
|
|
191
202
|
specifier.imported.type === 'Identifier' &&
|
|
192
203
|
specifier.imported.name === 'serverActions') {
|
|
193
204
|
needsUseActionImport = true;
|
|
205
|
+
// F9: each `serverActions.<name>` read constructs a fresh
|
|
206
|
+
// stub object, so `serverActions.create !== serverActions.create`
|
|
207
|
+
// across two reads. Not new in the middleware refactor, but
|
|
208
|
+
// worth flagging: callers that store the stub in a variable
|
|
209
|
+
// and treat it as a stable identity (e.g. `Map` keys) will
|
|
210
|
+
// surprise themselves. The contract is "stubs are descriptor
|
|
211
|
+
// records, not singletons."
|
|
194
212
|
stubs.push(`const ${specifier.local.name} = new Proxy({}, {\n` +
|
|
195
213
|
` get(_, action) {\n` +
|
|
196
214
|
` const stub = { __module: ${JSON.stringify(moduleKey)}, __action: String(action) };\n` +
|
|
@@ -207,7 +225,7 @@ export function serverOnlyPlugin() {
|
|
|
207
225
|
? '* as ' + specifier.local.name
|
|
208
226
|
: '<unknown>';
|
|
209
227
|
throw new Error(`${id}: \`${importedName}\` is not a recognized export from a *.server.* module. ` +
|
|
210
|
-
`Allowed:
|
|
228
|
+
`Allowed: ${ALLOWED_SPECIFIERS_LIST}.`);
|
|
211
229
|
}
|
|
212
230
|
}
|
|
213
231
|
if (stubs.length > 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hono-preact",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Hono on the edge, Preact in the browser, manifest driven routes, typed RPC, streaming everywhere.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hono",
|
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
"types": "./dist/index.d.ts",
|
|
37
37
|
"import": "./dist/index.js"
|
|
38
38
|
},
|
|
39
|
+
"./page": {
|
|
40
|
+
"types": "./dist/page.d.ts",
|
|
41
|
+
"import": "./dist/page.js"
|
|
42
|
+
},
|
|
39
43
|
"./server": {
|
|
40
44
|
"types": "./dist/server.d.ts",
|
|
41
45
|
"import": "./dist/server.js"
|
|
@@ -44,6 +48,14 @@
|
|
|
44
48
|
"types": "./dist/vite.d.ts",
|
|
45
49
|
"import": "./dist/vite.js"
|
|
46
50
|
},
|
|
51
|
+
"./adapter-cloudflare": {
|
|
52
|
+
"types": "./dist/adapter-cloudflare.d.ts",
|
|
53
|
+
"import": "./dist/adapter-cloudflare.js"
|
|
54
|
+
},
|
|
55
|
+
"./adapter-node": {
|
|
56
|
+
"types": "./dist/adapter-node.d.ts",
|
|
57
|
+
"import": "./dist/adapter-node.js"
|
|
58
|
+
},
|
|
47
59
|
"./internal": {
|
|
48
60
|
"types": "./dist/internal.d.ts",
|
|
49
61
|
"import": "./dist/internal.js"
|
|
@@ -52,18 +64,34 @@
|
|
|
52
64
|
"dependencies": {
|
|
53
65
|
"@babel/parser": "^7.29.2",
|
|
54
66
|
"@babel/types": "^7.29.0",
|
|
55
|
-
"@hono/vite-build": "^1.11.1",
|
|
56
|
-
"@hono/vite-dev-server": "^0.25.1",
|
|
57
67
|
"@preact/preset-vite": "^2.10.5",
|
|
58
68
|
"magic-string": "^0.30.21"
|
|
59
69
|
},
|
|
60
70
|
"peerDependencies": {
|
|
71
|
+
"@cloudflare/vite-plugin": "^1.37.1",
|
|
72
|
+
"@hono/node-server": "^1.19.14",
|
|
73
|
+
"@hono/node-ws": "^1.3.1",
|
|
61
74
|
"hono": ">=4.0.0",
|
|
62
75
|
"hoofd": ">=1.0.0",
|
|
63
76
|
"preact": ">=10.0.0",
|
|
64
77
|
"preact-iso": ">=2.11.0",
|
|
65
78
|
"preact-render-to-string": ">=6.0.0",
|
|
66
|
-
"vite": ">=
|
|
79
|
+
"vite": ">=6.0.0",
|
|
80
|
+
"wrangler": "^4.92.0"
|
|
81
|
+
},
|
|
82
|
+
"peerDependenciesMeta": {
|
|
83
|
+
"@cloudflare/vite-plugin": {
|
|
84
|
+
"optional": true
|
|
85
|
+
},
|
|
86
|
+
"@hono/node-server": {
|
|
87
|
+
"optional": true
|
|
88
|
+
},
|
|
89
|
+
"@hono/node-ws": {
|
|
90
|
+
"optional": true
|
|
91
|
+
},
|
|
92
|
+
"wrangler": {
|
|
93
|
+
"optional": true
|
|
94
|
+
}
|
|
67
95
|
},
|
|
68
96
|
"devDependencies": {
|
|
69
97
|
"typescript": "*",
|