@withl5e/l5e 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +24 -0
- package/dist/action.js +10 -0
- package/dist/action.js.map +1 -0
- package/dist/client-D67hK4Yy.js +9 -0
- package/dist/client-D67hK4Yy.js.map +1 -0
- package/dist/entry-server-Ckh6zfgm.js +258 -0
- package/dist/entry-server-Ckh6zfgm.js.map +1 -0
- package/dist/entry-server.js +12 -0
- package/dist/entry-server.js.map +1 -0
- package/dist/generateMetadata-C5QsMS-H.js +144 -0
- package/dist/generateMetadata-C5QsMS-H.js.map +1 -0
- package/dist/index-BIt7MJT9.js +163 -0
- package/dist/index-BIt7MJT9.js.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/island/client.js +5 -0
- package/dist/island/client.js.map +1 -0
- package/dist/island/runtime.js +98 -0
- package/dist/island/runtime.js.map +1 -0
- package/dist/island.js +39 -0
- package/dist/island.js.map +1 -0
- package/dist/jsx-runtime-C2Vw67N2.js +256 -0
- package/dist/jsx-runtime-C2Vw67N2.js.map +1 -0
- package/dist/jsx-runtime.js +26 -0
- package/dist/jsx-runtime.js.map +1 -0
- package/dist/middleware.js +9 -0
- package/dist/middleware.js.map +1 -0
- package/dist/seo.js +7 -0
- package/dist/seo.js.map +1 -0
- package/dist/server.js +489 -0
- package/dist/server.js.map +1 -0
- package/dist/swap/server.js +15 -0
- package/dist/swap/server.js.map +1 -0
- package/dist/swap.js +121 -0
- package/dist/swap.js.map +1 -0
- package/dist/tooltip.js +129 -0
- package/dist/tooltip.js.map +1 -0
- package/dist/vite-plugin.js +381 -0
- package/dist/vite-plugin.js.map +1 -0
- package/index.ts +1 -0
- package/package.json +129 -0
- package/src/action/define-action.ts +8 -0
- package/src/action/index.ts +2 -0
- package/src/action/types.ts +21 -0
- package/src/core/bundler.ts +275 -0
- package/src/core/const.ts +2 -0
- package/src/core/entry-server.d.ts +1 -0
- package/src/core/entry-server.ts +381 -0
- package/src/core/exceptions.ts +80 -0
- package/src/core/head-priority.ts +15 -0
- package/src/core/index.ts +40 -0
- package/src/core/jsx-runtime.ts +325 -0
- package/src/core/jsx-types.d.ts +548 -0
- package/src/core/render.ts +181 -0
- package/src/core/request.ts +31 -0
- package/src/core/server.ts +740 -0
- package/src/core/vite-plugin.ts +779 -0
- package/src/island/ClientIsland.ts +71 -0
- package/src/island/client.ts +3 -0
- package/src/island/index.ts +3 -0
- package/src/island/runtime.ts +149 -0
- package/src/island/strategy-registry.ts +10 -0
- package/src/island/types.ts +28 -0
- package/src/middleware/defineMiddleware.ts +5 -0
- package/src/middleware/index.ts +133 -0
- package/src/middleware/sequence.ts +105 -0
- package/src/middleware/types.ts +28 -0
- package/src/seo/generateMetadata.tsx +559 -0
- package/src/seo/index.ts +10 -0
- package/src/seo/mergeMetadata.ts +200 -0
- package/src/seo/types.ts +316 -0
- package/src/swap/SwapResponse.tsx +16 -0
- package/src/swap/create-swap.ts +121 -0
- package/src/swap/index.ts +8 -0
- package/src/swap/parse.ts +12 -0
- package/src/swap/server.ts +1 -0
- package/src/swap/swap.ts +57 -0
- package/src/swap/types.ts +47 -0
- package/src/swap/utils.ts +7 -0
- package/src/tooltip/index.ts +2 -0
- package/src/tooltip/tooltip-loader.ts +108 -0
- package/src/tooltip/tooltip-runtime.ts +173 -0
- package/types.d.ts +14 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { jsxFactory, registerIsland, type JSXChild, type JSXNode } from '../core/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Derive component name from `from` path.
|
|
5
|
+
* "./react/Counter" → "Counter"
|
|
6
|
+
* "~/features/newsfeed/react/NewsfeedApp" → "NewsfeedApp"
|
|
7
|
+
*/
|
|
8
|
+
function deriveComponentName(from: string): string {
|
|
9
|
+
const segments = from.split('/');
|
|
10
|
+
let filename = segments[segments.length - 1];
|
|
11
|
+
filename = filename.replace(/\.(tsx?|jsx?)$/, '');
|
|
12
|
+
return filename;
|
|
13
|
+
}
|
|
14
|
+
export type MountStrategy = 'load' | 'idle' | 'visible' | 'media' | 'none';
|
|
15
|
+
export interface ClientIslandProps {
|
|
16
|
+
from: string;
|
|
17
|
+
props?: Record<string, any>;
|
|
18
|
+
mount?: MountStrategy | string;
|
|
19
|
+
mountOpts?: string;
|
|
20
|
+
class?: string;
|
|
21
|
+
id?: string;
|
|
22
|
+
children?: JSXChild | JSXChild[];
|
|
23
|
+
|
|
24
|
+
/** INTERNAL — injected by vite-plugin. Format: "[name]_[hash]" */
|
|
25
|
+
__key?: string;
|
|
26
|
+
/** INTERNAL — injected by vite-plugin. Manifest-compatible source path with extension */
|
|
27
|
+
__src?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function ClientIsland(attrs: ClientIslandProps): JSXNode {
|
|
31
|
+
const {
|
|
32
|
+
from,
|
|
33
|
+
props = {},
|
|
34
|
+
mount = 'load',
|
|
35
|
+
mountOpts,
|
|
36
|
+
class: className,
|
|
37
|
+
id,
|
|
38
|
+
children,
|
|
39
|
+
__key,
|
|
40
|
+
__src,
|
|
41
|
+
} = attrs;
|
|
42
|
+
|
|
43
|
+
const componentName = deriveComponentName(from);
|
|
44
|
+
|
|
45
|
+
// Register island in render context (like useClientJs)
|
|
46
|
+
// server.ts will use this to generate per-page window.__L5E_ISLANDS__
|
|
47
|
+
if (__key && __src) {
|
|
48
|
+
registerIsland(__key, __src, componentName);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const dataAttrs: Record<string, string> = {
|
|
52
|
+
'data-island': __key || 'unresolved',
|
|
53
|
+
'data-island-name': componentName,
|
|
54
|
+
'data-island-props': JSON.stringify(props),
|
|
55
|
+
'data-island-mount': mount,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (mountOpts) {
|
|
59
|
+
dataAttrs['data-island-opts'] = mountOpts;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return jsxFactory(
|
|
63
|
+
'div',
|
|
64
|
+
{
|
|
65
|
+
...dataAttrs,
|
|
66
|
+
...(id ? { id } : {}),
|
|
67
|
+
class: className ? `l5e-island ${className}` : 'l5e-island',
|
|
68
|
+
},
|
|
69
|
+
children,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { IslandMeta } from './types';
|
|
2
|
+
import { strategies, registerMountStrategy } from './strategy-registry';
|
|
3
|
+
|
|
4
|
+
// ============================================================
|
|
5
|
+
// PART 1: Built-in strategies
|
|
6
|
+
// ============================================================
|
|
7
|
+
|
|
8
|
+
registerMountStrategy('load', (mount) => {
|
|
9
|
+
mount();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
registerMountStrategy('idle', (mount) => {
|
|
13
|
+
if ('requestIdleCallback' in window) {
|
|
14
|
+
requestIdleCallback(() => mount());
|
|
15
|
+
} else {
|
|
16
|
+
setTimeout(() => mount(), 200);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
registerMountStrategy('visible', (mount, opts, el) => {
|
|
21
|
+
const rootMargin = opts || '200px';
|
|
22
|
+
const observer = new IntersectionObserver(
|
|
23
|
+
(entries) => {
|
|
24
|
+
entries.forEach((entry) => {
|
|
25
|
+
if (entry.isIntersecting) {
|
|
26
|
+
observer.disconnect();
|
|
27
|
+
mount();
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
{ rootMargin },
|
|
32
|
+
);
|
|
33
|
+
observer.observe(el);
|
|
34
|
+
return () => observer.disconnect();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
registerMountStrategy('media', (mount, opts) => {
|
|
38
|
+
if (!opts) {
|
|
39
|
+
console.error('[l5e-island] Strategy "media" requires mountOpts (media query)');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const mql = window.matchMedia(opts);
|
|
43
|
+
if (mql.matches) {
|
|
44
|
+
mount();
|
|
45
|
+
} else {
|
|
46
|
+
const handler = (e: MediaQueryListEvent) => {
|
|
47
|
+
if (e.matches) mount();
|
|
48
|
+
};
|
|
49
|
+
mql.addEventListener('change', handler, { once: true });
|
|
50
|
+
return () => mql.removeEventListener('change', handler);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
registerMountStrategy('none', () => {
|
|
55
|
+
// Don't mount - keep placeholder as-is
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ============================================================
|
|
59
|
+
// PART 2: Import custom strategies (if any)
|
|
60
|
+
// ============================================================
|
|
61
|
+
|
|
62
|
+
import 'virtual:l5e-island-strategies';
|
|
63
|
+
|
|
64
|
+
// ============================================================
|
|
65
|
+
// PART 3: Island discovery + mount logic
|
|
66
|
+
// ============================================================
|
|
67
|
+
|
|
68
|
+
// Per-page island registry injected by server.ts as inline script
|
|
69
|
+
// Format: { "Counter_a3f2": "/assets/Counter-Abc123.js" }
|
|
70
|
+
const islandRegistry: Record<string, string> = (window as any).__L5E_ISLANDS__ || {};
|
|
71
|
+
|
|
72
|
+
function discoverIslands(): IslandMeta[] {
|
|
73
|
+
return Array.from(document.querySelectorAll('[data-island]')).map((el) => ({
|
|
74
|
+
element: el as HTMLElement,
|
|
75
|
+
registryKey: el.getAttribute('data-island')!,
|
|
76
|
+
exportName: el.getAttribute('data-island-name')!,
|
|
77
|
+
props: JSON.parse(el.getAttribute('data-island-props') || '{}'),
|
|
78
|
+
mount: el.getAttribute('data-island-mount') || 'load',
|
|
79
|
+
mountOpts: el.getAttribute('data-island-opts') || undefined,
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function createMountFn(island: IslandMeta): () => Promise<void> {
|
|
84
|
+
let mounted = false;
|
|
85
|
+
return async () => {
|
|
86
|
+
if (mounted) return;
|
|
87
|
+
mounted = true;
|
|
88
|
+
|
|
89
|
+
// Look up component URL from per-page registry
|
|
90
|
+
const url = islandRegistry[island.registryKey];
|
|
91
|
+
if (!url) {
|
|
92
|
+
console.error(
|
|
93
|
+
`[l5e-island] Component "${island.registryKey}" not found in page registry.`,
|
|
94
|
+
`Available: ${Object.keys(islandRegistry).join(', ')}`,
|
|
95
|
+
);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const [{ createRoot }, { createElement }, mod] = await Promise.all([
|
|
101
|
+
import('react-dom/client'),
|
|
102
|
+
import('react'),
|
|
103
|
+
import(/* @vite-ignore */ url),
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
const Component = mod.default || mod[island.exportName];
|
|
107
|
+
if (!Component) {
|
|
108
|
+
console.error(`[l5e-island] No export "default" or "${island.exportName}" in module`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const root = createRoot(island.element);
|
|
113
|
+
root.render(createElement(Component, island.props));
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error(`[l5e-island] Failed to mount "${island.registryKey}":`, error);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function scheduleMount(island: IslandMeta) {
|
|
121
|
+
const strategy = strategies.get(island.mount);
|
|
122
|
+
|
|
123
|
+
if (!strategy) {
|
|
124
|
+
console.error(
|
|
125
|
+
`[l5e-island] Strategy "${island.mount}" not found.`,
|
|
126
|
+
`Available: ${[...strategies.keys()].join(', ')}`,
|
|
127
|
+
`\nDid you forget to registerMountStrategy("${island.mount}", ...)?`,
|
|
128
|
+
);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const mountFn = createMountFn(island);
|
|
133
|
+
strategy(mountFn, island.mountOpts, island.element);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ============================================================
|
|
137
|
+
// PART 4: Boot
|
|
138
|
+
// ============================================================
|
|
139
|
+
|
|
140
|
+
function boot() {
|
|
141
|
+
const islands = discoverIslands();
|
|
142
|
+
islands.forEach(scheduleMount);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (document.readyState === 'loading') {
|
|
146
|
+
document.addEventListener('DOMContentLoaded', boot);
|
|
147
|
+
} else {
|
|
148
|
+
boot();
|
|
149
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { MountStrategy } from './types';
|
|
2
|
+
|
|
3
|
+
export const strategies = new Map<string, MountStrategy>();
|
|
4
|
+
|
|
5
|
+
export function registerMountStrategy(name: string, fn: MountStrategy) {
|
|
6
|
+
if (strategies.has(name)) {
|
|
7
|
+
console.warn(`[l5e-island] Strategy "${name}" already registered, overwriting.`);
|
|
8
|
+
}
|
|
9
|
+
strategies.set(name, fn);
|
|
10
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mount strategy function signature
|
|
3
|
+
*
|
|
4
|
+
* @param mount - Call when you want to load JS + mount React component
|
|
5
|
+
* @param opts - Value from mountOpts prop (string | undefined)
|
|
6
|
+
* @param el - DOM element containing the island
|
|
7
|
+
* @returns - Optional cleanup function (called when island is removed)
|
|
8
|
+
*/
|
|
9
|
+
export type MountStrategy = (
|
|
10
|
+
mount: () => Promise<void>,
|
|
11
|
+
opts: string | undefined,
|
|
12
|
+
el: HTMLElement,
|
|
13
|
+
) => void | (() => void);
|
|
14
|
+
|
|
15
|
+
export interface IslandMeta {
|
|
16
|
+
element: HTMLElement;
|
|
17
|
+
registryKey: string; // "Counter_a3f2" - to find loader in registry
|
|
18
|
+
exportName: string; // "Counter" - to find export in module
|
|
19
|
+
props: Record<string, any>;
|
|
20
|
+
mount: string; // Strategy name
|
|
21
|
+
mountOpts?: string; // Options passed to strategy function
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface IslandEntry {
|
|
25
|
+
component: string; // "Counter" (derived from resolvedPath filename)
|
|
26
|
+
resolvedPath: string; // "/src/views/test-island/react/Counter"
|
|
27
|
+
key: string; // "Counter_a3f2"
|
|
28
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { parseCookies } from '../core/request';
|
|
2
|
+
import { defineMiddleware } from './defineMiddleware';
|
|
3
|
+
import { sequence } from './sequence';
|
|
4
|
+
import type { CreateContext, MiddlewareContext, RewritePayload } from './types';
|
|
5
|
+
|
|
6
|
+
export type {
|
|
7
|
+
CreateContext,
|
|
8
|
+
MiddlewareContext,
|
|
9
|
+
MiddlewareHandler,
|
|
10
|
+
MiddlewareNext,
|
|
11
|
+
RewritePayload,
|
|
12
|
+
} from './types';
|
|
13
|
+
|
|
14
|
+
export { defineMiddleware, sequence };
|
|
15
|
+
|
|
16
|
+
export function createContext({
|
|
17
|
+
request,
|
|
18
|
+
requestInfo,
|
|
19
|
+
locals = {},
|
|
20
|
+
clientAddress,
|
|
21
|
+
}: CreateContext): MiddlewareContext {
|
|
22
|
+
const context = {
|
|
23
|
+
cookies: parseCookies(request.headers.get('cookie') ?? undefined),
|
|
24
|
+
request,
|
|
25
|
+
requestInfo,
|
|
26
|
+
url: new URL(request.url),
|
|
27
|
+
redirect(path: string | URL, status = 302) {
|
|
28
|
+
return new Response(null, {
|
|
29
|
+
status,
|
|
30
|
+
headers: {
|
|
31
|
+
Location: path.toString(),
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
rewrite(payload: RewritePayload) {
|
|
36
|
+
return Promise.resolve(new Response(null));
|
|
37
|
+
},
|
|
38
|
+
get clientAddress() {
|
|
39
|
+
if (clientAddress) {
|
|
40
|
+
return clientAddress;
|
|
41
|
+
}
|
|
42
|
+
throw new Error('clientAddress is not available for this request.');
|
|
43
|
+
},
|
|
44
|
+
} as Omit<MiddlewareContext, 'locals'> & {
|
|
45
|
+
locals: Record<string, unknown>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
Object.defineProperty(context, 'locals', {
|
|
49
|
+
enumerable: true,
|
|
50
|
+
get() {
|
|
51
|
+
if (typeof locals !== 'object' || locals === null || Array.isArray(locals)) {
|
|
52
|
+
throw new Error('Middleware locals must be a plain object.');
|
|
53
|
+
}
|
|
54
|
+
return locals;
|
|
55
|
+
},
|
|
56
|
+
set() {
|
|
57
|
+
throw new Error('Middleware locals cannot be reassigned. Mutate its properties instead.');
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
context.rewrite = (payload: RewritePayload) => {
|
|
62
|
+
return Promise.resolve(
|
|
63
|
+
new Response(null, {
|
|
64
|
+
status: 307,
|
|
65
|
+
headers: {
|
|
66
|
+
'X-L5E-Rewrite': stringifyRewritePayload(payload, context.url),
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return context;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function isLocalsSerializable(value: unknown): boolean {
|
|
76
|
+
const stack: unknown[] = [value];
|
|
77
|
+
|
|
78
|
+
while (stack.length > 0) {
|
|
79
|
+
const current = stack.pop();
|
|
80
|
+
const type = typeof current;
|
|
81
|
+
|
|
82
|
+
if (current === null || type === 'string' || type === 'number' || type === 'boolean') {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (Array.isArray(current)) {
|
|
87
|
+
stack.push(...current);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (type === 'object' && isPlainObject(current)) {
|
|
92
|
+
stack.push(...Object.values(current as Record<string, unknown>));
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isPlainObject(value: unknown): value is object {
|
|
103
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
104
|
+
|
|
105
|
+
const proto = Object.getPrototypeOf(value);
|
|
106
|
+
if (proto === null) return true;
|
|
107
|
+
|
|
108
|
+
let baseProto = proto;
|
|
109
|
+
while (Object.getPrototypeOf(baseProto) !== null) {
|
|
110
|
+
baseProto = Object.getPrototypeOf(baseProto);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return proto === baseProto;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function trySerializeLocals(value: unknown): string {
|
|
117
|
+
if (isLocalsSerializable(value)) {
|
|
118
|
+
return JSON.stringify(value);
|
|
119
|
+
}
|
|
120
|
+
throw new Error("The passed value can't be serialized.");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function stringifyRewritePayload(payload: RewritePayload, currentUrl: URL): string {
|
|
124
|
+
if (payload instanceof Request) {
|
|
125
|
+
return payload.url;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (payload instanceof URL) {
|
|
129
|
+
return payload.href;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return new URL(payload, currentUrl).href;
|
|
133
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { parseCookies } from '../core/request';
|
|
2
|
+
import { defineMiddleware } from './defineMiddleware';
|
|
3
|
+
import type { MiddlewareContext, MiddlewareHandler, RewritePayload } from './types';
|
|
4
|
+
|
|
5
|
+
// From SvelteKit via Astro: compose middleware handlers in declaration order.
|
|
6
|
+
export function sequence(...handlers: Array<MiddlewareHandler | false | null | undefined>) {
|
|
7
|
+
const filtered = handlers.filter(Boolean) as MiddlewareHandler[];
|
|
8
|
+
const length = filtered.length;
|
|
9
|
+
|
|
10
|
+
if (!length) {
|
|
11
|
+
return defineMiddleware((_context, next) => {
|
|
12
|
+
return next();
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return defineMiddleware((context, next) => {
|
|
17
|
+
let carriedPayload: RewritePayload | undefined;
|
|
18
|
+
return applyHandle(0, context);
|
|
19
|
+
|
|
20
|
+
function applyHandle(
|
|
21
|
+
i: number,
|
|
22
|
+
handleContext: MiddlewareContext,
|
|
23
|
+
): Promise<Response> | Response {
|
|
24
|
+
const handle = filtered[i];
|
|
25
|
+
|
|
26
|
+
return handle(handleContext, async (payload?: RewritePayload) => {
|
|
27
|
+
if (i < length - 1) {
|
|
28
|
+
if (payload) {
|
|
29
|
+
carriedPayload = payload;
|
|
30
|
+
updateContextForRewrite(handleContext, payload);
|
|
31
|
+
}
|
|
32
|
+
return applyHandle(i + 1, handleContext);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return next(payload ?? carriedPayload);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function updateContextForRewrite(context: MiddlewareContext, payload: RewritePayload): void {
|
|
42
|
+
let request: Request;
|
|
43
|
+
|
|
44
|
+
if (payload instanceof Request) {
|
|
45
|
+
request = payload;
|
|
46
|
+
} else if (payload instanceof URL) {
|
|
47
|
+
request = new Request(payload.href, context.request.clone());
|
|
48
|
+
} else {
|
|
49
|
+
request = new Request(new URL(payload, context.url).href, context.request.clone());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const previousUrl = context.url;
|
|
53
|
+
const nextUrl = new URL(request.url);
|
|
54
|
+
const nextCookies = parseCookies(request.headers.get('cookie') ?? undefined);
|
|
55
|
+
context.request = request;
|
|
56
|
+
context.url = nextUrl;
|
|
57
|
+
context.cookies = nextCookies;
|
|
58
|
+
|
|
59
|
+
if (context.requestInfo) {
|
|
60
|
+
const headers: Record<string, string> = {};
|
|
61
|
+
request.headers.forEach((value, key) => {
|
|
62
|
+
headers[key] = value;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
context.requestInfo = {
|
|
66
|
+
...context.requestInfo,
|
|
67
|
+
url: nextUrl,
|
|
68
|
+
path: getRequestInfoPath(nextUrl, previousUrl, context.requestInfo.path),
|
|
69
|
+
pathname: nextUrl.pathname,
|
|
70
|
+
method: request.method,
|
|
71
|
+
headers,
|
|
72
|
+
cookies: nextCookies,
|
|
73
|
+
query: Object.fromEntries(nextUrl.searchParams.entries()),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getRequestInfoPath(nextUrl: URL, previousUrl: URL, previousPath?: string): string {
|
|
79
|
+
const nextPath = `${nextUrl.pathname}${nextUrl.search}` || '/';
|
|
80
|
+
if (!previousPath) {
|
|
81
|
+
return nextPath;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const previousPathname = previousPath.split('?')[0] || '/';
|
|
85
|
+
if (
|
|
86
|
+
previousPathname === previousUrl.pathname ||
|
|
87
|
+
!previousUrl.pathname.endsWith(previousPathname)
|
|
88
|
+
) {
|
|
89
|
+
return nextPath;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const basePathname = previousUrl.pathname.slice(
|
|
93
|
+
0,
|
|
94
|
+
previousUrl.pathname.length - previousPathname.length,
|
|
95
|
+
);
|
|
96
|
+
if (!basePathname || !nextUrl.pathname.startsWith(basePathname)) {
|
|
97
|
+
return nextPath;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const strippedPathname = nextUrl.pathname.slice(basePathname.length) || '/';
|
|
101
|
+
const normalizedPathname = strippedPathname.startsWith('/')
|
|
102
|
+
? strippedPathname
|
|
103
|
+
: `/${strippedPathname}`;
|
|
104
|
+
return `${normalizedPathname}${nextUrl.search}`;
|
|
105
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { RequestInfo } from '../core/entry-server';
|
|
2
|
+
|
|
3
|
+
export type RewritePayload = string | URL | Request;
|
|
4
|
+
|
|
5
|
+
export type MiddlewareNext = (payload?: RewritePayload) => Promise<Response>;
|
|
6
|
+
|
|
7
|
+
export interface CreateContext {
|
|
8
|
+
request: Request;
|
|
9
|
+
requestInfo?: RequestInfo;
|
|
10
|
+
locals?: Record<string, unknown>;
|
|
11
|
+
clientAddress?: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface MiddlewareContext {
|
|
15
|
+
request: Request;
|
|
16
|
+
requestInfo?: RequestInfo;
|
|
17
|
+
url: URL;
|
|
18
|
+
cookies: Record<string, string>;
|
|
19
|
+
locals: Record<string, unknown>;
|
|
20
|
+
redirect: (path: string | URL, status?: number) => Response;
|
|
21
|
+
rewrite: (payload: RewritePayload) => Promise<Response>;
|
|
22
|
+
clientAddress: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type MiddlewareHandler = (
|
|
26
|
+
context: MiddlewareContext,
|
|
27
|
+
next: MiddlewareNext,
|
|
28
|
+
) => Response | Promise<Response>;
|