sunpeak 0.9.2 → 0.9.6

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 (41) hide show
  1. package/README.md +2 -2
  2. package/dist/chatgpt/conversation.d.ts +5 -2
  3. package/dist/chatgpt/iframe-resource.d.ts +69 -0
  4. package/dist/chatgpt/index.cjs +2 -1
  5. package/dist/chatgpt/index.cjs.map +1 -1
  6. package/dist/chatgpt/index.d.ts +1 -0
  7. package/dist/chatgpt/index.js +2 -1
  8. package/dist/discovery-a4WId9PC.cjs +125 -0
  9. package/dist/discovery-a4WId9PC.cjs.map +1 -0
  10. package/dist/discovery-ft3cd2dW.js +126 -0
  11. package/dist/discovery-ft3cd2dW.js.map +1 -0
  12. package/dist/index.cjs +13 -1
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.js +30 -18
  15. package/dist/index.js.map +1 -1
  16. package/dist/lib/discovery.d.ts +110 -0
  17. package/dist/lib/index.d.ts +1 -0
  18. package/dist/mcp/entry.cjs +2 -10
  19. package/dist/mcp/entry.cjs.map +1 -1
  20. package/dist/mcp/entry.js +1 -9
  21. package/dist/mcp/entry.js.map +1 -1
  22. package/dist/{simulator-url-CexnaL-e.js → simulator-url-BOSS60NS.js} +592 -4
  23. package/dist/simulator-url-BOSS60NS.js.map +1 -0
  24. package/dist/{simulator-url-CG8lAAC3.cjs → simulator-url-BStCoFTv.cjs} +593 -5
  25. package/dist/simulator-url-BStCoFTv.cjs.map +1 -0
  26. package/dist/types/simulation.d.ts +4 -1
  27. package/package.json +1 -1
  28. package/template/.sunpeak/dev.tsx +6 -91
  29. package/template/README.md +2 -2
  30. package/template/dist/albums.json +1 -1
  31. package/template/dist/carousel.json +1 -1
  32. package/template/dist/map.json +1 -1
  33. package/template/dist/review.json +1 -1
  34. package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_SegmentedControl.js +4 -4
  35. package/template/node_modules/.vite/deps/@openai_apps-sdk-ui_components_Select.js +6 -6
  36. package/template/node_modules/.vite/deps/_metadata.json +25 -25
  37. package/template/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  38. package/template/src/resources/index.ts +3 -25
  39. package/template/src/simulations/index.ts +3 -10
  40. package/dist/simulator-url-CG8lAAC3.cjs.map +0 -1
  41. package/dist/simulator-url-CexnaL-e.js.map +0 -1
@@ -8,10 +8,13 @@ import type * as React from 'react';
8
8
  /**
9
9
  * A simulation packages a component with its example data and metadata.
10
10
  * Each simulation represents a complete tool experience in the simulator.
11
+ *
12
+ * Specify either `resourceComponent` (React component) or `resourceScript` (URL to built .js file).
11
13
  */
