@hyperspan/framework 1.0.0-alpha.8 → 1.0.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/package.json +3 -6
- package/src/actions.test.ts +24 -83
- package/src/actions.ts +12 -7
- package/src/client/_hs/hyperspan-streaming.client.ts +36 -64
- package/src/client/js.test.ts +200 -0
- package/src/client/js.ts +76 -43
- package/src/cookies.ts +234 -0
- package/src/index.ts +1 -1
- package/src/layout.ts +24 -1
- package/src/middleware.ts +87 -1
- package/src/server.test.ts +116 -1
- package/src/server.ts +101 -41
- package/src/types.ts +83 -32
- package/src/utils.test.ts +196 -0
- package/src/plugins.ts +0 -94
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperspan/framework",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Hyperspan Web Framework",
|
|
5
5
|
"main": "src/server.ts",
|
|
6
6
|
"types": "src/server.ts",
|
|
@@ -37,10 +37,6 @@
|
|
|
37
37
|
"types": "./src/client/js.ts",
|
|
38
38
|
"default": "./src/client/js.ts"
|
|
39
39
|
},
|
|
40
|
-
"./plugins": {
|
|
41
|
-
"types": "./src/plugins.ts",
|
|
42
|
-
"default": "./src/plugins.ts"
|
|
43
|
-
},
|
|
44
40
|
"./actions": {
|
|
45
41
|
"types": "./src/actions.ts",
|
|
46
42
|
"default": "./src/actions.ts"
|
|
@@ -76,7 +72,8 @@
|
|
|
76
72
|
"typescript": "^5.9.3"
|
|
77
73
|
},
|
|
78
74
|
"dependencies": {
|
|
79
|
-
"@hyperspan/html": "^1.0.0
|
|
75
|
+
"@hyperspan/html": "^1.0.0",
|
|
76
|
+
"isbot": "^5.1.32",
|
|
80
77
|
"zod": "^4.1.12"
|
|
81
78
|
}
|
|
82
79
|
}
|
package/src/actions.test.ts
CHANGED
|
@@ -1,92 +1,33 @@
|
|
|
1
1
|
import { test, expect, describe } from 'bun:test';
|
|
2
|
-
import {
|
|
2
|
+
import { createAction } from './actions';
|
|
3
3
|
import { html, render, type HSHtml } from '@hyperspan/html';
|
|
4
4
|
import { createContext } from './server';
|
|
5
5
|
import type { Hyperspan as HS } from './types';
|
|
6
6
|
import * as z from 'zod/v4';
|
|
7
7
|
|
|
8
|
-
describe('formDataToJSON', () => {
|
|
9
|
-
test('formDataToJSON returns empty object for empty FormData', () => {
|
|
10
|
-
const formData = new FormData();
|
|
11
|
-
const result = formDataToJSON(formData);
|
|
12
|
-
|
|
13
|
-
expect(result).toEqual({});
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test('formDataToJSON handles simple FormData object', () => {
|
|
17
|
-
const formData = new FormData();
|
|
18
|
-
formData.append('name', 'John Doe');
|
|
19
|
-
formData.append('email', 'john@example.com');
|
|
20
|
-
formData.append('age', '30');
|
|
21
|
-
|
|
22
|
-
const result = formDataToJSON(formData);
|
|
23
|
-
|
|
24
|
-
expect(result).toEqual({
|
|
25
|
-
name: 'John Doe',
|
|
26
|
-
email: 'john@example.com',
|
|
27
|
-
age: '30',
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test('formDataToJSON handles complex FormData with nested fields', () => {
|
|
32
|
-
const formData = new FormData();
|
|
33
|
-
formData.append('user[firstName]', 'John');
|
|
34
|
-
formData.append('user[lastName]', 'Doe');
|
|
35
|
-
formData.append('user[email]', 'john@example.com');
|
|
36
|
-
formData.append('user[address][street]', '123 Main St');
|
|
37
|
-
formData.append('user[address][city]', 'New York');
|
|
38
|
-
formData.append('user[address][zip]', '10001');
|
|
39
|
-
|
|
40
|
-
const result = formDataToJSON(formData);
|
|
41
|
-
|
|
42
|
-
expect(result).toEqual({
|
|
43
|
-
user: {
|
|
44
|
-
firstName: 'John',
|
|
45
|
-
lastName: 'Doe',
|
|
46
|
-
email: 'john@example.com',
|
|
47
|
-
address: {
|
|
48
|
-
street: '123 Main St',
|
|
49
|
-
city: 'New York',
|
|
50
|
-
zip: '10001',
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
} as any);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test('formDataToJSON handles FormData with array of values', () => {
|
|
57
|
-
const formData = new FormData();
|
|
58
|
-
formData.append('tags', 'javascript');
|
|
59
|
-
formData.append('tags', 'typescript');
|
|
60
|
-
formData.append('tags', 'nodejs');
|
|
61
|
-
formData.append('colors[]', 'red');
|
|
62
|
-
formData.append('colors[]', 'green');
|
|
63
|
-
formData.append('colors[]', 'blue');
|
|
64
|
-
|
|
65
|
-
const result = formDataToJSON(formData);
|
|
66
|
-
|
|
67
|
-
expect(result).toEqual({
|
|
68
|
-
tags: ['javascript', 'typescript', 'nodejs'],
|
|
69
|
-
colors: ['red', 'green', 'blue'],
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
8
|
describe('createAction', () => {
|
|
75
9
|
test('creates an action with a simple form and no schema', async () => {
|
|
76
10
|
const action = createAction({
|
|
77
|
-
|
|
78
|
-
|
|
11
|
+
name: 'test',
|
|
12
|
+
schema: z.object({
|
|
13
|
+
name: z.string().min(1, 'Name is required'),
|
|
14
|
+
}),
|
|
15
|
+
}).form((c) => {
|
|
16
|
+
return html`
|
|
79
17
|
<form>
|
|
80
18
|
<input type="text" name="name" />
|
|
81
19
|
<button type="submit">Submit</button>
|
|
82
20
|
</form>
|
|
83
21
|
`;
|
|
84
|
-
|
|
22
|
+
}).post(async (c, { data }) => {
|
|
23
|
+
return c.res.html(`
|
|
24
|
+
<p>Hello, ${data?.name}!</p>
|
|
25
|
+
`);
|
|
85
26
|
});
|
|
86
27
|
|
|
87
28
|
expect(action).toBeDefined();
|
|
88
29
|
expect(action._kind).toBe('hsAction');
|
|
89
|
-
expect(action.
|
|
30
|
+
expect(action._path()).toContain('/__actions/');
|
|
90
31
|
|
|
91
32
|
// Test render method
|
|
92
33
|
const request = new Request('http://localhost:3000/');
|
|
@@ -106,17 +47,17 @@ describe('createAction', () => {
|
|
|
106
47
|
});
|
|
107
48
|
|
|
108
49
|
const action = createAction({
|
|
50
|
+
name: 'test',
|
|
109
51
|
schema,
|
|
110
|
-
|
|
111
|
-
|
|
52
|
+
}).form((c, { data }) => {
|
|
53
|
+
return html`
|
|
112
54
|
<form>
|
|
113
|
-
<input type="text" name="name"
|
|
114
|
-
<input type="email" name="email"
|
|
55
|
+
<input type="text" name="name" />
|
|
56
|
+
<input type="email" name="email" />
|
|
115
57
|
<button type="submit">Submit</button>
|
|
116
58
|
</form>
|
|
117
59
|
`;
|
|
118
|
-
|
|
119
|
-
}).post(async (c: HS.Context, { data }) => {
|
|
60
|
+
}).post(async (c, { data }) => {
|
|
120
61
|
return c.res.html(`
|
|
121
62
|
<p>Hello, ${data?.name}!</p>
|
|
122
63
|
<p>Your email is ${data?.email}.</p>
|
|
@@ -125,7 +66,7 @@ describe('createAction', () => {
|
|
|
125
66
|
|
|
126
67
|
expect(action).toBeDefined();
|
|
127
68
|
expect(action._kind).toBe('hsAction');
|
|
128
|
-
expect(action.
|
|
69
|
+
expect(action._path()).toContain('/__actions/');
|
|
129
70
|
|
|
130
71
|
// Test render method
|
|
131
72
|
const request = new Request('http://localhost:3000/');
|
|
@@ -142,7 +83,7 @@ describe('createAction', () => {
|
|
|
142
83
|
formData.append('name', 'John Doe');
|
|
143
84
|
formData.append('email', 'john@example.com');
|
|
144
85
|
|
|
145
|
-
const postRequest = new Request(`http://localhost:3000${action.
|
|
86
|
+
const postRequest = new Request(`http://localhost:3000${action._path()}`, {
|
|
146
87
|
method: 'POST',
|
|
147
88
|
body: formData,
|
|
148
89
|
});
|
|
@@ -163,9 +104,10 @@ describe('createAction', () => {
|
|
|
163
104
|
});
|
|
164
105
|
|
|
165
106
|
const action = createAction({
|
|
107
|
+
name: 'test',
|
|
166
108
|
schema,
|
|
167
|
-
|
|
168
|
-
|
|
109
|
+
}).form((c, { data, error }) => {
|
|
110
|
+
return html`
|
|
169
111
|
<form>
|
|
170
112
|
<input type="text" name="name" value="${data?.name || ''}" />
|
|
171
113
|
${error ? html`<div class="error">Validation failed</div>` : ''}
|
|
@@ -173,8 +115,7 @@ describe('createAction', () => {
|
|
|
173
115
|
<button type="submit">Submit</button>
|
|
174
116
|
</form>
|
|
175
117
|
`;
|
|
176
|
-
|
|
177
|
-
}).post(async (c: HS.Context, { data }) => {
|
|
118
|
+
}).post(async (c, { data }) => {
|
|
178
119
|
return c.res.html(`
|
|
179
120
|
<p>Hello, ${data?.name}!</p>
|
|
180
121
|
<p>Your email is ${data?.email}.</p>
|
package/src/actions.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { html
|
|
1
|
+
import { html } from '@hyperspan/html';
|
|
2
2
|
import { createRoute, returnHTMLResponse } from './server';
|
|
3
3
|
import * as z from 'zod/v4';
|
|
4
4
|
import type { Hyperspan as HS } from './types';
|
|
5
5
|
import { assetHash, formDataToJSON } from './utils';
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
6
|
+
import { buildClientJS } from './client/js';
|
|
7
|
+
import { validateBody } from './middleware';
|
|
8
|
+
|
|
9
|
+
const actionsClientJS = await buildClientJS(import.meta.resolve('./client/_hs/hyperspan-actions.client'));
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Actions = Form + route handler
|
|
@@ -19,7 +21,7 @@ import { renderClientJS } from './client/js';
|
|
|
19
21
|
* 5. Replaces form content in place with HTML response content from server via the Idiomorph library
|
|
20
22
|
* 6. Handles any Exception thrown on server as error displayed back to user on the page
|
|
21
23
|
*/
|
|
22
|
-
export function createAction<T extends z.
|
|
24
|
+
export function createAction<T extends z.ZodObject<any, any>>(params: { name: string; schema?: T }): HS.Action<T> {
|
|
23
25
|
const { name, schema } = params;
|
|
24
26
|
const path = `/__actions/${assetHash(name)}`;
|
|
25
27
|
|
|
@@ -30,7 +32,7 @@ export function createAction<T extends z.ZodTypeAny>(params: { name: string; sch
|
|
|
30
32
|
.get((c: HS.Context) => api.render(c))
|
|
31
33
|
.post(async (c: HS.Context) => {
|
|
32
34
|
// Parse form data
|
|
33
|
-
const formData = await c.req.
|
|
35
|
+
const formData = await c.req.formData();
|
|
34
36
|
const jsonData = formDataToJSON(formData) as Partial<z.infer<T>>;
|
|
35
37
|
const schemaData = schema ? schema.safeParse(jsonData) : null;
|
|
36
38
|
const data = schemaData?.success ? (schemaData.data as Partial<z.infer<T>>) : jsonData;
|
|
@@ -69,7 +71,10 @@ export function createAction<T extends z.ZodTypeAny>(params: { name: string; sch
|
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
return await returnHTMLResponse(c, () => api.render(c, { data, error }), { status: 400 });
|
|
72
|
-
});
|
|
74
|
+
}, { middleware: schema ? [validateBody(schema)] : [] });
|
|
75
|
+
|
|
76
|
+
// Set the name of the action for the route
|
|
77
|
+
route._config.name = name;
|
|
73
78
|
|
|
74
79
|
const api: HS.Action<T> = {
|
|
75
80
|
_kind: 'hsAction',
|
|
@@ -101,7 +106,7 @@ export function createAction<T extends z.ZodTypeAny>(params: { name: string; sch
|
|
|
101
106
|
*/
|
|
102
107
|
render(c: HS.Context, props?: HS.ActionProps<T>) {
|
|
103
108
|
const formContent = api._form ? api._form(c, props || {}) : null;
|
|
104
|
-
return formContent ? html`<hs-action url="${this._path()}">${formContent}</hs-action>${
|
|
109
|
+
return formContent ? html`<hs-action url="${this._path()}">${formContent}</hs-action>${actionsClientJS.renderScriptTag()}` : null;
|
|
105
110
|
},
|
|
106
111
|
errorHandler(handler) {
|
|
107
112
|
_errorHandler = handler;
|
|
@@ -1,68 +1,5 @@
|
|
|
1
|
-
import { Idiomorph } from './idiomorph';
|
|
2
1
|
import { lazyLoadScripts } from './hyperspan-scripts.client';
|
|
3
2
|
|
|
4
|
-
/**
|
|
5
|
-
* Used for streaming content from the server to the client.
|
|
6
|
-
*/
|
|
7
|
-
function htmlAsyncContentObserver() {
|
|
8
|
-
if (typeof MutationObserver != 'undefined') {
|
|
9
|
-
// Hyperspan - Async content loader
|
|
10
|
-
// Puts streamed content in its place immediately after it is added to the DOM
|
|
11
|
-
const asyncContentObserver = new MutationObserver((list) => {
|
|
12
|
-
const asyncContent = list
|
|
13
|
-
.map((mutation) =>
|
|
14
|
-
Array.from(mutation.addedNodes).find((node: any) => {
|
|
15
|
-
if (!node || !node?.id || typeof node.id !== 'string') {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
return node.id?.startsWith('async_loading_') && node.id?.endsWith('_content');
|
|
19
|
-
})
|
|
20
|
-
)
|
|
21
|
-
.filter((node: any) => node);
|
|
22
|
-
|
|
23
|
-
asyncContent.forEach((templateEl: any) => {
|
|
24
|
-
try {
|
|
25
|
-
// Also observe for content inside the template content (shadow DOM is separate)
|
|
26
|
-
asyncContentObserver.observe(templateEl.content, { childList: true, subtree: true });
|
|
27
|
-
|
|
28
|
-
const slotId = templateEl.id.replace('_content', '');
|
|
29
|
-
const slotEl = document.getElementById(slotId);
|
|
30
|
-
|
|
31
|
-
if (slotEl) {
|
|
32
|
-
// Content AND slot are present - let's insert the content into the slot
|
|
33
|
-
// Ensure the content is fully done streaming in before inserting it into the slot
|
|
34
|
-
waitForContent(templateEl.content, (el2) => {
|
|
35
|
-
return Array.from(el2.childNodes).find(
|
|
36
|
-
(node) => node.nodeType === Node.COMMENT_NODE && node.nodeValue === 'end'
|
|
37
|
-
);
|
|
38
|
-
})
|
|
39
|
-
.then((endComment) => {
|
|
40
|
-
templateEl.content.removeChild(endComment);
|
|
41
|
-
const content = templateEl.content.cloneNode(true);
|
|
42
|
-
Idiomorph.morph(slotEl, content);
|
|
43
|
-
templateEl.parentNode.removeChild(templateEl);
|
|
44
|
-
lazyLoadScripts();
|
|
45
|
-
})
|
|
46
|
-
.catch(console.error);
|
|
47
|
-
} else {
|
|
48
|
-
// Slot is NOT present - wait for it to be added to the DOM so we can insert the content into it
|
|
49
|
-
waitForContent(document.body, () => {
|
|
50
|
-
return document.getElementById(slotId);
|
|
51
|
-
}).then((slotEl) => {
|
|
52
|
-
Idiomorph.morph(slotEl, templateEl.content.cloneNode(true));
|
|
53
|
-
lazyLoadScripts();
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
} catch (e) {
|
|
57
|
-
console.error(e);
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
asyncContentObserver.observe(document.body, { childList: true, subtree: true });
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
htmlAsyncContentObserver();
|
|
65
|
-
|
|
66
3
|
/**
|
|
67
4
|
* Wait until ALL of the content inside an element is present from streaming in.
|
|
68
5
|
* Large chunks of content can sometimes take more than a single tick to write to DOM.
|
|
@@ -91,4 +28,39 @@ async function waitForContent(
|
|
|
91
28
|
reject(new Error(`[Hyperspan] Timeout waiting for end of streaming content ${el.id}`));
|
|
92
29
|
}, options.timeoutMs || 10000);
|
|
93
30
|
});
|
|
94
|
-
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function renderStreamChunk(chunk: { id: string }) {
|
|
34
|
+
const slotId = chunk.id;
|
|
35
|
+
const slotEl = document.getElementById(slotId);
|
|
36
|
+
const templateEl = document.getElementById(`${slotId}_content`) as HTMLTemplateElement;
|
|
37
|
+
|
|
38
|
+
if (slotEl) {
|
|
39
|
+
// Content AND slot are present - let's insert the content into the slot
|
|
40
|
+
// Ensure the content is fully done streaming in before inserting it into the slot
|
|
41
|
+
waitForContent(templateEl.content as unknown as HTMLElement, (el2) => {
|
|
42
|
+
return Array.from(el2.childNodes).find(
|
|
43
|
+
(node) => node.nodeType === Node.COMMENT_NODE && node.nodeValue === 'end'
|
|
44
|
+
);
|
|
45
|
+
})
|
|
46
|
+
.then((endComment) => {
|
|
47
|
+
templateEl.content.removeChild(endComment as Node);
|
|
48
|
+
const content = templateEl.content.cloneNode(true);
|
|
49
|
+
slotEl.replaceWith(content);
|
|
50
|
+
templateEl.parentNode?.removeChild(templateEl);
|
|
51
|
+
lazyLoadScripts();
|
|
52
|
+
})
|
|
53
|
+
.catch(console.error);
|
|
54
|
+
} else {
|
|
55
|
+
// Slot is NOT present - wait for it to be added to the DOM so we can insert the content into it
|
|
56
|
+
waitForContent(document.body, () => {
|
|
57
|
+
return document.getElementById(slotId);
|
|
58
|
+
}).then((slotEl) => {
|
|
59
|
+
(slotEl as HTMLElement)?.replaceWith(templateEl.content.cloneNode(true));
|
|
60
|
+
lazyLoadScripts();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// @ts-ignore
|
|
66
|
+
window._hscc = renderStreamChunk;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { test, expect, describe } from 'bun:test';
|
|
2
|
+
import { functionToString } from './js';
|
|
3
|
+
|
|
4
|
+
describe('functionToString', () => {
|
|
5
|
+
describe('named functions', () => {
|
|
6
|
+
test('converts named function to string', () => {
|
|
7
|
+
function myFunction() {
|
|
8
|
+
return 'hello';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const result = functionToString(myFunction);
|
|
12
|
+
expect(result).toContain('function');
|
|
13
|
+
expect(result).toContain('myFunction');
|
|
14
|
+
expect(result).toContain("return 'hello'");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('converts named function with parameters', () => {
|
|
18
|
+
function add(a: number, b: number) {
|
|
19
|
+
return a + b;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const result = functionToString(add);
|
|
23
|
+
expect(result).toContain('function');
|
|
24
|
+
expect(result).toContain('add');
|
|
25
|
+
expect(result).toContain('a');
|
|
26
|
+
expect(result).toContain('b');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('converts named async function', () => {
|
|
30
|
+
async function fetchData() {
|
|
31
|
+
const response = await fetch('/api/data');
|
|
32
|
+
return response.json();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const result = functionToString(fetchData);
|
|
36
|
+
expect(result).toContain('async function');
|
|
37
|
+
expect(result).toContain('fetchData');
|
|
38
|
+
expect(result).toContain('await');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('anonymous functions', () => {
|
|
43
|
+
test('converts anonymous function to string', () => {
|
|
44
|
+
const fn = function () {
|
|
45
|
+
return 'anonymous';
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const result = functionToString(fn);
|
|
49
|
+
expect(result).toContain('function');
|
|
50
|
+
expect(result).toContain("return 'anonymous'");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('converts anonymous function with parameters', () => {
|
|
54
|
+
const fn = function (x: number, y: number) {
|
|
55
|
+
return x * y;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const result = functionToString(fn);
|
|
59
|
+
expect(result).toContain('function');
|
|
60
|
+
expect(result).toContain('x');
|
|
61
|
+
expect(result).toContain('y');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('converts anonymous async function', () => {
|
|
65
|
+
const fn = async function () {
|
|
66
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
67
|
+
return 'done';
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const result = functionToString(fn);
|
|
71
|
+
expect(result).toContain('async function');
|
|
72
|
+
expect(result).toContain('await');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('arrow functions', () => {
|
|
77
|
+
test('converts single-line arrow function without braces', () => {
|
|
78
|
+
const fn = (x: number) => x * 2;
|
|
79
|
+
|
|
80
|
+
const result = functionToString(fn);
|
|
81
|
+
expect(result).toContain('function(x) { return x * 2; }');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('converts single-line arrow function with single parameter', () => {
|
|
85
|
+
const fn = (name: string) => `Hello, ${name}!`;
|
|
86
|
+
|
|
87
|
+
const result = functionToString(fn);
|
|
88
|
+
expect(result).toContain('function(name) { return `Hello, ${name}!`; }');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('converts single-line arrow function with multiple parameters', () => {
|
|
92
|
+
const fn = (a: number, b: number) => a + b;
|
|
93
|
+
|
|
94
|
+
const result = functionToString(fn);
|
|
95
|
+
expect(result).toContain('function(a, b) { return a + b; }');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('converts arrow function with braces', () => {
|
|
99
|
+
const fn = (x: number) => {
|
|
100
|
+
const doubled = x * 2;
|
|
101
|
+
return doubled;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const result = functionToString(fn);
|
|
105
|
+
expect(result).toContain('function');
|
|
106
|
+
expect(result).toContain('x');
|
|
107
|
+
expect(result).toContain('doubled');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('converts multi-line arrow function', () => {
|
|
111
|
+
const fn = (items: string[]) => {
|
|
112
|
+
const filtered = items.filter(item => item.length > 0);
|
|
113
|
+
return filtered.map(item => item.toUpperCase());
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const result = functionToString(fn);
|
|
117
|
+
expect(result).toContain('function');
|
|
118
|
+
expect(result).toContain('items');
|
|
119
|
+
expect(result).toContain('filtered');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('converts async arrow function without braces', () => {
|
|
123
|
+
const fn = async (id: number) => await fetch(`/api/${id}`);
|
|
124
|
+
|
|
125
|
+
const result = functionToString(fn);
|
|
126
|
+
expect(result).toContain('async function');
|
|
127
|
+
expect(result).toContain('id');
|
|
128
|
+
expect(result).toContain('await');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('converts async arrow function with braces', () => {
|
|
132
|
+
const fn = async (id: number) => {
|
|
133
|
+
const response = await fetch(`/api/${id}`);
|
|
134
|
+
return response.json();
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const result = functionToString(fn);
|
|
138
|
+
expect(result).toContain('async function');
|
|
139
|
+
expect(result).toContain('id');
|
|
140
|
+
expect(result).toContain('await');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('converts arrow function with no parameters', () => {
|
|
144
|
+
const fn = () => 'no params';
|
|
145
|
+
|
|
146
|
+
const result = functionToString(fn);
|
|
147
|
+
expect(result).toContain('function');
|
|
148
|
+
expect(result).toContain("return 'no params'");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('converts arrow function with complex expression', () => {
|
|
152
|
+
const fn = (obj: { x: number; y: number }) => obj.x + obj.y;
|
|
153
|
+
|
|
154
|
+
const result = functionToString(fn);
|
|
155
|
+
expect(result).toContain('function');
|
|
156
|
+
expect(result).toContain('return');
|
|
157
|
+
expect(result).toContain('obj.x + obj.y');
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('edge cases', () => {
|
|
162
|
+
test('handles function with whitespace', () => {
|
|
163
|
+
const fn = function () {
|
|
164
|
+
return 'test';
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const result = functionToString(fn);
|
|
168
|
+
expect(result).toContain('function');
|
|
169
|
+
expect(result).toContain("return 'test'");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('handles arrow function with whitespace', () => {
|
|
173
|
+
const fn = (x) => x * 2;
|
|
174
|
+
|
|
175
|
+
const result = functionToString(fn);
|
|
176
|
+
expect(result).toContain('function');
|
|
177
|
+
expect(result).toContain('return');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('handles function with comments', () => {
|
|
181
|
+
const fn = function () {
|
|
182
|
+
// This is a comment
|
|
183
|
+
return 'commented';
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const result = functionToString(fn);
|
|
187
|
+
expect(result).toContain('function');
|
|
188
|
+
expect(result).toContain("return 'commented'");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('handles nested arrow functions', () => {
|
|
192
|
+
const fn = (arr: number[]) => arr.map(x => x * 2);
|
|
193
|
+
|
|
194
|
+
const result = functionToString(fn);
|
|
195
|
+
expect(result).toContain('function');
|
|
196
|
+
// The nested arrow function should also be converted
|
|
197
|
+
expect(result).toContain('x * 2');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|