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,83 @@
1
+ import { streamSSE } from 'hono/streaming';
2
+ function encodeErrorPayload(err) {
3
+ const message = err instanceof Error ? err.message : String(err);
4
+ const name = err instanceof Error ? err.name : 'Error';
5
+ return JSON.stringify({ message, name });
6
+ }
7
+ /**
8
+ * Wrap an async generator as an SSE response.
9
+ *
10
+ * Each yield is JSON-encoded and written as a `data:` event.
11
+ * If `emitResult` is true and the generator's return value is defined,
12
+ * it is written as `event: result\ndata: <json>` before the stream closes.
13
+ * If the generator throws, an `event: error\ndata: {"message","name"}` frame
14
+ * is written and the stream closes cleanly (Hono's default error handler is
15
+ * never invoked because we catch inside the callback).
16
+ */
17
+ export function sseGeneratorResponse(c, gen, options = {}) {
18
+ const { emitResult = false } = options;
19
+ return streamSSE(c, async (stream) => {
20
+ try {
21
+ while (!stream.aborted) {
22
+ const step = await gen.next();
23
+ if (step.done) {
24
+ if (emitResult && step.value !== undefined) {
25
+ await stream.writeSSE({
26
+ event: 'result',
27
+ data: JSON.stringify(step.value),
28
+ });
29
+ }
30
+ return;
31
+ }
32
+ await stream.writeSSE({ data: JSON.stringify(step.value) });
33
+ }
34
+ // Aborted; release the generator cleanly.
35
+ await gen.return(undefined).catch(() => {
36
+ /* swallow */
37
+ });
38
+ }
39
+ catch (err) {
40
+ await gen.return(undefined).catch(() => {
41
+ /* swallow */
42
+ });
43
+ await stream.writeSSE({
44
+ event: 'error',
45
+ data: encodeErrorPayload(err),
46
+ });
47
+ }
48
+ });
49
+ }
50
+ /**
51
+ * Wrap a ReadableStream<T> (with T a JSON-encodable value) as an SSE response.
52
+ * Each enqueued chunk is JSON-encoded and written as a `data:` event.
53
+ */
54
+ export function sseReadableStreamResponse(c, source) {
55
+ return streamSSE(c, async (stream) => {
56
+ const reader = source.getReader();
57
+ try {
58
+ while (!stream.aborted) {
59
+ const { done, value } = await reader.read();
60
+ if (done)
61
+ return;
62
+ await stream.writeSSE({ data: JSON.stringify(value) });
63
+ }
64
+ }
65
+ catch (err) {
66
+ await stream.writeSSE({
67
+ event: 'error',
68
+ data: encodeErrorPayload(err),
69
+ });
70
+ }
71
+ finally {
72
+ reader.cancel().catch(() => {
73
+ /* swallow */
74
+ });
75
+ }
76
+ });
77
+ }
78
+ export function isAsyncGenerator(value) {
79
+ return (value != null &&
80
+ typeof value === 'object' &&
81
+ typeof value[Symbol.asyncIterator] === 'function' &&
82
+ typeof value.next === 'function');
83
+ }
@@ -0,0 +1 @@
1
+ export * from './server/index';
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC"}
package/dist/server.js ADDED
@@ -0,0 +1 @@
1
+ export * from './server/index.js';
@@ -0,0 +1,10 @@
1
+ import type { Plugin } from 'vite';
2
+ export declare const VIRTUAL_CLIENT_ENTRY_ID = "virtual:hono-preact/client";
3
+ export interface GenerateClientEntrySourceOptions {
4
+ routesAbsPath: string;
5
+ }
6
+ export declare function generateClientEntrySource(opts: GenerateClientEntrySourceOptions): string;
7
+ export interface ClientEntryPluginOptions {
8
+ routes: string;
9
+ }
10
+ export declare function clientEntryPlugin(opts: ClientEntryPluginOptions): Plugin;
@@ -0,0 +1,47 @@
1
+ import * as path from 'node:path';
2
+ export const VIRTUAL_CLIENT_ENTRY_ID = 'virtual:hono-preact/client';
3
+ const RESOLVED_ID = '\0' + VIRTUAL_CLIENT_ENTRY_ID;
4
+ export function generateClientEntrySource(opts) {
5
+ return (`import { h, hydrate } from 'preact';\n` +
6
+ `import { LocationProvider } from 'preact-iso';\n` +
7
+ `import { Routes } from 'hono-preact';\n` +
8
+ `import { __dispatchRouteChange, installStreamRegistry } from 'hono-preact/internal';\n` +
9
+ `import routes from '${opts.routesAbsPath}';\n` +
10
+ `\n` +
11
+ `installStreamRegistry();\n` +
12
+ `\n` +
13
+ `let lastPath;\n` +
14
+ `function onRouteChange(path) {\n` +
15
+ ` const from = lastPath;\n` +
16
+ ` lastPath = path;\n` +
17
+ ` __dispatchRouteChange(path, from);\n` +
18
+ `}\n` +
19
+ `\n` +
20
+ `hydrate(\n` +
21
+ ` h(LocationProvider, null,\n` +
22
+ ` h(Routes, { routes, onRouteChange })\n` +
23
+ ` ),\n` +
24
+ ` document.getElementById('app')\n` +
25
+ `);\n`);
26
+ }
27
+ export function clientEntryPlugin(opts) {
28
+ let routesAbsPath = '';
29
+ return {
30
+ name: 'hono-preact:client-entry',
31
+ enforce: 'pre',
32
+ configResolved(config) {
33
+ routesAbsPath = path.isAbsolute(opts.routes)
34
+ ? opts.routes
35
+ : path.resolve(config.root, opts.routes);
36
+ },
37
+ resolveId(id) {
38
+ if (id === VIRTUAL_CLIENT_ENTRY_ID)
39
+ return RESOLVED_ID;
40
+ },
41
+ load(id) {
42
+ if (id !== RESOLVED_ID)
43
+ return;
44
+ return generateClientEntrySource({ routesAbsPath });
45
+ },
46
+ };
47
+ }
@@ -0,0 +1,12 @@
1
+ import type { Plugin } from 'vite';
2
+ /**
3
+ * Defines a minimal `process` object on the global so libraries that read
4
+ * `process.env.NODE_ENV` at module-evaluation time in the browser do not
5
+ * throw a `ReferenceError`. The shim is `??=` so it never clobbers an
6
+ * existing `process` (other shims, polyfills, runtime injection).
7
+ *
8
+ * Mounted by transforming the configured client entry to prepend an import
9
+ * of a virtual module that carries the shim source. This guarantees the
10
+ * shim runs first regardless of the order of the user's other imports.
11
+ */
12
+ export declare function clientShimPlugin(clientEntry: string): Plugin;
@@ -0,0 +1,62 @@
1
+ import path from 'node:path';
2
+ const VIRTUAL_ID = 'virtual:hono-preact/client-shim';
3
+ const RESOLVED_ID = '\0' + VIRTUAL_ID;
4
+ /**
5
+ * Defines a minimal `process` object on the global so libraries that read
6
+ * `process.env.NODE_ENV` at module-evaluation time in the browser do not
7
+ * throw a `ReferenceError`. The shim is `??=` so it never clobbers an
8
+ * existing `process` (other shims, polyfills, runtime injection).
9
+ *
10
+ * Mounted by transforming the configured client entry to prepend an import
11
+ * of a virtual module that carries the shim source. This guarantees the
12
+ * shim runs first regardless of the order of the user's other imports.
13
+ */
14
+ export function clientShimPlugin(clientEntry) {
15
+ let resolvedEntry = null;
16
+ let shimSource = '';
17
+ return {
18
+ name: 'hono-preact:client-shim',
19
+ enforce: 'pre',
20
+ apply(_, { command, mode }) {
21
+ // Inject during dev (`vite serve`) and the client build only. The SSR
22
+ // build runs in Node/Workers and does not need the shim.
23
+ return command === 'serve' || (command === 'build' && mode === 'client');
24
+ },
25
+ configResolved(config) {
26
+ resolvedEntry = path.resolve(config.root, clientEntry);
27
+ const nodeEnv = config.isProduction ? 'production' : 'development';
28
+ shimSource = `globalThis.process ??= { env: { NODE_ENV: ${JSON.stringify(nodeEnv)} } };\n`;
29
+ },
30
+ resolveId(id) {
31
+ if (id === VIRTUAL_ID)
32
+ return RESOLVED_ID;
33
+ },
34
+ load(id) {
35
+ if (id === RESOLVED_ID)
36
+ return shimSource;
37
+ },
38
+ transform(code, id) {
39
+ if (resolvedEntry === null)
40
+ return;
41
+ // Virtual client entry: matches the resolved virtual id directly. The
42
+ // configured `clientEntry` carries the unresolved `virtual:` form, which
43
+ // we mirror here so the shim still injects.
44
+ if (clientEntry.startsWith('virtual:') && id === '\0' + clientEntry) {
45
+ return {
46
+ code: `import '${VIRTUAL_ID}';\n${code}`,
47
+ map: null,
48
+ };
49
+ }
50
+ // Disk-based entry: equal, or `<entry>?<query>`.
51
+ if (!id.startsWith(resolvedEntry))
52
+ return;
53
+ const tail = id.length - resolvedEntry.length;
54
+ if (tail !== 0 && id.charCodeAt(resolvedEntry.length) !== 63 /* '?' */)
55
+ return;
56
+ return {
57
+ code: `import '${VIRTUAL_ID}';\n${code}`,
58
+ map: null,
59
+ };
60
+ },
61
+ };
62
+ }
@@ -0,0 +1,2 @@
1
+ import type { Plugin } from 'vite';
2
+ export declare function guardStripPlugin(): Plugin;
@@ -0,0 +1,96 @@
1
+ import { parse } from '@babel/parser';
2
+ import MagicString from 'magic-string';
3
+ import { BABEL_PARSER_PLUGINS } from './parser-options.js';
4
+ const ISO_PACKAGE_SOURCES = new Set(['../iso/index.js', 'hono-preact']);
5
+ const NOOP_IMPORT_SOURCE = 'hono-preact/internal';
6
+ const NOOP_LOCAL_NAME = '__$guardNoop_hpiso';
7
+ function collectLocalBindings(ast, targets) {
8
+ const bindings = new Map();
9
+ for (const node of ast.program.body) {
10
+ if (node.type !== 'ImportDeclaration')
11
+ continue;
12
+ const imp = node;
13
+ if (!ISO_PACKAGE_SOURCES.has(imp.source.value))
14
+ continue;
15
+ for (const spec of imp.specifiers) {
16
+ if (spec.type !== 'ImportSpecifier')
17
+ continue;
18
+ if (spec.imported.type !== 'Identifier')
19
+ continue;
20
+ const name = spec.imported.name;
21
+ if (name === 'defineServerGuard' || name === 'defineClientGuard') {
22
+ if (targets.has(name)) {
23
+ bindings.set(spec.local.name, name);
24
+ }
25
+ }
26
+ }
27
+ }
28
+ return bindings;
29
+ }
30
+ function findCallsByLocalName(node, bindings, hits) {
31
+ if (!node || typeof node !== 'object')
32
+ return;
33
+ if (Array.isArray(node)) {
34
+ for (const child of node)
35
+ findCallsByLocalName(child, bindings, hits);
36
+ return;
37
+ }
38
+ const n = node;
39
+ if (n.type === 'CallExpression' &&
40
+ n.callee?.type === 'Identifier' &&
41
+ n.callee.name &&
42
+ bindings.has(n.callee.name) &&
43
+ n.arguments &&
44
+ n.arguments.length >= 1 &&
45
+ n.arguments[0].start !== undefined &&
46
+ n.arguments[0].end !== undefined) {
47
+ hits.push({
48
+ start: n.start,
49
+ end: n.end,
50
+ argStart: n.arguments[0].start,
51
+ argEnd: n.arguments[0].end,
52
+ });
53
+ }
54
+ for (const key of Object.keys(node)) {
55
+ if (key === 'loc' ||
56
+ key === 'leadingComments' ||
57
+ key === 'trailingComments')
58
+ continue;
59
+ findCallsByLocalName(node[key], bindings, hits);
60
+ }
61
+ }
62
+ export function guardStripPlugin() {
63
+ return {
64
+ name: 'hono-preact:guard-strip',
65
+ enforce: 'pre',
66
+ transform(code, id, options) {
67
+ if (!/\.[jt]sx?$/.test(id))
68
+ return;
69
+ if (/\.server\.[jt]sx?$/.test(id))
70
+ return;
71
+ const stripping = options?.ssr
72
+ ? 'defineClientGuard'
73
+ : 'defineServerGuard';
74
+ if (!code.includes(stripping))
75
+ return;
76
+ const ast = parse(code, {
77
+ sourceType: 'module',
78
+ plugins: BABEL_PARSER_PLUGINS,
79
+ errorRecovery: true,
80
+ });
81
+ const bindings = collectLocalBindings(ast, new Set([stripping]));
82
+ if (bindings.size === 0)
83
+ return;
84
+ const hits = [];
85
+ findCallsByLocalName(ast.program, bindings, hits);
86
+ if (hits.length === 0)
87
+ return;
88
+ const s = new MagicString(code);
89
+ for (const hit of [...hits].reverse()) {
90
+ s.overwrite(hit.argStart, hit.argEnd, NOOP_LOCAL_NAME);
91
+ }
92
+ s.prepend(`import { ${NOOP_LOCAL_NAME} } from '${NOOP_IMPORT_SOURCE}';\n`);
93
+ return { code: s.toString(), map: s.generateMap({ hires: true }) };
94
+ },
95
+ };
96
+ }
@@ -0,0 +1,12 @@
1
+ import { type BuildEnvironmentOptions, type Plugin } from 'vite';
2
+ export interface HonoPreactOptions {
3
+ layout?: string;
4
+ routes?: string;
5
+ api?: string;
6
+ clientEntry?: string;
7
+ entry?: string;
8
+ clientBuild?: BuildEnvironmentOptions;
9
+ serverBuild?: BuildEnvironmentOptions;
10
+ sharedBuild?: BuildEnvironmentOptions;
11
+ }
12
+ export declare function honoPreact(options?: HonoPreactOptions): Plugin[];
@@ -0,0 +1,111 @@
1
+ import build from '@hono/vite-build/cloudflare-workers';
2
+ import devServer, { defaultOptions } from '@hono/vite-dev-server';
3
+ import cloudflareAdapter from '@hono/vite-dev-server/cloudflare';
4
+ import preact from '@preact/preset-vite';
5
+ import { clientShimPlugin } from './client-shim.js';
6
+ import { clientEntryPlugin, VIRTUAL_CLIENT_ENTRY_ID } from './client-entry.js';
7
+ import { serverLoaderValidationPlugin } from './server-loader-validation.js';
8
+ import { moduleKeyPlugin } from './module-key-plugin.js';
9
+ import { serverOnlyPlugin } from './server-only.js';
10
+ import { guardStripPlugin } from './guard-strip.js';
11
+ import { GENERATED_SERVER_ENTRY_RELATIVE, generatedServerEntryAbsPath, serverEntryPlugin, } from './server-entry.js';
12
+ export function honoPreact(options = {}) {
13
+ const { layout = 'src/Layout.tsx', routes = 'src/routes.ts', api = 'src/api.ts', clientEntry = VIRTUAL_CLIENT_ENTRY_ID, entry, clientBuild = {}, serverBuild = {}, sharedBuild = {}, } = options;
14
+ const useGeneratedEntry = entry === undefined;
15
+ const resolvedEntry = entry ?? GENERATED_SERVER_ENTRY_RELATIVE;
16
+ const configPlugin = {
17
+ name: 'hono-preact:config',
18
+ config(_, { mode }) {
19
+ const shared = {
20
+ resolve: {
21
+ dedupe: ['preact', 'preact/compat', 'preact/hooks', 'preact-iso'],
22
+ },
23
+ build: {
24
+ target: 'esnext',
25
+ assetsDir: 'static',
26
+ ssrEmitAssets: true,
27
+ minify: true,
28
+ ...sharedBuild,
29
+ },
30
+ };
31
+ if (mode === 'client') {
32
+ const { rollupOptions: userRollup, ...restClientBuild } = clientBuild;
33
+ return {
34
+ ...shared,
35
+ build: {
36
+ ...shared.build,
37
+ sourcemap: true,
38
+ cssCodeSplit: true,
39
+ copyPublicDir: false,
40
+ ...restClientBuild,
41
+ rollupOptions: {
42
+ input: userRollup?.input ?? [clientEntry],
43
+ output: {
44
+ entryFileNames: 'static/client.js',
45
+ chunkFileNames: 'static/[name]-[hash].js',
46
+ assetFileNames: 'static/[name]-[hash].[ext]',
47
+ // Array-form output is not supported; use an OutputOptions object to
48
+ // override individual fields (entryFileNames, chunkFileNames, etc.).
49
+ ...(userRollup?.output && !Array.isArray(userRollup.output)
50
+ ? userRollup.output
51
+ : {}),
52
+ },
53
+ },
54
+ },
55
+ };
56
+ }
57
+ return {
58
+ ...shared,
59
+ ssr: {
60
+ noExternal: ['preact-render-to-string', 'preact-iso', 'hono-preact'],
61
+ },
62
+ build: {
63
+ ...shared.build,
64
+ // Inline sourcemaps so SSR error stacks point at user source
65
+ // (e.g. `pages/issue.server.ts:42`) instead of the rolled-up
66
+ // Worker chunk. We pick `inline` over a separate .map file because
67
+ // SSR is a single bundle and Workers tooling will not look at a
68
+ // sibling .map. Bundle size grows; for typical app server bundles
69
+ // this is a few hundred KB at most and is worth the debuggability.
70
+ // Users can override by passing `serverBuild.sourcemap: false`.
71
+ sourcemap: 'inline',
72
+ ...serverBuild,
73
+ },
74
+ };
75
+ },
76
+ };
77
+ return [
78
+ configPlugin,
79
+ clientShimPlugin(clientEntry),
80
+ clientEntryPlugin({ routes }),
81
+ ...(useGeneratedEntry
82
+ ? [
83
+ serverEntryPlugin({
84
+ layout,
85
+ routes,
86
+ api,
87
+ outputPath: generatedServerEntryAbsPath(),
88
+ }),
89
+ ]
90
+ : []),
91
+ serverLoaderValidationPlugin(),
92
+ moduleKeyPlugin(),
93
+ serverOnlyPlugin(),
94
+ guardStripPlugin(),
95
+ Object.assign(build({ entry: resolvedEntry }), {
96
+ apply: (_, { command, mode }) => command === 'build' && mode !== 'client',
97
+ }),
98
+ Object.assign(devServer({
99
+ entry: resolvedEntry,
100
+ exclude: [
101
+ ...defaultOptions.exclude,
102
+ /\.scss/,
103
+ /\.css/,
104
+ /\?url/,
105
+ /\?inline/,
106
+ ],
107
+ adapter: cloudflareAdapter,
108
+ }), { apply: 'serve' }),
109
+ ...preact(),
110
+ ];
111
+ }
@@ -0,0 +1,7 @@
1
+ export { honoPreact } from './hono-preact.js';
2
+ export { serverLoaderValidationPlugin } from './server-loader-validation.js';
3
+ export { serverOnlyPlugin, VITE_ROOT_ACCESSOR } from './server-only.js';
4
+ export { moduleKeyPlugin } from './module-key-plugin.js';
5
+ export { GENERATED_SERVER_ENTRY_RELATIVE, generatedServerEntryAbsPath, serverEntryPlugin, } from './server-entry.js';
6
+ export { clientEntryPlugin, VIRTUAL_CLIENT_ENTRY_ID } from './client-entry.js';
7
+ export { guardStripPlugin } from './guard-strip.js';
@@ -0,0 +1,7 @@
1
+ export { honoPreact } from './hono-preact.js';
2
+ export { serverLoaderValidationPlugin } from './server-loader-validation.js';
3
+ export { serverOnlyPlugin, VITE_ROOT_ACCESSOR } from './server-only.js';
4
+ export { moduleKeyPlugin } from './module-key-plugin.js';
5
+ export { GENERATED_SERVER_ENTRY_RELATIVE, generatedServerEntryAbsPath, serverEntryPlugin, } from './server-entry.js';
6
+ export { clientEntryPlugin, VIRTUAL_CLIENT_ENTRY_ID } from './client-entry.js';
7
+ export { guardStripPlugin } from './guard-strip.js';
@@ -0,0 +1,12 @@
1
+ import type { Plugin } from 'vite';
2
+ /**
3
+ * Transforms `.server.*` files to inject a stable module-level
4
+ * `__moduleKey` export and to thread that key into `defineLoader` calls.
5
+ * The key is path-derived (see `deriveModuleKey`), so it survives builds
6
+ * and HMR, and is unique per file.
7
+ *
8
+ * Pairs with `serverOnlyPlugin`, which transforms client-side imports of
9
+ * `.server.*` files. Both plugins compute the same key from the same
10
+ * absolute path + viteRoot.
11
+ */
12
+ export declare function moduleKeyPlugin(): Plugin;
@@ -0,0 +1,114 @@
1
+ import { parse } from '@babel/parser';
2
+ import MagicString from 'magic-string';
3
+ import { deriveModuleKey } from './module-key.js';
4
+ import { parseServerLoaders } from './server-loaders-parser.js';
5
+ import { BABEL_PARSER_PLUGINS } from './parser-options.js';
6
+ /**
7
+ * Transforms `.server.*` files to inject a stable module-level
8
+ * `__moduleKey` export and to thread that key into `defineLoader` calls.
9
+ * The key is path-derived (see `deriveModuleKey`), so it survives builds
10
+ * and HMR, and is unique per file.
11
+ *
12
+ * Pairs with `serverOnlyPlugin`, which transforms client-side imports of
13
+ * `.server.*` files. Both plugins compute the same key from the same
14
+ * absolute path + viteRoot.
15
+ */
16
+ export function moduleKeyPlugin() {
17
+ let viteRoot;
18
+ return {
19
+ name: 'module-key',
20
+ enforce: 'pre',
21
+ configResolved(config) {
22
+ viteRoot = config.root;
23
+ },
24
+ // transform receives an `options.ssr` argument from Vite, but we want
25
+ // __moduleKey injected into both the client and SSR builds (so the SSR
26
+ // runtime sees the same routing key the handler reads), so we ignore the
27
+ // flag and always run.
28
+ transform(code, id) {
29
+ if (viteRoot === undefined)
30
+ return;
31
+ if (!/\.server\.[jt]sx?$/.test(id))
32
+ return;
33
+ if (!id.startsWith(viteRoot + '/'))
34
+ return;
35
+ if (/^\s*export\s+const\s+__moduleKey\s*=/m.test(code))
36
+ return;
37
+ const key = deriveModuleKey(id, viteRoot);
38
+ const s = new MagicString(code);
39
+ s.prepend(`export const __moduleKey = ${JSON.stringify(key)};\n`);
40
+ // Walk the AST for top-level CallExpressions whose callee is the
41
+ // identifier `defineLoader` and which have exactly one argument.
42
+ // Rewrite to `defineLoader(<arg>, { __moduleKey: '<key>' })`.
43
+ let ast;
44
+ try {
45
+ ast = parse(code, {
46
+ sourceType: 'module',
47
+ plugins: BABEL_PARSER_PLUGINS,
48
+ errorRecovery: true,
49
+ });
50
+ }
51
+ catch {
52
+ // If the file fails to parse we still emit the prepended
53
+ // __moduleKey so the routing layer works even if loader threading
54
+ // doesn't. Surface the parse error to Vite so the user sees it.
55
+ return { code: s.toString(), map: s.generateMap({ hires: true }) };
56
+ }
57
+ // Rewrite a defineLoader call to include __moduleKey and, when inside
58
+ // a serverLoaders object, __loaderName. Handles both the single-arg
59
+ // form (appends a new opts object) and the two-arg form (merges into
60
+ // the existing opts ObjectExpression).
61
+ const visitCallWithName = (node, loaderName) => {
62
+ if (node.callee.type !== 'Identifier' ||
63
+ node.callee.name !== 'defineLoader') {
64
+ return;
65
+ }
66
+ if (node.arguments.length === 0 || node.arguments.length > 2)
67
+ return;
68
+ const fnArg = node.arguments[0];
69
+ if (fnArg.type === 'StringLiteral')
70
+ return; // not a valid defineLoader fn form; skip
71
+ if (node.arguments.length === 1) {
72
+ const insertAt = fnArg.end;
73
+ if (insertAt == null)
74
+ return;
75
+ const namePart = loaderName
76
+ ? `, __loaderName: ${JSON.stringify(loaderName)}`
77
+ : '';
78
+ s.appendRight(insertAt, `, { __moduleKey: ${JSON.stringify(key)}${namePart} }`);
79
+ return;
80
+ }
81
+ // arguments.length === 2: merge __moduleKey/__loaderName into the
82
+ // existing opts object literal. Bail if it isn't an ObjectExpression.
83
+ const optsArg = node.arguments[1];
84
+ if (optsArg.type !== 'ObjectExpression')
85
+ return;
86
+ const insertAt = optsArg.properties[0]?.start ?? optsArg.start + 1;
87
+ const namePart = loaderName
88
+ ? `__loaderName: ${JSON.stringify(loaderName)}, `
89
+ : '';
90
+ s.appendRight(insertAt, `__moduleKey: ${JSON.stringify(key)}, ${namePart}`);
91
+ };
92
+ // Walk serverLoaders entries via the shared parser, then mutate each call.
93
+ for (const entry of parseServerLoaders(ast.program)) {
94
+ visitCallWithName(entry.call, entry.name);
95
+ }
96
+ // Top-level fallthrough: legacy `export const loader = defineLoader(...)`
97
+ // (single-loader path). defineLoader is overwhelmingly used at module
98
+ // scope; we don't recurse into nested function bodies to keep the plugin cheap.
99
+ for (const stmt of ast.program.body) {
100
+ if (stmt.type === 'ExportNamedDeclaration' &&
101
+ stmt.declaration?.type === 'VariableDeclaration') {
102
+ for (const decl of stmt.declaration.declarations) {
103
+ if (decl.id.type === 'Identifier' &&
104
+ decl.id.name !== 'serverLoaders' &&
105
+ decl.init?.type === 'CallExpression') {
106
+ visitCallWithName(decl.init, undefined);
107
+ }
108
+ }
109
+ }
110
+ }
111
+ return { code: s.toString(), map: s.generateMap({ hires: true }) };
112
+ },
113
+ };
114
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Derive the stable module key for a `.server.*` file.
3
+ *
4
+ * The key is the file's path relative to the Vite project root, with the
5
+ * `.server.{ts,tsx,js,jsx}` extension stripped, and path separators
6
+ * normalized to forward slashes (so the key is identical on Windows and
7
+ * POSIX). Used as the routing key for `__loaders`/`__actions` RPC, the
8
+ * payload of `Symbol.for(...)` for `__id`, and the value of the
9
+ * module-level `__moduleKey` export.
10
+ */
11
+ export declare function deriveModuleKey(absPath: string, viteRoot: string): string;
@@ -0,0 +1,20 @@
1
+ import * as path from 'node:path';
2
+ /**
3
+ * Derive the stable module key for a `.server.*` file.
4
+ *
5
+ * The key is the file's path relative to the Vite project root, with the
6
+ * `.server.{ts,tsx,js,jsx}` extension stripped, and path separators
7
+ * normalized to forward slashes (so the key is identical on Windows and
8
+ * POSIX). Used as the routing key for `__loaders`/`__actions` RPC, the
9
+ * payload of `Symbol.for(...)` for `__id`, and the value of the
10
+ * module-level `__moduleKey` export.
11
+ */
12
+ export function deriveModuleKey(absPath, viteRoot) {
13
+ // Normalize backslashes to forward slashes before computing the relative
14
+ // path so that Windows-style inputs work correctly on any platform.
15
+ const normalizedAbs = absPath.replace(/\\/g, '/');
16
+ const normalizedRoot = viteRoot.replace(/\\/g, '/');
17
+ const rel = path.posix.relative(normalizedRoot, normalizedAbs);
18
+ const stripped = rel.replace(/\.server\.[jt]sx?$/, '');
19
+ return stripped;
20
+ }
@@ -0,0 +1,16 @@
1
+ import type { ParserOptions } from '@babel/parser';
2
+ /**
3
+ * Shared `@babel/parser` options for every code-walk in this package.
4
+ *
5
+ * Keep this list permissive: we parse user code (`.server.ts`, `routes.ts`,
6
+ * `api.ts`, etc.), not just framework-authored code, so any stage-3 syntax a
7
+ * user might reasonably write should round-trip. A user who writes
8
+ * `class Foo { @inject() bar }` or `import data from './x.json' with { type: 'json' }`
9
+ * should not get a silent code-walk failure where the framework expected to
10
+ * find a `defineLoader(...)` call and ran past it.
11
+ *
12
+ * Each call site adds `sourceType: 'module'` and `errorRecovery: true`
13
+ * separately because both are universal at our call sites and the
14
+ * `ParserOptions` shape carries them as top-level fields.
15
+ */
16
+ export declare const BABEL_PARSER_PLUGINS: ParserOptions['plugins'];