12
14
  export interface Simulation {
13
15
  name: string;
14
- resourceComponent: React.ComponentType;
16
+ resourceComponent?: React.ComponentType;
17
+ resourceScript?: string;
15
18
  userMessage?: string;
16
19
  tool: Tool;
17
20
  resource: Resource;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sunpeak",
3
- "version": "0.9.2",
3
+ "version": "0.9.6",
4
4
  "description": "The ChatGPT App framework. Quickstart, build, & test your ChatGPT App locally!",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -10,101 +10,16 @@
10
10
  */
11
11
  import { StrictMode } from 'react';
12
12
  import { createRoot } from 'react-dom/client';
13
- import { ChatGPTSimulator, type Simulation } from 'sunpeak';
13
+ import { ChatGPTSimulator, buildDevSimulations } from 'sunpeak';
14
14
  import resourceComponents from '../src/resources';
15
15
  import '../src/styles/globals.css';
16
16
 
17
- // Auto-discover all simulation and resource JSON files
18
- const simulationModules = import.meta.glob('../src/simulations/*-simulation.json', {
19
- eager: true,
17
+ // Build simulations from discovered files
18
+ const simulations = buildDevSimulations({
19
+ simulationModules: import.meta.glob('../src/simulations/*-simulation.json', { eager: true }),
20
+ resourceModules: import.meta.glob('../src/resources/*-resource.json', { eager: true }),
21
+ resourceComponents: resourceComponents as Record<string, React.ComponentType>,
20
22
  });
21
- const resourceModules = import.meta.glob('../src/resources/*-resource.json', { eager: true });
22
-
23
- // Build resource map from discovered files
24
- type ResourceData = { name: string; [key: string]: unknown };
25
- const resourcesMap = new Map<string, ResourceData>();
26
- for (const [path, module] of Object.entries(resourceModules)) {
27
- // Extract key from path: '../src/resources/review-resource.json' -> 'review'
28
- const match = path.match(/\/([^/]+)-resource\.json$/);
29
- const key = match?.[1];
30
- if (key) {
31
- resourcesMap.set(key, (module as { default: ResourceData }).default);
32
- }
33
- }
34
-
35
- // Get sorted resource keys for best-match lookup (longest first)
36
- const resourceKeys = Array.from(resourcesMap.keys()).sort((a, b) => b.length - a.length);
37
-
38
- /**
39
- * Find the best matching resource for a simulation key.
40
- * Matches the longest resource name that is a prefix of the simulation key.
41
- * e.g., 'albums-show' matches 'albums' (not 'album' if both exist)
42
- */
43
- function findResourceKey(simulationKey: string): string | undefined {
44
- // Simulation key format: {resource}-{tool} (e.g., 'albums-show')
45
- // Find the longest resource name that matches as a prefix followed by '-'
46
- for (const resourceKey of resourceKeys) {
47
- if (simulationKey === resourceKey || simulationKey.startsWith(resourceKey + '-')) {
48
- return resourceKey;
49
- }
50
- }
51
- return undefined;
52
- }
53
-
54
- /**
55
- * Convert resource name to component name
56
- * Example: 'carousel' -> 'CarouselResource', 'review' -> 'ReviewResource'
57
- */
58
- function getResourceComponent(name: string): React.ComponentType {
59
- const pascalName = name.charAt(0).toUpperCase() + name.slice(1);
60
- const componentName = `${pascalName}Resource`;
61
-
62
- const component = (resourceComponents as Record<string, React.ComponentType>)[componentName];
63
-
64
- if (!component) {
65
- throw new Error(
66
- `Resource component "${componentName}" not found for resource "${name}". ` +
67
- `Make sure src/resources/${name}-resource.tsx exists with a default export.`
68
- );
69
- }
70
-
71
- return component;
72
- }
73
-
74
- // Build simulations object from discovered files
75
- type SimulationData = Omit<Simulation, 'name' | 'resourceComponent' | 'resource'>;
76
- const simulations: Record<string, Simulation> = {};
77
-
78
- for (const [path, module] of Object.entries(simulationModules)) {
79
- // Extract simulation key from path: '../src/simulations/albums-show-simulation.json' -> 'albums-show'
80
- const match = path.match(/\/([^/]+)-simulation\.json$/);
81
- const simulationKey = match?.[1];
82
- if (!simulationKey) continue;
83
-
84
- const simulation = (module as { default: SimulationData }).default;
85
-
86
- // Find matching resource by best prefix match
87
- const resourceKey = findResourceKey(simulationKey);
88
- if (!resourceKey) {
89
- console.warn(
90
- `No matching resource found for simulation "${simulationKey}". ` +
91
- `Expected a resource file like src/resources/${simulationKey.split('-')[0]}-resource.json`
92
- );
93
- continue;
94
- }
95
-
96
- const resource = resourcesMap.get(resourceKey)!;
97
-
98
- simulations[simulationKey] = {
99
- ...simulation,
100
- name: simulationKey,
101
- resource: {
102
- uri: `ui://${resource.name}`, // Dummy URI.
103
- ...resource,
104
- },
105
- resourceComponent: getResourceComponent(resource.name),
106
- };
107
- }
108
23
 
109
24
  // Read app config from environment or use defaults
110
25
  const appName = import.meta.env?.VITE_APP_NAME || 'Sunpeak';
@@ -100,8 +100,8 @@ Each `.json` file is a copy of each of your resource MCP metadata files `src/res
100
100
 
101
101
  To add a new UI (MCP Resource), simply create the following files:
102
102
 
103
- - `src/resource/NAME-resource.tsx`
104
- - `src/resource/NAME-resource.json`
103
+ - `src/resources/NAME-resource.tsx`
104
+ - `src/resources/NAME-resource.json`
105
105
  - `src/simulations/NAME-TOOLNAME-simulation.json`
106
106
 
107
107
  Only the resource files are required to generate a production build and ship a UI. Create the simulation file if you want to preview your resource in `sunpeak dev` or `sunpeak mcp`.
@@ -12,5 +12,5 @@
12
12
  ]
13
13
  }
14
14
  },
15
- "uri": "ui://albums-mk1rwc3i"
15
+ "uri": "ui://albums-mk8u79b0"
16
16
  }
@@ -12,5 +12,5 @@
12
12
  ]
13
13
  }
14
14
  },
15
- "uri": "ui://carousel-mk1rwc3i"
15
+ "uri": "ui://carousel-mk8u79b0"
16
16
  }
@@ -18,5 +18,5 @@
18
18
  ]
19
19
  }
20
20
  },
21
- "uri": "ui://map-mk1rwc3i"
21
+ "uri": "ui://map-mk8u79b0"
22
22
  }
@@ -12,5 +12,5 @@
12
12
  ]
13
13
  }
14
14
  },
15
- "uri": "ui://review-mk1rwc3i"
15
+ "uri": "ui://review-mk8u79b0"
16
16
  }
@@ -1,7 +1,3 @@
1
- import {
2
- dist_exports4 as dist_exports
3
- } from "./chunk-TSEQUROC.js";
4
- import "./chunk-XZTIOEPG.js";
5
1
  import {
6
2
  useResizeObserver
7
3
  } from "./chunk-JAGHY6H6.js";
@@ -10,6 +6,10 @@ import {
10
6
  waitForAnimationFrame
11
7
  } from "./chunk-DYQDWJMS.js";
12
8
  import "./chunk-EGRHWZRV.js";
9
+ import {
10
+ dist_exports4 as dist_exports
11
+ } from "./chunk-TSEQUROC.js";
12
+ import "./chunk-XZTIOEPG.js";
13
13
  import {
14
14
  clsx_default
15
15
  } from "./chunk-CNYJBM5F.js";
@@ -1,12 +1,6 @@
1
1
  import {
2
2
  Input
3
3
  } from "./chunk-2DZGWGIP.js";
4
- import {
5
- dist_exports,
6
- dist_exports3 as dist_exports2,
7
- dist_exports5 as dist_exports3
8
- } from "./chunk-TSEQUROC.js";
9
- import "./chunk-XZTIOEPG.js";
10
4
  import {
11
5
  Button,
12
6
  LoadingIndicator,
@@ -25,6 +19,12 @@ import {
25
19
  waitForAnimationFrame
26
20
  } from "./chunk-DYQDWJMS.js";
27
21
  import "./chunk-EGRHWZRV.js";
22
+ import {
23
+ dist_exports,
24
+ dist_exports3 as dist_exports2,
25
+ dist_exports5 as dist_exports3
26
+ } from "./chunk-TSEQUROC.js";
27
+ import "./chunk-XZTIOEPG.js";
28
28
  import {
29
29
  clsx_default
30
30
  } from "./chunk-CNYJBM5F.js";
@@ -7,115 +7,115 @@
7
7
  "react": {
8
8
  "src": "../../../../node_modules/.pnpm/react@19.2.3/node_modules/react/index.js",
9
9
  "file": "react.js",
10
- "fileHash": "9f8922cb",
10
+ "fileHash": "5dce5a83",
11
11
  "needsInterop": true
12
12
  },
13
13
  "react-dom": {
14
14
  "src": "../../../../node_modules/.pnpm/react-dom@19.2.3_react@19.2.3/node_modules/react-dom/index.js",
15
15
  "file": "react-dom.js",
16
- "fileHash": "b39e5f22",
16
+ "fileHash": "f1f9ceb7",
17
17
  "needsInterop": true
18
18
  },
19
19
  "react/jsx-dev-runtime": {
20
20
  "src": "../../../../node_modules/.pnpm/react@19.2.3/node_modules/react/jsx-dev-runtime.js",
21
21
  "file": "react_jsx-dev-runtime.js",
22
- "fileHash": "98b0fafe",
22
+ "fileHash": "6d41e9e9",
23
23
  "needsInterop": true
24
24
  },
25
25
  "react/jsx-runtime": {
26
26
  "src": "../../../../node_modules/.pnpm/react@19.2.3/node_modules/react/jsx-runtime.js",
27
27
  "file": "react_jsx-runtime.js",
28
- "fileHash": "589dec17",
28
+ "fileHash": "76d0ee1e",
29
29
  "needsInterop": true
30
30
  },
31
31
  "@openai/apps-sdk-ui/components/Avatar": {
32
32
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.1_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._90324f97b7190ccfdbe40a9e8bef3385/node_modules/@openai/apps-sdk-ui/dist/es/components/Avatar/index.js",
33
33
  "file": "@openai_apps-sdk-ui_components_Avatar.js",
34
- "fileHash": "e1ca40fe",
34
+ "fileHash": "cc1e3682",
35
35
  "needsInterop": false
36
36
  },
37
37
  "@openai/apps-sdk-ui/components/Button": {
38
38
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.1_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._90324f97b7190ccfdbe40a9e8bef3385/node_modules/@openai/apps-sdk-ui/dist/es/components/Button/index.js",
39
39
  "file": "@openai_apps-sdk-ui_components_Button.js",
40
- "fileHash": "4566ca48",
40
+ "fileHash": "ae4fe364",
41
41
  "needsInterop": false
42
42
  },
43
43
  "@openai/apps-sdk-ui/components/Checkbox": {
44
44
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.1_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._90324f97b7190ccfdbe40a9e8bef3385/node_modules/@openai/apps-sdk-ui/dist/es/components/Checkbox/index.js",
45
45
  "file": "@openai_apps-sdk-ui_components_Checkbox.js",
46
- "fileHash": "b2edde35",
46
+ "fileHash": "1db3db7f",
47
47
  "needsInterop": false
48
48
  },
49
49
  "@openai/apps-sdk-ui/components/Icon": {
50
50
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.1_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._90324f97b7190ccfdbe40a9e8bef3385/node_modules/@openai/apps-sdk-ui/dist/es/components/Icon/index.js",
51
51
  "file": "@openai_apps-sdk-ui_components_Icon.js",
52
- "fileHash": "e62f20ec",
52
+ "fileHash": "837c2737",
53
53
  "needsInterop": false
54
54
  },
55
55
  "@openai/apps-sdk-ui/components/Input": {
56
56
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.1_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._90324f97b7190ccfdbe40a9e8bef3385/node_modules/@openai/apps-sdk-ui/dist/es/components/Input/index.js",
57
57
  "file": "@openai_apps-sdk-ui_components_Input.js",
58
- "fileHash": "0204cb80",
58
+ "fileHash": "062e2a54",
59
59
  "needsInterop": false
60
60
  },
61
61
  "@openai/apps-sdk-ui/components/SegmentedControl": {
62
62
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.1_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._90324f97b7190ccfdbe40a9e8bef3385/node_modules/@openai/apps-sdk-ui/dist/es/components/SegmentedControl/index.js",
63
63
  "file": "@openai_apps-sdk-ui_components_SegmentedControl.js",
64
- "fileHash": "7bf18eee",
64
+ "fileHash": "6e036e2c",
65
65
  "needsInterop": false
66
66
  },
67
67
  "@openai/apps-sdk-ui/components/Select": {
68
68
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.1_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._90324f97b7190ccfdbe40a9e8bef3385/node_modules/@openai/apps-sdk-ui/dist/es/components/Select/index.js",
69
69
  "file": "@openai_apps-sdk-ui_components_Select.js",
70
- "fileHash": "79ea3e4b",
70
+ "fileHash": "6ca9ce17",
71
71
  "needsInterop": false
72
72
  },
73
73
  "@openai/apps-sdk-ui/components/Textarea": {
74
74
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.1_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._90324f97b7190ccfdbe40a9e8bef3385/node_modules/@openai/apps-sdk-ui/dist/es/components/Textarea/index.js",
75
75
  "file": "@openai_apps-sdk-ui_components_Textarea.js",
76
- "fileHash": "44843a19",
76
+ "fileHash": "6bbf0e0f",
77
77
  "needsInterop": false
78
78
  },
79
79
  "@openai/apps-sdk-ui/theme": {
80
80
  "src": "../../../../node_modules/.pnpm/@openai+apps-sdk-ui@0.2.1_@types+react-dom@19.2.3_@types+react@19.2.7__@types+react@19._90324f97b7190ccfdbe40a9e8bef3385/node_modules/@openai/apps-sdk-ui/dist/es/lib/theme.js",
81
81
  "file": "@openai_apps-sdk-ui_theme.js",
82
- "fileHash": "e60dca1c",
82
+ "fileHash": "125dd763",
83
83
  "needsInterop": false
84
84
  },
85
85
  "clsx": {
86
86
  "src": "../../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs",
87
87
  "file": "clsx.js",
88
- "fileHash": "f15d30c1",
88
+ "fileHash": "adae7188",
89
89
  "needsInterop": false
90
90
  },
91
91
  "embla-carousel-react": {
92
92
  "src": "../../../../node_modules/.pnpm/embla-carousel-react@8.6.0_react@19.2.3/node_modules/embla-carousel-react/esm/embla-carousel-react.esm.js",
93
93
  "file": "embla-carousel-react.js",
94
- "fileHash": "6b3141d4",
94
+ "fileHash": "27e70439",
95
95
  "needsInterop": false
96
96
  },
97
97
  "embla-carousel-wheel-gestures": {
98
98
  "src": "../../../../node_modules/.pnpm/embla-carousel-wheel-gestures@8.1.0_embla-carousel@8.6.0/node_modules/embla-carousel-wheel-gestures/dist/embla-carousel-wheel-gestures.esm.js",
99
99
  "file": "embla-carousel-wheel-gestures.js",
100
- "fileHash": "efa2153a",
100
+ "fileHash": "03d5e96c",
101
101
  "needsInterop": false
102
102
  },
103
103
  "mapbox-gl": {
104
104
  "src": "../../../../node_modules/.pnpm/mapbox-gl@3.17.0/node_modules/mapbox-gl/dist/mapbox-gl.js",
105
105
  "file": "mapbox-gl.js",
106
- "fileHash": "2b20f694",
106
+ "fileHash": "af8061a4",
107
107
  "needsInterop": true
108
108
  },
109
109
  "react-dom/client": {
110
110
  "src": "../../../../node_modules/.pnpm/react-dom@19.2.3_react@19.2.3/node_modules/react-dom/client.js",
111
111
  "file": "react-dom_client.js",
112
- "fileHash": "5e9ee085",
112
+ "fileHash": "9add8d51",
113
113
  "needsInterop": true
114
114
  },
115
115
  "tailwind-merge": {
116
116
  "src": "../../../../node_modules/.pnpm/tailwind-merge@3.4.0/node_modules/tailwind-merge/dist/bundle-mjs.mjs",
117
117
  "file": "tailwind-merge.js",
118
- "fileHash": "487e9f49",
118
+ "fileHash": "eaf80d9a",
119
119
  "needsInterop": false
120
120
  }
121
121
  },
@@ -123,12 +123,6 @@
123
123
  "chunk-2DZGWGIP": {
124
124
  "file": "chunk-2DZGWGIP.js"
125
125
  },
126
- "chunk-TSEQUROC": {
127
- "file": "chunk-TSEQUROC.js"
128
- },
129
- "chunk-XZTIOEPG": {
130
- "file": "chunk-XZTIOEPG.js"
131
- },
132
126
  "chunk-N6DVYEXK": {
133
127
  "file": "chunk-N6DVYEXK.js"
134
128
  },
@@ -144,6 +138,12 @@
144
138
  "chunk-EGRHWZRV": {
145
139
  "file": "chunk-EGRHWZRV.js"
146
140
  },
141
+ "chunk-TSEQUROC": {
142
+ "file": "chunk-TSEQUROC.js"
143
+ },
144
+ "chunk-XZTIOEPG": {
145
+ "file": "chunk-XZTIOEPG.js"
146
+ },
147
147
  "chunk-CNYJBM5F": {
148
148
  "file": "chunk-CNYJBM5F.js"
149
149
  },
@@ -1 +1 @@
1
- {"version":"4.0.16","results":[[":src/components/album/albums.test.tsx",{"duration":337.9990579999999,"failed":false}],[":src/resources/carousel-resource.test.tsx",{"duration":249.7428500000001,"failed":false}],[":src/resources/review-resource.test.tsx",{"duration":532.4556819999998,"failed":false}],[":src/components/map/place-inspector.test.tsx",{"duration":402.02946900000006,"failed":false}],[":src/components/map/map-view.test.tsx",{"duration":91.84771499999988,"failed":false}],[":src/components/album/fullscreen-viewer.test.tsx",{"duration":254.4845110000001,"failed":false}],[":src/components/map/place-list.test.tsx",{"duration":157.57383200000004,"failed":false}],[":src/components/map/place-card.test.tsx",{"duration":353.70146399999976,"failed":false}],[":src/components/map/place-carousel.test.tsx",{"duration":461.7055280000002,"failed":false}],[":src/components/album/album-carousel.test.tsx",{"duration":96.31957500000021,"failed":false}],[":src/components/carousel/carousel.test.tsx",{"duration":88.95574399999987,"failed":false}],[":src/resources/map-resource.test.tsx",{"duration":294.10313500000007,"failed":false}],[":src/resources/albums-resource.test.tsx",{"duration":276.18517599999996,"failed":false}],[":src/components/album/film-strip.test.tsx",{"duration":478.3753600000002,"failed":false}],[":src/components/album/album-card.test.tsx",{"duration":350.313899,"failed":false}],[":src/components/carousel/card.test.tsx",{"duration":99.09871700000008,"failed":false}]]}
1
+ {"version":"4.0.16","results":[[":src/resources/review-resource.test.tsx",{"duration":559.5211790000001,"failed":false}],[":src/resources/carousel-resource.test.tsx",{"duration":274.45098800000005,"failed":false}],[":src/components/album/albums.test.tsx",{"duration":293.8420480000002,"failed":false}],[":src/components/map/place-inspector.test.tsx",{"duration":419.9698040000003,"failed":false}],[":src/components/map/map-view.test.tsx",{"duration":74.87890800000014,"failed":false}],[":src/components/album/fullscreen-viewer.test.tsx",{"duration":268.03875199999993,"failed":false}],[":src/components/map/place-list.test.tsx",{"duration":144.34349999999995,"failed":false}],[":src/components/map/place-card.test.tsx",{"duration":357.63647600000013,"failed":false}],[":src/components/map/place-carousel.test.tsx",{"duration":430.33475,"failed":false}],[":src/components/album/album-carousel.test.tsx",{"duration":88.78069400000004,"failed":false}],[":src/components/carousel/carousel.test.tsx",{"duration":88.54834000000005,"failed":false}],[":src/resources/map-resource.test.tsx",{"duration":280.1168210000001,"failed":false}],[":src/resources/albums-resource.test.tsx",{"duration":286.3251439999999,"failed":false}],[":src/components/album/film-strip.test.tsx",{"duration":479.77164000000016,"failed":false}],[":src/components/album/album-card.test.tsx",{"duration":331.31021499999997,"failed":false}],[":src/components/carousel/card.test.tsx",{"duration":85.36528099999987,"failed":false}]]}
@@ -8,32 +8,10 @@
8
8
  * - Default export: export default MyComponent
9
9
  * - Named export: export const ReviewResource = ...
10
10
  */
11
+ import { createResourceExports } from 'sunpeak';
11
12
 
12
13
  // Auto-discover all resource component files
13
14
  const resourceModules = import.meta.glob('./*-resource.tsx', { eager: true });
14
15
 
15
- // Build exports object from discovered files
16
- const resources: Record<string, React.ComponentType> = {};
17
-
18
- for (const [path, module] of Object.entries(resourceModules)) {
19
- // Extract key from path: './review-resource.tsx' -> 'review'
20
- const match = path.match(/\.\/(.+)-resource\.tsx$/);
21
- const key = match?.[1];
22
- if (!key) continue;
23
-
24
- // Convert to PascalCase and append 'Resource': 'review' -> 'ReviewResource'
25
- const pascalKey = key.charAt(0).toUpperCase() + key.slice(1);
26
- const exportName = `${pascalKey}Resource`;
27
-
28
- const mod = module as Record<string, unknown>;
29
-
30
- // Try default export first, then named export matching the expected name
31
- const component = mod.default ?? mod[exportName];
32
-
33
- // Accept functions (regular components) or objects (forwardRef/memo components)
34
- if (component && (typeof component === 'function' || typeof component === 'object')) {
35
- resources[exportName] = component as React.ComponentType;
36
- }
37
- }
38
-
39
- export default resources;
16
+ // Build exports object from discovered files using library helper
17
+ export default createResourceExports(resourceModules);
@@ -7,17 +7,10 @@
7
7
  * This file can be safely imported in Node.js contexts (like MCP servers)
8
8
  * without causing issues with CSS imports or React components.
9
9
  */
10
+ import { createSimulationIndex } from 'sunpeak';
10
11
 
11
12
  // Auto-discover all simulation JSON files
12
13
  const simulationModules = import.meta.glob('./*-simulation.json', { eager: true });
13
14
 
14
- // Build SIMULATIONS object from discovered files
15
- // Key is the full name without -simulation.json suffix (e.g., 'albums-show')
16
- export const SIMULATIONS = Object.fromEntries(
17
- Object.entries(simulationModules).map(([path, module]) => {
18
- // Extract simulation key from path: './albums-show-simulation.json' -> 'albums-show'
19
- const match = path.match(/\.\/(.+)-simulation\.json$/);
20
- const key = match?.[1] ?? path;
21
- return [key, (module as { default: unknown }).default];
22
- })
23
- ) as Record<string, unknown>;
15
+ // Build SIMULATIONS object from discovered files using library helper
16
+ export const SIMULATIONS = createSimulationIndex(simulationModules);