@hyperspan/framework 1.0.29 → 2.0.0-alpha.13
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 +30 -7
- package/src/actions.test.ts +1 -1
- package/src/actions.ts +3 -6
- package/src/adapter.test.ts +23 -0
- package/src/client/_hs/hyperspan-actions.client.ts +37 -45
- package/src/client/js.test.ts +157 -4
- package/src/client/js.ts +287 -64
- package/src/client/manifest.ts +83 -0
- package/src/dev-bindings.ts +14 -0
- package/src/fetch-handler.test.ts +113 -0
- package/src/fetch-handler.ts +168 -0
- package/src/index.ts +27 -1
- package/src/island.test.ts +30 -0
- package/src/island.ts +37 -0
- package/src/layout.test.ts +12 -0
- package/src/layout.ts +28 -11
- package/src/register-routes.ts +64 -0
- package/src/server.test.ts +60 -2
- package/src/server.ts +42 -11
- package/src/ssr/mock-dom.test.ts +1 -1
- package/src/types.ts +72 -3
- package/src/utils.test.ts +72 -2
- package/src/utils.ts +66 -10
- package/tsconfig.json +4 -8
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperspan/framework",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0-alpha.13",
|
|
4
4
|
"description": "Hyperspan Web Framework",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"main": "src/server.ts",
|
|
6
7
|
"types": "src/server.ts",
|
|
7
8
|
"public": true,
|
|
8
9
|
"publishConfig": {
|
|
9
|
-
"access": "public"
|
|
10
|
+
"access": "public",
|
|
11
|
+
"tag": "alpha"
|
|
10
12
|
},
|
|
11
13
|
"exports": {
|
|
12
14
|
".": {
|
|
@@ -25,10 +27,18 @@
|
|
|
25
27
|
"types": "./src/utils.ts",
|
|
26
28
|
"default": "./src/utils.ts"
|
|
27
29
|
},
|
|
30
|
+
"./routes": {
|
|
31
|
+
"types": "./src/register-routes.ts",
|
|
32
|
+
"default": "./src/register-routes.ts"
|
|
33
|
+
},
|
|
28
34
|
"./layout": {
|
|
29
35
|
"types": "./src/layout.ts",
|
|
30
36
|
"default": "./src/layout.ts"
|
|
31
37
|
},
|
|
38
|
+
"./dev-bindings": {
|
|
39
|
+
"types": "./src/dev-bindings.ts",
|
|
40
|
+
"default": "./src/dev-bindings.ts"
|
|
41
|
+
},
|
|
32
42
|
"./client/css": {
|
|
33
43
|
"types": "./src/client/css.ts",
|
|
34
44
|
"default": "./src/client/css.ts"
|
|
@@ -37,6 +47,10 @@
|
|
|
37
47
|
"types": "./src/client/js.ts",
|
|
38
48
|
"default": "./src/client/js.ts"
|
|
39
49
|
},
|
|
50
|
+
"./client/manifest": {
|
|
51
|
+
"types": "./src/client/manifest.ts",
|
|
52
|
+
"default": "./src/client/manifest.ts"
|
|
53
|
+
},
|
|
40
54
|
"./actions": {
|
|
41
55
|
"types": "./src/actions.ts",
|
|
42
56
|
"default": "./src/actions.ts"
|
|
@@ -48,6 +62,15 @@
|
|
|
48
62
|
"./ssr/mock-dom": {
|
|
49
63
|
"types": "./src/ssr/mock-dom.ts",
|
|
50
64
|
"default": "./src/ssr/mock-dom.ts"
|
|
65
|
+
},
|
|
66
|
+
"./client/_hs/hyperspan-streaming.client.ts": {
|
|
67
|
+
"default": "./src/client/_hs/hyperspan-streaming.client.ts"
|
|
68
|
+
},
|
|
69
|
+
"./client/_hs/hyperspan-actions.client.ts": {
|
|
70
|
+
"default": "./src/client/_hs/hyperspan-actions.client.ts"
|
|
71
|
+
},
|
|
72
|
+
"./client/_hs/hyperspan-scripts.client.ts": {
|
|
73
|
+
"default": "./src/client/_hs/hyperspan-scripts.client.ts"
|
|
51
74
|
}
|
|
52
75
|
},
|
|
53
76
|
"author": "Vance Lucas <vance@vancelucas.com>",
|
|
@@ -55,7 +78,7 @@
|
|
|
55
78
|
"keywords": [
|
|
56
79
|
"framework",
|
|
57
80
|
"node",
|
|
58
|
-
"
|
|
81
|
+
"vite",
|
|
59
82
|
"web framework",
|
|
60
83
|
"javascript",
|
|
61
84
|
"typescript",
|
|
@@ -71,17 +94,17 @@
|
|
|
71
94
|
"url": "https://github.com/vlucas/hyperspan/issues"
|
|
72
95
|
},
|
|
73
96
|
"scripts": {
|
|
74
|
-
"test": "
|
|
97
|
+
"test": "vitest run"
|
|
75
98
|
},
|
|
76
99
|
"devDependencies": {
|
|
77
|
-
"@types/bun": "^1.3.1",
|
|
78
100
|
"@types/debug": "^4.1.12",
|
|
79
101
|
"@types/node": "^24.10.0",
|
|
80
102
|
"prettier": "^3.5.2",
|
|
81
|
-
"typescript": "^5.9.3"
|
|
103
|
+
"typescript": "^5.9.3",
|
|
104
|
+
"vitest": "^4.1.10"
|
|
82
105
|
},
|
|
83
106
|
"dependencies": {
|
|
84
|
-
"@hyperspan/html": "^
|
|
107
|
+
"@hyperspan/html": "^2.0.0-alpha.13",
|
|
85
108
|
"debug": "^4.4.3",
|
|
86
109
|
"isbot": "^5.1.32",
|
|
87
110
|
"zod": "^4.4.3"
|
package/src/actions.test.ts
CHANGED
package/src/actions.ts
CHANGED
|
@@ -3,14 +3,11 @@ import { createRoute, HTTPResponseException, returnHTMLResponse } from './server
|
|
|
3
3
|
import * as z from 'zod';
|
|
4
4
|
import type { Hyperspan as HS } from './types';
|
|
5
5
|
import { assetHash, formDataToJSON } from './utils';
|
|
6
|
-
import {
|
|
6
|
+
import { hyperspanActionsScriptTag } from './layout';
|
|
7
7
|
import { validateBody, ZodValidationError } from './middleware';
|
|
8
|
-
import
|
|
8
|
+
import debug from 'debug';
|
|
9
9
|
|
|
10
10
|
const log = debug('hyperspan:actions');
|
|
11
|
-
const actionsClientJS = await buildClientJS(
|
|
12
|
-
import.meta.resolve('./client/_hs/hyperspan-actions.client')
|
|
13
|
-
);
|
|
14
11
|
|
|
15
12
|
/**
|
|
16
13
|
* Actions = Form + route handler
|
|
@@ -138,7 +135,7 @@ export function createAction<S extends z.ZodType>(params: {
|
|
|
138
135
|
const formContent = api._form ? api._form(c, props || {}) : null;
|
|
139
136
|
return formContent
|
|
140
137
|
? html`<hs-action url="${this._path()}">${formContent}</hs-action
|
|
141
|
-
>${
|
|
138
|
+
>${hyperspanActionsScriptTag()}`
|
|
142
139
|
: null;
|
|
143
140
|
},
|
|
144
141
|
errorHandler(handler) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import type { Adapter } from './types';
|
|
3
|
+
import { createConfig } from './server';
|
|
4
|
+
|
|
5
|
+
const cloudflare: Adapter = {
|
|
6
|
+
name: 'cloudflare',
|
|
7
|
+
createEntry() {
|
|
8
|
+
return {
|
|
9
|
+
fetch: async () => new Response('ok'),
|
|
10
|
+
};
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe('createConfig deployAdapter', () => {
|
|
15
|
+
test('defaults to Node when omitted or undefined', () => {
|
|
16
|
+
expect(createConfig({}).deployAdapter.name).toBe('node');
|
|
17
|
+
expect(createConfig({ deployAdapter: undefined }).deployAdapter.name).toBe('node');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('keeps an explicit adapter', () => {
|
|
21
|
+
expect(createConfig({ deployAdapter: cloudflare }).deployAdapter).toBe(cloudflare);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -59,62 +59,56 @@ function setActionLoading(hsActionTag: HTMLElement | null, loading: boolean) {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
const actionFormObserver = new MutationObserver((list) => {
|
|
63
|
-
list.forEach((mutation) => {
|
|
64
|
-
mutation.addedNodes.forEach((node) => {
|
|
65
|
-
if (node && ('closest' in node || node instanceof HTMLFormElement)) {
|
|
66
|
-
bindHSActionForm(
|
|
67
|
-
(node as HTMLElement).closest('hs-action') as HSAction,
|
|
68
|
-
node instanceof HTMLFormElement
|
|
69
|
-
? node
|
|
70
|
-
: ((node as HTMLElement | HTMLFormElement).querySelector('form') as HTMLFormElement)
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
62
|
/**
|
|
78
|
-
* Server action component
|
|
63
|
+
* Server action component — holds the action URL; form submit is handled via delegation.
|
|
79
64
|
*/
|
|
80
|
-
class HSAction extends HTMLElement {
|
|
81
|
-
constructor() {
|
|
82
|
-
super();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
connectedCallback() {
|
|
86
|
-
actionFormObserver.observe(this, { childList: true, subtree: true });
|
|
87
|
-
bindHSActionForm(this, this.querySelector('form') as HTMLFormElement);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
65
|
+
class HSAction extends HTMLElement {}
|
|
90
66
|
window.customElements.define('hs-action', HSAction);
|
|
91
67
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
*/
|
|
95
|
-
function
|
|
96
|
-
if (
|
|
68
|
+
let actionDelegationInitialized = false;
|
|
69
|
+
|
|
70
|
+
/** One document-level handler so morphs never leave forms without listeners or stack duplicates. */
|
|
71
|
+
function initHSActionFormDelegation() {
|
|
72
|
+
if (actionDelegationInitialized) {
|
|
97
73
|
return;
|
|
98
74
|
}
|
|
75
|
+
actionDelegationInitialized = true;
|
|
99
76
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
e
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
77
|
+
document.addEventListener(
|
|
78
|
+
'submit',
|
|
79
|
+
(e) => {
|
|
80
|
+
const form = e.target;
|
|
81
|
+
if (!(form instanceof HTMLFormElement)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const hsAction = form.closest('hs-action');
|
|
86
|
+
if (!hsAction) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
|
|
92
|
+
const url = hsAction.getAttribute('url') || '';
|
|
93
|
+
if (url) {
|
|
94
|
+
form.setAttribute('action', url);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
formSubmitToRoute(form);
|
|
98
|
+
},
|
|
99
|
+
true
|
|
100
|
+
);
|
|
109
101
|
}
|
|
110
102
|
|
|
103
|
+
initHSActionFormDelegation();
|
|
104
|
+
|
|
111
105
|
/**
|
|
112
106
|
* Submit form data to route and replace contents with response
|
|
113
107
|
*/
|
|
114
|
-
|
|
115
|
-
function formSubmitToRoute(e: Event, form: HTMLFormElement, opts: TFormSubmitOptons) {
|
|
108
|
+
function formSubmitToRoute(form: HTMLFormElement) {
|
|
116
109
|
const formData = new FormData(form);
|
|
117
|
-
const
|
|
110
|
+
const hsActionTag = form.closest('hs-action') as HTMLElement | null;
|
|
111
|
+
const formUrl = hsActionTag?.getAttribute('url') || form.getAttribute('action') || '';
|
|
118
112
|
const method = form.getAttribute('method')?.toUpperCase() || 'POST';
|
|
119
113
|
const confirmMessage = form.getAttribute('data-confirm') || '';
|
|
120
114
|
const headers = {
|
|
@@ -129,7 +123,6 @@ function formSubmitToRoute(e: Event, form: HTMLFormElement, opts: TFormSubmitOpt
|
|
|
129
123
|
}
|
|
130
124
|
}
|
|
131
125
|
|
|
132
|
-
const hsActionTag = form.closest('hs-action') as HTMLElement | null;
|
|
133
126
|
const eventTarget: EventTarget = hsActionTag || document;
|
|
134
127
|
const fetchDetail: HS.ActionFetchDetail = {
|
|
135
128
|
form,
|
|
@@ -185,7 +178,6 @@ function formSubmitToRoute(e: Event, form: HTMLFormElement, opts: TFormSubmitOpt
|
|
|
185
178
|
|
|
186
179
|
dispatchActionEvent(eventTarget, HS_ACTION_AFTER_SWAP, swapDetail);
|
|
187
180
|
|
|
188
|
-
opts.afterResponse && opts.afterResponse();
|
|
189
181
|
lazyLoadScripts();
|
|
190
182
|
}
|
|
191
183
|
|
package/src/client/js.test.ts
CHANGED
|
@@ -1,9 +1,152 @@
|
|
|
1
|
-
import { describe, expect, test } from '
|
|
2
|
-
import {
|
|
1
|
+
import { describe, expect, test, beforeEach } from 'vitest';
|
|
2
|
+
import { mkdtempSync, writeFileSync, existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { render } from '@hyperspan/html';
|
|
6
|
+
import {
|
|
7
|
+
buildClientJS,
|
|
8
|
+
discoverClientExports,
|
|
9
|
+
extractExports,
|
|
10
|
+
getClientJSEntries,
|
|
11
|
+
registerPathAliases,
|
|
12
|
+
resetClientJSEntriesForTests,
|
|
13
|
+
} from './js';
|
|
14
|
+
|
|
15
|
+
describe('buildClientJS', () => {
|
|
16
|
+
let clientFile: string;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
resetClientJSEntriesForTests();
|
|
20
|
+
const dir = mkdtempSync(join(tmpdir(), 'hs-client-'));
|
|
21
|
+
clientFile = join(dir, 'picker.ts');
|
|
22
|
+
writeFileSync(
|
|
23
|
+
clientFile,
|
|
24
|
+
`export function mountPicker() {
|
|
25
|
+
document.getElementById('picker')?.classList.add('ready');
|
|
26
|
+
}
|
|
27
|
+
`
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('registers entry and returns stable esmName from path hash', async () => {
|
|
32
|
+
const result = await buildClientJS(clientFile);
|
|
33
|
+
|
|
34
|
+
expect(result.esmName).toMatch(/^client-[0-9a-f]{16}$/);
|
|
35
|
+
expect(result.publicPath).toBe(`/_hs/js/${result.esmName}.js`);
|
|
36
|
+
expect(getClientJSEntries()).toHaveLength(1);
|
|
37
|
+
expect(getClientJSEntries()[0].absPath).toBe(clientFile);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('renderScriptTag with no loader uses import map key', async () => {
|
|
41
|
+
const result = await buildClientJS(clientFile);
|
|
42
|
+
const tag = render(result.renderScriptTag());
|
|
43
|
+
|
|
44
|
+
expect(tag).toContain(`import "${result.esmName}"`);
|
|
45
|
+
expect(tag).toContain(`data-source-id="${result.assetHash}"`);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('renderScriptTag with function inlines bootstrap code', async () => {
|
|
49
|
+
const result = await buildClientJS(clientFile);
|
|
50
|
+
const tag = render(
|
|
51
|
+
result.renderScriptTag(({ mountPicker }: { mountPicker: () => void }) => {
|
|
52
|
+
mountPicker();
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
expect(tag).toContain(`import {mountPicker} from "${result.esmName}"`);
|
|
57
|
+
expect(tag).toContain('mountPicker()');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('renderScriptTag with string inlines bootstrap code', async () => {
|
|
61
|
+
const result = await buildClientJS(clientFile);
|
|
62
|
+
const tag = render(result.renderScriptTag('({ mountPicker }) => mountPicker()'));
|
|
63
|
+
|
|
64
|
+
expect(tag).toContain('({ mountPicker }) => mountPicker()');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('import.meta.resolve at the call site registers a real file', async () => {
|
|
68
|
+
const result = await buildClientJS(import.meta.resolve('./_hs/hyperspan-streaming.client.ts'), {
|
|
69
|
+
type: 'iife',
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(result.publicPath).toMatch(/^\/_hs\/js\/client-[0-9a-f]{16}\.js$/);
|
|
73
|
+
expect(getClientJSEntries()[0].type).toBe('iife');
|
|
74
|
+
expect(existsSync(getClientJSEntries()[0].absPath)).toBe(true);
|
|
75
|
+
expect(getClientJSEntries()[0].absPath).toMatch(/hyperspan-streaming\.client\.ts$/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('relative paths must be resolved at the call site', async () => {
|
|
79
|
+
await expect(buildClientJS('../client/picker.ts')).rejects.toThrow(/import\.meta\.resolve/);
|
|
80
|
+
await expect(buildClientJS('./picker.ts')).rejects.toThrow(/import\.meta\.resolve/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('type iife renders a classic script tag', async () => {
|
|
84
|
+
const result = await buildClientJS(clientFile, { type: 'iife' });
|
|
85
|
+
const tag = render(result.renderScriptTag());
|
|
86
|
+
|
|
87
|
+
expect(tag).toContain(`<script src="${result.publicPath}"`);
|
|
88
|
+
expect(tag).not.toContain('type="module"');
|
|
89
|
+
expect(getClientJSEntries()[0].type).toBe('iife');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('logical app-relative paths hash consistently and resolve on disk when present', async () => {
|
|
93
|
+
const dir = mkdtempSync(join(tmpdir(), 'hs-client-logical-'));
|
|
94
|
+
const { mkdirSync } = await import('node:fs');
|
|
95
|
+
mkdirSync(join(dir, 'app/client'), { recursive: true });
|
|
96
|
+
const logicalPath = join(dir, 'app/client/stats.ts');
|
|
97
|
+
writeFileSync(logicalPath, `export function mountStats() {}\n`);
|
|
98
|
+
|
|
99
|
+
const cwd = process.cwd();
|
|
100
|
+
process.chdir(dir);
|
|
101
|
+
try {
|
|
102
|
+
const first = await buildClientJS('app/client/stats.ts');
|
|
103
|
+
resetClientJSEntriesForTests();
|
|
104
|
+
const second = await buildClientJS('app/client/stats.ts');
|
|
105
|
+
expect(first.esmName).toBe(second.esmName);
|
|
106
|
+
expect(getClientJSEntries()[0].absPath.replace(/^\/private/, '')).toBe(
|
|
107
|
+
logicalPath.replace(/^\/private/, '')
|
|
108
|
+
);
|
|
109
|
+
} finally {
|
|
110
|
+
process.chdir(cwd);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('tsconfig aliases resolve inside buildClientJS without import.meta.resolve', async () => {
|
|
115
|
+
const dir = mkdtempSync(join(tmpdir(), 'hs-client-alias-'));
|
|
116
|
+
const { mkdirSync } = await import('node:fs');
|
|
117
|
+
mkdirSync(join(dir, 'app/client'), { recursive: true });
|
|
118
|
+
const absPath = join(dir, 'app/client/stats.ts');
|
|
119
|
+
writeFileSync(absPath, `export function mountStats() {}\n`);
|
|
120
|
+
|
|
121
|
+
registerPathAliases({ '~/': `${dir}/`, '~': dir });
|
|
122
|
+
|
|
123
|
+
const result = await buildClientJS('~/app/client/stats.ts');
|
|
124
|
+
expect(getClientJSEntries()[0].absPath.replace(/^\/private/, '')).toBe(
|
|
125
|
+
absPath.replace(/^\/private/, '')
|
|
126
|
+
);
|
|
127
|
+
expect(getClientJSEntries()[0].modulePath).toBe('~/app/client/stats.ts');
|
|
128
|
+
|
|
129
|
+
resetClientJSEntriesForTests();
|
|
130
|
+
registerPathAliases({ '~/': `${dir}/`, '~': dir });
|
|
131
|
+
const second = await buildClientJS('~/app/client/stats.ts');
|
|
132
|
+
expect(second.esmName).toBe(result.esmName);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('aliased specifiers keep a stable hash when the file is missing', async () => {
|
|
136
|
+
registerPathAliases({ '~/': '/definitely-missing-hyperspan-root/' });
|
|
137
|
+
const first = await buildClientJS('~/app/client/missing.ts');
|
|
138
|
+
resetClientJSEntriesForTests();
|
|
139
|
+
registerPathAliases({ '~/': '/definitely-missing-hyperspan-root/' });
|
|
140
|
+
const second = await buildClientJS('~/app/client/missing.ts');
|
|
141
|
+
expect(first.esmName).toBe(second.esmName);
|
|
142
|
+
expect(existsSync(getClientJSEntries()[0].absPath)).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
3
145
|
|
|
4
146
|
describe('extractExports', () => {
|
|
5
147
|
test('extracts aliased export from bundled JS content', () => {
|
|
6
|
-
const contents =
|
|
148
|
+
const contents =
|
|
149
|
+
'function d(n,t){if(t)n.classList.remove("hidden");else n.classList.add("hidden")}function p(){let n=document.querySelector("[data-google-spreadsheet-id]"),t=document.querySelector("[data-google-sheet-select]"),a=document.querySelector("[data-google-sheet-message]");if(console.log("googleSheetsPickerClient",n,t),!n||!t)return;let l=(e)=>{if(!a)return;a.textContent=e||"",d(a,Boolean(e))},h=(e)=>{t.innerHTML="",e.forEach((s)=>{let o=document.createElement("option");o.value=s,o.textContent=s,t.appendChild(o)})},r=(e)=>{d(t,e),t.disabled=!e,t.required=e},c=async()=>{let e=n.value.trim();if(l(null),!e){r(!1);return}try{let s=await fetch(`/api/google-sheets/${encodeURIComponent(e)}`),o=await s.json();if(!s.ok||o.error)throw Error(o.error||"Unable to load sheet names.");let i=(o.sheets||[]).map((u)=>u.title).filter(Boolean);if(i.length===0){l("No sheets found in that spreadsheet."),r(!1);return}h(i),t.value=i[0],r(!0)}catch(s){l(s instanceof Error?s.message:"Unable to load sheet names."),r(!1)}};n.addEventListener("input",c),n.addEventListener("change",c)}export{p as mountGoogleSheetsPicker};';
|
|
7
150
|
|
|
8
151
|
const result = extractExports(contents);
|
|
9
152
|
|
|
@@ -13,7 +156,7 @@ describe('extractExports', () => {
|
|
|
13
156
|
});
|
|
14
157
|
});
|
|
15
158
|
|
|
16
|
-
test('returns
|
|
159
|
+
test('returns namespace export when none found', () => {
|
|
17
160
|
const contents = 'function noop(){return 1}const value=2;';
|
|
18
161
|
|
|
19
162
|
const result = extractExports(contents);
|
|
@@ -25,3 +168,13 @@ describe('extractExports', () => {
|
|
|
25
168
|
});
|
|
26
169
|
});
|
|
27
170
|
|
|
171
|
+
describe('discoverClientExports', () => {
|
|
172
|
+
test('discovers named exports from source', () => {
|
|
173
|
+
const result = discoverClientExports(
|
|
174
|
+
`export function mountPicker() {}\nexport const VERSION = 1;`
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
expect(result.exports).toBe('{mountPicker, VERSION}');
|
|
178
|
+
expect(result.fnArgs).toBe('{mountPicker, VERSION}');
|
|
179
|
+
});
|
|
180
|
+
});
|