hono-preact 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.
Files changed (130) hide show
  1. package/README.md +47 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +1 -0
  5. package/dist/internal.d.ts +1 -0
  6. package/dist/internal.d.ts.map +1 -0
  7. package/dist/internal.js +1 -0
  8. package/dist/iso/action.d.ts +78 -0
  9. package/dist/iso/action.js +189 -0
  10. package/dist/iso/cache.d.ts +17 -0
  11. package/dist/iso/cache.js +122 -0
  12. package/dist/iso/client-script.d.ts +2 -0
  13. package/dist/iso/client-script.js +13 -0
  14. package/dist/iso/define-loader.d.ts +47 -0
  15. package/dist/iso/define-loader.js +118 -0
  16. package/dist/iso/define-page.d.ts +10 -0
  17. package/dist/iso/define-page.js +7 -0
  18. package/dist/iso/define-routes.d.ts +34 -0
  19. package/dist/iso/define-routes.js +251 -0
  20. package/dist/iso/form.d.ts +7 -0
  21. package/dist/iso/form.js +40 -0
  22. package/dist/iso/guard.d.ts +33 -0
  23. package/dist/iso/guard.js +32 -0
  24. package/dist/iso/head.d.ts +6 -0
  25. package/dist/iso/head.js +4 -0
  26. package/dist/iso/index.d.ts +30 -0
  27. package/dist/iso/index.js +29 -0
  28. package/dist/iso/internal/cache-key.d.ts +2 -0
  29. package/dist/iso/internal/cache-key.js +8 -0
  30. package/dist/iso/internal/contexts.d.ts +12 -0
  31. package/dist/iso/internal/contexts.js +7 -0
  32. package/dist/iso/internal/envelope.d.ts +8 -0
  33. package/dist/iso/internal/envelope.js +21 -0
  34. package/dist/iso/internal/guard-noop.d.ts +7 -0
  35. package/dist/iso/internal/guard-noop.js +6 -0
  36. package/dist/iso/internal/guards.d.ts +14 -0
  37. package/dist/iso/internal/guards.js +54 -0
  38. package/dist/iso/internal/loader-fetch.d.ts +20 -0
  39. package/dist/iso/internal/loader-fetch.js +123 -0
  40. package/dist/iso/internal/loader-runner.d.ts +15 -0
  41. package/dist/iso/internal/loader-runner.js +59 -0
  42. package/dist/iso/internal/loader-stub.d.ts +8 -0
  43. package/dist/iso/internal/loader-stub.js +19 -0
  44. package/dist/iso/internal/loader.d.ts +13 -0
  45. package/dist/iso/internal/loader.js +31 -0
  46. package/dist/iso/internal/optimistic-overlay.d.ts +10 -0
  47. package/dist/iso/internal/optimistic-overlay.js +11 -0
  48. package/dist/iso/internal/preload.d.ts +15 -0
  49. package/dist/iso/internal/preload.js +36 -0
  50. package/dist/iso/internal/route-boundary.d.ts +25 -0
  51. package/dist/iso/internal/route-boundary.js +24 -0
  52. package/dist/iso/internal/route-change.d.ts +4 -0
  53. package/dist/iso/internal/route-change.js +18 -0
  54. package/dist/iso/internal/route-locations.d.ts +11 -0
  55. package/dist/iso/internal/route-locations.js +15 -0
  56. package/dist/iso/internal/sse-decoder.d.ts +5 -0
  57. package/dist/iso/internal/sse-decoder.js +43 -0
  58. package/dist/iso/internal/stream-registry.d.ts +60 -0
  59. package/dist/iso/internal/stream-registry.js +98 -0
  60. package/dist/iso/internal/streaming-ssr.d.ts +17 -0
  61. package/dist/iso/internal/streaming-ssr.js +32 -0
  62. package/dist/iso/internal/use-loader-runner.d.ts +12 -0
  63. package/dist/iso/internal/use-loader-runner.js +185 -0
  64. package/dist/iso/internal/wrap-promise.d.ts +4 -0
  65. package/dist/iso/internal/wrap-promise.js +24 -0
  66. package/dist/iso/internal.d.ts +19 -0
  67. package/dist/iso/internal.js +49 -0
  68. package/dist/iso/is-browser.d.ts +4 -0
  69. package/dist/iso/is-browser.js +6 -0
  70. package/dist/iso/optimistic-action.d.ts +19 -0
  71. package/dist/iso/optimistic-action.js +25 -0
  72. package/dist/iso/optimistic.d.ts +5 -0
  73. package/dist/iso/optimistic.js +31 -0
  74. package/dist/iso/page.d.ts +16 -0
  75. package/dist/iso/page.js +10 -0
  76. package/dist/iso/prefetch.d.ts +22 -0
  77. package/dist/iso/prefetch.js +78 -0
  78. package/dist/iso/reload-context.d.ts +6 -0
  79. package/dist/iso/reload-context.js +9 -0
  80. package/dist/iso/route-change.d.ts +2 -0
  81. package/dist/iso/route-change.js +10 -0
  82. package/dist/iso/view-transitions.d.ts +1 -0
  83. package/dist/iso/view-transitions.js +6 -0
  84. package/dist/server/actions-handler.d.ts +33 -0
  85. package/dist/server/actions-handler.js +159 -0
  86. package/dist/server/context.d.ts +6 -0
  87. package/dist/server/context.js +6 -0
  88. package/dist/server/index.d.ts +5 -0
  89. package/dist/server/index.js +5 -0
  90. package/dist/server/loaders-handler.d.ts +30 -0
  91. package/dist/server/loaders-handler.js +117 -0
  92. package/dist/server/middleware/location.d.ts +1 -0
  93. package/dist/server/middleware/location.js +10 -0
  94. package/dist/server/render.d.ts +5 -0
  95. package/dist/server/render.js +203 -0
  96. package/dist/server/route-server-modules.d.ts +12 -0
  97. package/dist/server/route-server-modules.js +13 -0
  98. package/dist/server/sse.d.ts +22 -0
  99. package/dist/server/sse.js +83 -0
  100. package/dist/server.d.ts +1 -0
  101. package/dist/server.d.ts.map +1 -0
  102. package/dist/server.js +1 -0
  103. package/dist/vite/client-entry.d.ts +10 -0
  104. package/dist/vite/client-entry.js +47 -0
  105. package/dist/vite/client-shim.d.ts +12 -0
  106. package/dist/vite/client-shim.js +62 -0
  107. package/dist/vite/guard-strip.d.ts +2 -0
  108. package/dist/vite/guard-strip.js +96 -0
  109. package/dist/vite/hono-preact.d.ts +12 -0
  110. package/dist/vite/hono-preact.js +111 -0
  111. package/dist/vite/index.d.ts +7 -0
  112. package/dist/vite/index.js +7 -0
  113. package/dist/vite/module-key-plugin.d.ts +12 -0
  114. package/dist/vite/module-key-plugin.js +114 -0
  115. package/dist/vite/module-key.d.ts +11 -0
  116. package/dist/vite/module-key.js +20 -0
  117. package/dist/vite/parser-options.d.ts +16 -0
  118. package/dist/vite/parser-options.js +22 -0
  119. package/dist/vite/server-entry.d.ts +26 -0
  120. package/dist/vite/server-entry.js +201 -0
  121. package/dist/vite/server-loader-validation.d.ts +2 -0
  122. package/dist/vite/server-loader-validation.js +73 -0
  123. package/dist/vite/server-loaders-parser.d.ts +22 -0
  124. package/dist/vite/server-loaders-parser.js +64 -0
  125. package/dist/vite/server-only.d.ts +3 -0
  126. package/dist/vite/server-only.js +244 -0
  127. package/dist/vite.d.ts +1 -0
  128. package/dist/vite.d.ts.map +1 -0
  129. package/dist/vite.js +1 -0
  130. package/package.json +78 -0
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Shared `@babel/parser` options for every code-walk in this package.
3
+ *
4
+ * Keep this list permissive: we parse user code (`.server.ts`, `routes.ts`,
5
+ * `api.ts`, etc.), not just framework-authored code, so any stage-3 syntax a
6
+ * user might reasonably write should round-trip. A user who writes
7
+ * `class Foo { @inject() bar }` or `import data from './x.json' with { type: 'json' }`
8
+ * should not get a silent code-walk failure where the framework expected to
9
+ * find a `defineLoader(...)` call and ran past it.
10
+ *
11
+ * Each call site adds `sourceType: 'module'` and `errorRecovery: true`
12
+ * separately because both are universal at our call sites and the
13
+ * `ParserOptions` shape carries them as top-level fields.
14
+ */
15
+ export const BABEL_PARSER_PLUGINS = [
16
+ 'typescript',
17
+ 'jsx',
18
+ 'decorators',
19
+ 'decoratorAutoAccessors',
20
+ 'importAttributes',
21
+ 'explicitResourceManagement',
22
+ ];
@@ -0,0 +1,26 @@
1
+ import type { Plugin } from 'vite';
2
+ export interface GenerateServerEntrySourceOptions {
3
+ layoutAbsPath: string;
4
+ routesAbsPath: string;
5
+ apiAbsPath: string | undefined;
6
+ }
7
+ export declare function generateServerEntrySource(opts: GenerateServerEntrySourceOptions): string;
8
+ export type CatchAllWarning = {
9
+ kind: 'wildcard';
10
+ method: string;
11
+ pattern: string;
12
+ line: number | undefined;
13
+ } | {
14
+ kind: 'notFound';
15
+ line: number | undefined;
16
+ };
17
+ export declare function findApiCatchAllRoutes(source: string): CatchAllWarning[];
18
+ export declare const GENERATED_SERVER_ENTRY_RELATIVE = "node_modules/.vite/hono-preact/server-entry.tsx";
19
+ export declare function generatedServerEntryAbsPath(cwd?: string): string;
20
+ export interface ServerEntryPluginOptions {
21
+ layout: string;
22
+ routes: string;
23
+ api: string;
24
+ outputPath: string;
25
+ }
26
+ export declare function serverEntryPlugin(opts: ServerEntryPluginOptions): Plugin;
@@ -0,0 +1,201 @@
1
+ import * as path from 'node:path';
2
+ import * as fs from 'node:fs';
3
+ import { parse } from '@babel/parser';
4
+ import { BABEL_PARSER_PLUGINS } from './parser-options.js';
5
+ export function generateServerEntrySource(opts) {
6
+ const { layoutAbsPath, routesAbsPath, apiAbsPath } = opts;
7
+ const apiImport = apiAbsPath ? `import userApp from '${apiAbsPath}';\n` : '';
8
+ const apiMount = apiAbsPath ? ` .route('/', userApp)\n` : '';
9
+ // The generated source is loaded as a virtual module, which Vite/esbuild
10
+ // treats as plain JS by default. Use h() to construct vnodes rather than
11
+ // JSX so the source compiles without a TSX loader hint.
12
+ return (`import { Hono } from 'hono';\n` +
13
+ `import { h } from 'preact';\n` +
14
+ `import { LocationProvider } from 'preact-iso';\n` +
15
+ `import { Routes, env } from 'hono-preact';\n` +
16
+ `import {\n` +
17
+ ` actionsHandler,\n` +
18
+ ` loadersHandler,\n` +
19
+ ` renderPage,\n` +
20
+ ` routeServerModules,\n` +
21
+ `} from 'hono-preact/server';\n` +
22
+ `import Layout from '${layoutAbsPath}';\n` +
23
+ `import routes from '${routesAbsPath}';\n` +
24
+ apiImport +
25
+ `\n` +
26
+ `env.current = 'server';\n` +
27
+ `const serverModules = routeServerModules(routes);\n` +
28
+ `const handlerOpts = { dev: import.meta.env.DEV };\n` +
29
+ `\n` +
30
+ `export const app = new Hono()\n` +
31
+ ` .post('/__loaders', loadersHandler(serverModules, handlerOpts))\n` +
32
+ ` .post('/__actions', actionsHandler(serverModules, handlerOpts))\n` +
33
+ apiMount +
34
+ ` .get('*', (c) => renderPage(c, h(Layout, null, h(LocationProvider, null, h(Routes, { routes })))));\n` +
35
+ `\n` +
36
+ `export default app;\n`);
37
+ }
38
+ const HONO_METHODS = new Set([
39
+ 'get',
40
+ 'post',
41
+ 'put',
42
+ 'patch',
43
+ 'delete',
44
+ 'options',
45
+ 'head',
46
+ 'all',
47
+ 'on',
48
+ ]);
49
+ const WILDCARD_PATTERNS = new Set(['*', '/*']);
50
+ // Walk treats these as opaque: their bodies are user handlers, not route
51
+ // registrations. Skipping the body keeps `c.notFound()` inside a handler
52
+ // from being mistaken for `app.notFound(...)` at registration time.
53
+ const FUNCTION_BODY_PARENTS = new Set([
54
+ 'FunctionDeclaration',
55
+ 'FunctionExpression',
56
+ 'ArrowFunctionExpression',
57
+ 'ObjectMethod',
58
+ 'ClassMethod',
59
+ ]);
60
+ export function findApiCatchAllRoutes(source) {
61
+ const warnings = [];
62
+ let ast;
63
+ try {
64
+ ast = parse(source, {
65
+ sourceType: 'module',
66
+ plugins: BABEL_PARSER_PLUGINS,
67
+ errorRecovery: true,
68
+ });
69
+ }
70
+ catch (err) {
71
+ // If api.ts won't parse, the build will fail elsewhere with a clearer
72
+ // error. Surface a note so the framework user can correlate a missing
73
+ // catch-all warning with a parse-time syntax issue rather than wondering
74
+ // why nothing was reported.
75
+ const msg = err instanceof Error ? err.message : String(err);
76
+ console.warn(`[hono-preact] Failed to parse api.ts for catch-all route detection: ${msg}. ` +
77
+ `The build will surface the real syntax error; this warning explains why ` +
78
+ `route-overlap warnings may be missing.`);
79
+ return warnings;
80
+ }
81
+ walk(ast.program, warnings);
82
+ return warnings;
83
+ }
84
+ function walk(node, warnings) {
85
+ if (!node || typeof node !== 'object')
86
+ return;
87
+ if (Array.isArray(node)) {
88
+ for (const child of node)
89
+ walk(child, warnings);
90
+ return;
91
+ }
92
+ const n = node;
93
+ if (n.type === 'CallExpression' &&
94
+ n.callee?.type === 'MemberExpression' &&
95
+ n.callee.property?.type === 'Identifier' &&
96
+ typeof n.callee.property.name === 'string') {
97
+ const method = n.callee.property.name;
98
+ const line = n.loc?.start?.line;
99
+ if (method === 'notFound') {
100
+ warnings.push({ kind: 'notFound', line });
101
+ }
102
+ else if (HONO_METHODS.has(method)) {
103
+ const firstArg = n.arguments?.[0];
104
+ if (firstArg?.type === 'StringLiteral' &&
105
+ typeof firstArg.value === 'string' &&
106
+ WILDCARD_PATTERNS.has(firstArg.value)) {
107
+ warnings.push({
108
+ kind: 'wildcard',
109
+ method,
110
+ pattern: firstArg.value,
111
+ line,
112
+ });
113
+ }
114
+ }
115
+ }
116
+ const isFunctionParent = typeof n.type === 'string' && FUNCTION_BODY_PARENTS.has(n.type);
117
+ for (const key of Object.keys(node)) {
118
+ if (key === 'loc' ||
119
+ key === 'leadingComments' ||
120
+ key === 'trailingComments')
121
+ continue;
122
+ if (isFunctionParent && key === 'body')
123
+ continue;
124
+ walk(node[key], warnings);
125
+ }
126
+ }
127
+ // Project-relative path of the on-disk file the plugin writes during
128
+ // configResolved. We use a relative path because @hono/vite-build prepends
129
+ // `/` to the entry when constructing its `import.meta.glob([...])`, and
130
+ // absolute paths produce a `//Users/...` double-slash that's brittle in the
131
+ // Cloudflare Workers runtime. Project-relative keeps the resulting glob
132
+ // pattern (`/node_modules/.vite/hono-preact/server-entry.tsx`) clean.
133
+ //
134
+ // We write to disk rather than register a virtual module because
135
+ // @hono/vite-build resolves its entry via `import.meta.glob([entry])`, which
136
+ // cannot match a `virtual:*` id. The Cloudflare Workers runtime then fails
137
+ // with "Can't import modules from ['/virtual:...']" when it tries to load the
138
+ // generated bundle.
139
+ export const GENERATED_SERVER_ENTRY_RELATIVE = 'node_modules/.vite/hono-preact/server-entry.tsx';
140
+ export function generatedServerEntryAbsPath(cwd = process.cwd()) {
141
+ return path.resolve(cwd, GENERATED_SERVER_ENTRY_RELATIVE);
142
+ }
143
+ 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
+ let apiAbsPath;
153
+ return {
154
+ name: 'hono-preact:server-entry',
155
+ enforce: 'pre',
156
+ configResolved(config) {
157
+ layoutAbsPath = path.isAbsolute(opts.layout)
158
+ ? opts.layout
159
+ : path.resolve(config.root, opts.layout);
160
+ routesAbsPath = path.isAbsolute(opts.routes)
161
+ ? opts.routes
162
+ : path.resolve(config.root, opts.routes);
163
+ const candidateApi = path.isAbsolute(opts.api)
164
+ ? opts.api
165
+ : path.resolve(config.root, opts.api);
166
+ apiAbsPath = fs.existsSync(candidateApi) ? candidateApi : undefined;
167
+ },
168
+ buildStart() {
169
+ // configResolved must have run first. Bail loudly if not — silent
170
+ // emission of a stub would be much harder to debug.
171
+ if (!layoutAbsPath || !routesAbsPath) {
172
+ throw new Error('[hono-preact] server-entry buildStart fired before configResolved; ' +
173
+ 'layout/routes paths were never resolved.');
174
+ }
175
+ const source = generateServerEntrySource({
176
+ layoutAbsPath,
177
+ routesAbsPath,
178
+ apiAbsPath,
179
+ });
180
+ fs.mkdirSync(path.dirname(opts.outputPath), { recursive: true });
181
+ fs.writeFileSync(opts.outputPath, source, 'utf8');
182
+ if (!apiAbsPath)
183
+ return;
184
+ const apiSource = fs.readFileSync(apiAbsPath, 'utf8');
185
+ const warnings = findApiCatchAllRoutes(apiSource);
186
+ for (const w of warnings) {
187
+ const where = `${apiAbsPath}${w.line != null ? `:${w.line}` : ''}`;
188
+ if (w.kind === 'notFound') {
189
+ this.warn(`[hono-preact] ${where}: app.notFound(...) acts as a catch-all and ` +
190
+ `will be shadowed by the framework's renderPage handler. ` +
191
+ `Move the behavior to a more specific path, or accept that it won't fire.`);
192
+ }
193
+ else {
194
+ this.warn(`[hono-preact] ${where}: app.${w.method}('${w.pattern}', ...) is a ` +
195
+ `catch-all route and will be shadowed by the framework's renderPage ` +
196
+ `handler. Move it to a more specific path, or accept that it won't fire.`);
197
+ }
198
+ }
199
+ },
200
+ };
201
+ }
@@ -0,0 +1,2 @@
1
+ import type { Plugin } from 'vite';
2
+ export declare function serverLoaderValidationPlugin(): Plugin;
@@ -0,0 +1,73 @@
1
+ import { parse } from '@babel/parser';
2
+ import { BABEL_PARSER_PLUGINS } from './parser-options.js';
3
+ const ALLOWED_NAMED_EXPORTS = new Set([
4
+ 'serverActions',
5
+ 'actionGuards',
6
+ 'serverLoaders',
7
+ ]);
8
+ const ALLOWED_NAMED_EXPORTS_LIST = [...ALLOWED_NAMED_EXPORTS]
9
+ .map((n) => `'${n}'`)
10
+ .join(', ');
11
+ export function serverLoaderValidationPlugin() {
12
+ return {
13
+ name: 'server-loader-validation',
14
+ enforce: 'pre',
15
+ transform(code, id) {
16
+ if (!/\.server\.[jt]sx?$/.test(id))
17
+ return;
18
+ const ast = parse(code, {
19
+ sourceType: 'module',
20
+ plugins: BABEL_PARSER_PLUGINS,
21
+ errorRecovery: true,
22
+ });
23
+ let hasDefault = false;
24
+ const namedExports = [];
25
+ const errors = [];
26
+ for (const node of ast.program.body) {
27
+ if (node.type === 'ExportDefaultDeclaration') {
28
+ hasDefault = true;
29
+ }
30
+ else if (node.type === 'ExportAllDeclaration') {
31
+ errors.push(`${id}: .server files may not use 'export * from ...'. Use explicit named exports only.`);
32
+ }
33
+ else if (node.type === 'ExportNamedDeclaration') {
34
+ const named = node;
35
+ if (named.exportKind === 'type')
36
+ continue;
37
+ for (const s of named.specifiers) {
38
+ namedExports.push(s.exported.type === 'Identifier'
39
+ ? s.exported.name
40
+ : s.exported.value);
41
+ }
42
+ if (named.declaration?.type === 'FunctionDeclaration' &&
43
+ named.declaration.id) {
44
+ namedExports.push(named.declaration.id.name);
45
+ }
46
+ else if (named.declaration?.type === 'VariableDeclaration') {
47
+ for (const decl of named.declaration.declarations) {
48
+ if (decl.id.type === 'Identifier')
49
+ namedExports.push(decl.id.name);
50
+ }
51
+ }
52
+ }
53
+ }
54
+ const disallowedExports = namedExports.filter((n) => !ALLOWED_NAMED_EXPORTS.has(n));
55
+ if (disallowedExports.length > 0) {
56
+ errors.push(`${id}: .server files may only export ${ALLOWED_NAMED_EXPORTS_LIST} as named exports (found: ${disallowedExports.join(', ')}). ` +
57
+ `Export the server loader as the default export only.`);
58
+ }
59
+ if (hasDefault) {
60
+ errors.push(`${id}: .server files may not use a default export. ` +
61
+ `Use \`export const serverLoaders = { default: defineLoader(...) }\` instead.`);
62
+ }
63
+ if (!namedExports.includes('serverActions') &&
64
+ !namedExports.includes('serverLoaders')) {
65
+ errors.push(`${id}: .server files must export either 'serverLoaders' or 'serverActions'. ` +
66
+ `Use \`export const serverLoaders = { default: defineLoader(fn) }\` to define loaders.`);
67
+ }
68
+ if (errors.length > 0) {
69
+ this.error(errors.join('\n'));
70
+ }
71
+ },
72
+ };
73
+ }
@@ -0,0 +1,22 @@
1
+ import type { Program, CallExpression, ObjectExpression } from '@babel/types';
2
+ export type ParsedLoaderEntry = {
3
+ /** Loader name (the key in serverLoaders, e.g. "summary"). */
4
+ name: string;
5
+ /** The defineLoader(...) CallExpression node -- for AST-mutation consumers. */
6
+ call: CallExpression;
7
+ /** The opts ObjectExpression if defineLoader has 2 args; otherwise null. */
8
+ optsArg: ObjectExpression | null;
9
+ };
10
+ /**
11
+ * Walk a parsed program for `export const serverLoaders = { name: defineLoader(fn, opts?), ... }`.
12
+ * Returns one entry per object property whose value is a defineLoader(...) call.
13
+ * Non-matching properties (spread elements, computed keys, non-call values) are silently skipped.
14
+ * If `serverLoaders` is absent or has the wrong shape, returns [].
15
+ */
16
+ export declare function parseServerLoaders(program: Program): ParsedLoaderEntry[];
17
+ /**
18
+ * Read the `params` option from a defineLoader opts ObjectExpression literal.
19
+ * Returns `string[]` for array literals of string literals, `'*'` for the
20
+ * wildcard string literal, or undefined if not present or unsupported shape.
21
+ */
22
+ export declare function readParamsOpt(opts: ObjectExpression): string[] | '*' | undefined;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Walk a parsed program for `export const serverLoaders = { name: defineLoader(fn, opts?), ... }`.
3
+ * Returns one entry per object property whose value is a defineLoader(...) call.
4
+ * Non-matching properties (spread elements, computed keys, non-call values) are silently skipped.
5
+ * If `serverLoaders` is absent or has the wrong shape, returns [].
6
+ */
7
+ export function parseServerLoaders(program) {
8
+ const entries = [];
9
+ for (const stmt of program.body) {
10
+ if (stmt.type !== 'ExportNamedDeclaration' ||
11
+ stmt.declaration?.type !== 'VariableDeclaration')
12
+ continue;
13
+ for (const decl of stmt.declaration.declarations) {
14
+ if (decl.id.type !== 'Identifier' ||
15
+ decl.id.name !== 'serverLoaders' ||
16
+ decl.init?.type !== 'ObjectExpression')
17
+ continue;
18
+ const obj = decl.init;
19
+ for (const prop of obj.properties) {
20
+ if (prop.type !== 'ObjectProperty' ||
21
+ prop.key.type !== 'Identifier' ||
22
+ prop.value.type !== 'CallExpression')
23
+ continue;
24
+ const call = prop.value;
25
+ if (call.callee.type !== 'Identifier' ||
26
+ call.callee.name !== 'defineLoader')
27
+ continue;
28
+ const secondArg = call.arguments[1];
29
+ const optsArg = secondArg?.type === 'ObjectExpression'
30
+ ? secondArg
31
+ : null;
32
+ entries.push({ name: prop.key.name, call, optsArg });
33
+ }
34
+ }
35
+ }
36
+ return entries;
37
+ }
38
+ /**
39
+ * Read the `params` option from a defineLoader opts ObjectExpression literal.
40
+ * Returns `string[]` for array literals of string literals, `'*'` for the
41
+ * wildcard string literal, or undefined if not present or unsupported shape.
42
+ */
43
+ export function readParamsOpt(opts) {
44
+ for (const prop of opts.properties) {
45
+ if (prop.type !== 'ObjectProperty' ||
46
+ prop.key.type !== 'Identifier' ||
47
+ prop.key.name !== 'params')
48
+ continue;
49
+ const val = prop.value;
50
+ if (val.type === 'StringLiteral' && val.value === '*') {
51
+ return '*';
52
+ }
53
+ if (val.type === 'ArrayExpression') {
54
+ const items = [];
55
+ for (const el of val.elements) {
56
+ if (el?.type === 'StringLiteral')
57
+ items.push(el.value);
58
+ }
59
+ if (items.length > 0)
60
+ return items;
61
+ }
62
+ }
63
+ return undefined;
64
+ }
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from 'vite';
2
+ export declare const VITE_ROOT_ACCESSOR: unique symbol;
3
+ export declare function serverOnlyPlugin(): Plugin;
@@ -0,0 +1,244 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { parse } from '@babel/parser';
4
+ import MagicString from 'magic-string';
5
+ import { deriveModuleKey } from './module-key.js';
6
+ import { parseServerLoaders, readParamsOpt } from './server-loaders-parser.js';
7
+ import { BABEL_PARSER_PLUGINS } from './parser-options.js';
8
+ // Symbol-keyed accessor used by unit tests to verify `configResolved` fires
9
+ // and captures the root. Hidden behind a Symbol so it does not appear in IDE
10
+ // autocomplete for the public Plugin surface.
11
+ export const VITE_ROOT_ACCESSOR = Symbol.for('@hono-preact/vite/server-only/viteRoot');
12
+ /**
13
+ * Reads a .server.* file synchronously and extracts the `params` option from
14
+ * each entry in the `serverLoaders` ObjectExpression. Returns a map of
15
+ * { loaderName -> params } for loaders that declare non-default params.
16
+ * Returns an empty object if the file cannot be parsed or has no serverLoaders.
17
+ */
18
+ function readSourceWithExtensionFallback(absServerPath) {
19
+ // TypeScript NodeNext convention: source code imports `.server.js` even
20
+ // though the file on disk is `.server.ts` (or .tsx). Try the literal path
21
+ // first (handles plain `.js` cases), then the TS-extension swaps.
22
+ const tries = [
23
+ absServerPath,
24
+ absServerPath.replace(/\.js$/, '.ts'),
25
+ absServerPath.replace(/\.jsx$/, '.tsx'),
26
+ ];
27
+ for (const p of tries) {
28
+ try {
29
+ return fs.readFileSync(p, 'utf8');
30
+ }
31
+ catch {
32
+ // try next candidate
33
+ }
34
+ }
35
+ return null;
36
+ }
37
+ function extractServerLoadersMeta(absServerPath) {
38
+ const src = readSourceWithExtensionFallback(absServerPath);
39
+ if (src == null)
40
+ return {};
41
+ let ast;
42
+ try {
43
+ ast = parse(src, {
44
+ sourceType: 'module',
45
+ plugins: BABEL_PARSER_PLUGINS,
46
+ errorRecovery: true,
47
+ });
48
+ }
49
+ catch {
50
+ return {};
51
+ }
52
+ const entries = parseServerLoaders(ast.program);
53
+ const meta = {};
54
+ for (const entry of entries) {
55
+ if (!entry.optsArg)
56
+ continue;
57
+ const params = readParamsOpt(entry.optsArg);
58
+ if (params !== undefined)
59
+ meta[entry.name] = params;
60
+ }
61
+ return meta;
62
+ }
63
+ function findDynamicServerImports(node, found) {
64
+ if (!node || typeof node !== 'object')
65
+ return;
66
+ if (Array.isArray(node)) {
67
+ for (const child of node)
68
+ findDynamicServerImports(child, found);
69
+ return;
70
+ }
71
+ const n = node;
72
+ if (n.type === 'CallExpression' &&
73
+ n.callee?.type === 'Import' &&
74
+ n.arguments?.[0]?.type === 'StringLiteral' &&
75
+ typeof n.arguments[0].value === 'string' &&
76
+ /\.server(\.[jt]sx?)?$/.test(n.arguments[0].value)) {
77
+ found.push({
78
+ start: n.start,
79
+ end: n.end,
80
+ source: n.arguments[0].value,
81
+ });
82
+ }
83
+ for (const key of Object.keys(node)) {
84
+ if (key === 'loc' ||
85
+ key === 'leadingComments' ||
86
+ key === 'trailingComments')
87
+ continue;
88
+ findDynamicServerImports(node[key], found);
89
+ }
90
+ }
91
+ export function serverOnlyPlugin() {
92
+ let viteRoot;
93
+ return {
94
+ name: 'server-only',
95
+ enforce: 'pre',
96
+ configResolved(config) {
97
+ viteRoot = config.root;
98
+ },
99
+ [VITE_ROOT_ACCESSOR]: () => viteRoot,
100
+ transform(code, id, options) {
101
+ if (options?.ssr)
102
+ return;
103
+ if (!/\.[jt]sx?$/.test(id))
104
+ return;
105
+ if (/\.server\.[jt]sx?$/.test(id))
106
+ return;
107
+ if (!code.includes('.server'))
108
+ return;
109
+ const ast = parse(code, {
110
+ sourceType: 'module',
111
+ plugins: BABEL_PARSER_PLUGINS,
112
+ errorRecovery: true,
113
+ });
114
+ for (const node of ast.program.body) {
115
+ if ((node.type === 'ExportNamedDeclaration' ||
116
+ node.type === 'ExportAllDeclaration') &&
117
+ node.source &&
118
+ /\.server(\.[jt]sx?)?$/.test(node.source.value)) {
119
+ throw new Error(`${id}: re-export from '${node.source.value}' (a .server.* module) is not supported. ` +
120
+ `Import the named member directly instead, e.g. ` +
121
+ `\`import { loader } from '${node.source.value}';\``);
122
+ }
123
+ }
124
+ const isServerImport = (node) => node.type === 'ImportDeclaration' &&
125
+ /\.server(\.[jt]sx?)?$/.test(node.source.value);
126
+ const serverImports = ast.program.body.filter(isServerImport);
127
+ const dynamicServerImports = [];
128
+ findDynamicServerImports(ast.program, dynamicServerImports);
129
+ if (serverImports.length === 0 && dynamicServerImports.length === 0)
130
+ return;
131
+ if (serverImports.length > 0 && viteRoot === undefined) {
132
+ this.warn(`serverOnlyPlugin: configResolved hasn't fired before transform on ${id}. ` +
133
+ `.server.* imports will not be transformed; this can leak server code into the client bundle. ` +
134
+ `Ensure moduleKeyPlugin and serverOnlyPlugin are added to the Vite config under the standard plugin pipeline.`);
135
+ return;
136
+ }
137
+ const importerDir = path.dirname(id);
138
+ const s = new MagicString(code);
139
+ let needsCreateLoaderStubImport = false;
140
+ let needsUseActionImport = false;
141
+ for (const serverImport of [...serverImports].reverse()) {
142
+ if (serverImport.importKind ===
143
+ 'type') {
144
+ s.overwrite(serverImport.start, serverImport.end, '');
145
+ continue;
146
+ }
147
+ // viteRoot is guaranteed defined here: the early-return above bails when
148
+ // we have static imports without a viteRoot. Dynamic-only files skip this
149
+ // loop entirely.
150
+ const absServerPath = path.resolve(importerDir, serverImport.source.value);
151
+ const moduleKey = deriveModuleKey(absServerPath, viteRoot);
152
+ if (moduleKey.startsWith('..')) {
153
+ this.warn(`serverOnlyPlugin: import of '${serverImport.source.value}' from '${id}' resolves outside the Vite root (${viteRoot}). ` +
154
+ `Generated module key '${moduleKey}' will not match any server-side moduleKeyPlugin output, so RPC calls will return 404. ` +
155
+ `Move the .server.* file inside the Vite root, or restructure the import.`);
156
+ }
157
+ const stubs = [];
158
+ let hasValueSpecifier = false;
159
+ for (const specifier of serverImport.specifiers) {
160
+ if (specifier.importKind ===
161
+ 'type') {
162
+ continue;
163
+ }
164
+ hasValueSpecifier = true;
165
+ if (specifier.type === 'ImportSpecifier' &&
166
+ specifier.imported.type === 'Identifier' &&
167
+ specifier.imported.name === 'serverLoaders') {
168
+ needsCreateLoaderStubImport = true;
169
+ const absServerPath = path.resolve(importerDir, serverImport.source.value);
170
+ const loadersMeta = extractServerLoadersMeta(absServerPath);
171
+ const metaVar = `__$serverLoadersMeta_${specifier.local.name}`;
172
+ const metaJson = JSON.stringify(loadersMeta);
173
+ stubs.push(`const ${metaVar} = ${metaJson};\n` +
174
+ `const ${specifier.local.name} = new Proxy({}, {\n` +
175
+ ` get(_, name) {\n` +
176
+ ` const __meta = ${metaVar}[String(name)];\n` +
177
+ ` return __$createLoaderStub_hpiso({\n` +
178
+ ` __moduleKey: ${JSON.stringify(moduleKey)},\n` +
179
+ ` __loaderName: String(name),\n` +
180
+ ` params: __meta,\n` +
181
+ ` });\n` +
182
+ ` }\n` +
183
+ `});`);
184
+ }
185
+ else if (specifier.type === 'ImportSpecifier' &&
186
+ specifier.imported.type === 'Identifier' &&
187
+ specifier.imported.name === 'actionGuards') {
188
+ stubs.push(`const ${specifier.local.name} = [];`);
189
+ }
190
+ else if (specifier.type === 'ImportSpecifier' &&
191
+ specifier.imported.type === 'Identifier' &&
192
+ specifier.imported.name === 'serverActions') {
193
+ needsUseActionImport = true;
194
+ stubs.push(`const ${specifier.local.name} = new Proxy({}, {\n` +
195
+ ` get(_, action) {\n` +
196
+ ` const stub = { __module: ${JSON.stringify(moduleKey)}, __action: String(action) };\n` +
197
+ ` stub.useAction = (opts) => __$useAction_hpiso(stub, opts);\n` +
198
+ ` return stub;\n` +
199
+ ` }\n` +
200
+ `});`);
201
+ }
202
+ else {
203
+ const importedName = specifier.type === 'ImportSpecifier' &&
204
+ specifier.imported.type === 'Identifier'
205
+ ? specifier.imported.name
206
+ : specifier.type === 'ImportNamespaceSpecifier'
207
+ ? '* as ' + specifier.local.name
208
+ : '<unknown>';
209
+ throw new Error(`${id}: \`${importedName}\` is not a recognized export from a *.server.* module. ` +
210
+ `Allowed: serverLoaders, serverActions, actionGuards.`);
211
+ }
212
+ }
213
+ if (stubs.length > 0) {
214
+ s.overwrite(serverImport.start, serverImport.end, stubs.join('\n'));
215
+ }
216
+ else if (!hasValueSpecifier) {
217
+ s.overwrite(serverImport.start, serverImport.end, '');
218
+ }
219
+ }
220
+ for (const imp of [...dynamicServerImports].reverse()) {
221
+ // Preserve __moduleKey in the client stub so callers (e.g.
222
+ // defineRoutes' wrapWithRouteLocations) can identify which server
223
+ // module this lazy import represents, even though the body is
224
+ // replaced with an empty resolved promise.
225
+ let stubContent = '{}';
226
+ if (viteRoot !== undefined) {
227
+ const absServerPath = path.resolve(importerDir, imp.source);
228
+ const moduleKey = deriveModuleKey(absServerPath, viteRoot);
229
+ if (!moduleKey.startsWith('..')) {
230
+ stubContent = `{ __moduleKey: ${JSON.stringify(moduleKey)} }`;
231
+ }
232
+ }
233
+ s.overwrite(imp.start, imp.end, `Promise.resolve(${stubContent})`);
234
+ }
235
+ if (needsCreateLoaderStubImport) {
236
+ s.prepend(`import { __$createLoaderStub_hpiso } from 'hono-preact/internal';\n`);
237
+ }
238
+ if (needsUseActionImport) {
239
+ s.prepend(`import { useAction as __$useAction_hpiso } from 'hono-preact';\n`);
240
+ }
241
+ return { code: s.toString(), map: s.generateMap({ hires: true }) };
242
+ },
243
+ };
244
+ }
package/dist/vite.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './vite/index';
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC"}
package/dist/vite.js ADDED
@@ -0,0 +1 @@
1
+ export * from './vite/index.js';