sunpeak 0.16.14 → 0.16.18

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 (124) hide show
  1. package/README.md +11 -9
  2. package/bin/commands/build.mjs +221 -222
  3. package/bin/commands/dev.mjs +194 -15
  4. package/bin/commands/new.mjs +21 -2
  5. package/bin/commands/start.mjs +25 -13
  6. package/dist/chatgpt/chatgpt-conversation.d.ts +3 -1
  7. package/dist/chatgpt/globals.css +37 -8
  8. package/dist/chatgpt/index.cjs +4 -5
  9. package/dist/chatgpt/index.cjs.map +1 -1
  10. package/dist/chatgpt/index.d.ts +2 -2
  11. package/dist/chatgpt/index.js +5 -6
  12. package/dist/chatgpt/index.js.map +1 -1
  13. package/dist/claude/claude-conversation.d.ts +3 -1
  14. package/dist/claude/index.cjs +2 -2
  15. package/dist/claude/index.d.ts +1 -1
  16. package/dist/claude/index.js +2 -2
  17. package/dist/{discovery-CH80W5l9.js → discovery-BVqD-JsT.js} +29 -22
  18. package/dist/discovery-BVqD-JsT.js.map +1 -0
  19. package/dist/{discovery-DmB8_4QL.cjs → discovery-D1gpaVz4.cjs} +29 -22
  20. package/dist/discovery-D1gpaVz4.cjs.map +1 -0
  21. package/dist/hooks/index.d.ts +10 -1
  22. package/dist/hooks/safe-area.d.ts +6 -2
  23. package/dist/hooks/use-call-server-tool.d.ts +2 -0
  24. package/dist/hooks/use-device-capabilities.d.ts +3 -0
  25. package/dist/hooks/use-platform.d.ts +3 -0
  26. package/dist/hooks/use-styles.d.ts +2 -0
  27. package/dist/hooks/use-time-zone.d.ts +1 -0
  28. package/dist/hooks/use-tool-info.d.ts +3 -0
  29. package/dist/hooks/use-user-agent.d.ts +1 -0
  30. package/dist/hooks/use-viewport.d.ts +3 -0
  31. package/dist/index-BITfgMxk.js +29 -0
  32. package/dist/index-BITfgMxk.js.map +1 -0
  33. package/dist/{index-B8WZDM6g.cjs → index-CJ3nfg7Q.cjs} +5 -4
  34. package/dist/index-CJ3nfg7Q.cjs.map +1 -0
  35. package/dist/{index-Ccuh7yMs.js → index-C_uYg-eE.js} +5 -4
  36. package/dist/index-C_uYg-eE.js.map +1 -0
  37. package/dist/index-DxCrGGzy.cjs +28 -0
  38. package/dist/index-DxCrGGzy.cjs.map +1 -0
  39. package/dist/index.cjs +43 -6
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.js +43 -6
  42. package/dist/index.js.map +1 -1
  43. package/dist/lib/discovery-cli.cjs +2 -2
  44. package/dist/lib/discovery-cli.cjs.map +1 -1
  45. package/dist/lib/discovery-cli.js +2 -2
  46. package/dist/lib/discovery-cli.js.map +1 -1
  47. package/dist/mcp/index.cjs +317 -150
  48. package/dist/mcp/index.cjs.map +1 -1
  49. package/dist/mcp/index.d.ts +2 -2
  50. package/dist/mcp/index.js +318 -151
  51. package/dist/mcp/index.js.map +1 -1
  52. package/dist/mcp/production-server.d.ts +22 -5
  53. package/dist/mcp/types.d.ts +7 -4
  54. package/dist/simulator/hosts.d.ts +2 -0
  55. package/dist/simulator/index.cjs +4 -3
  56. package/dist/simulator/index.cjs.map +1 -1
  57. package/dist/simulator/index.d.ts +2 -0
  58. package/dist/simulator/index.js +5 -4
  59. package/dist/simulator/mcp-app-host.d.ts +6 -0
  60. package/dist/simulator/simple-sidebar.d.ts +18 -4
  61. package/dist/simulator/simulator-url.d.ts +8 -0
  62. package/dist/simulator/simulator.d.ts +13 -1
  63. package/dist/simulator/use-simulator-state.d.ts +10 -6
  64. package/dist/{simulator-BAcm4Pby.js → simulator-DfTsXTK4.js} +1009 -497
  65. package/dist/simulator-DfTsXTK4.js.map +1 -0
  66. package/dist/{simulator-CiVQFdZN.cjs → simulator-mbqealxY.cjs} +1007 -495
  67. package/dist/simulator-mbqealxY.cjs.map +1 -0
  68. package/dist/{simulator-url-rgg_KYOg.cjs → simulator-url-DcSYRl-P.cjs} +7 -1
  69. package/dist/simulator-url-DcSYRl-P.cjs.map +1 -0
  70. package/dist/{simulator-url-CuLqtnSS.js → simulator-url-j_XV3EoP.js} +7 -1
  71. package/dist/simulator-url-j_XV3EoP.js.map +1 -0
  72. package/dist/style.css +37 -8
  73. package/dist/types/simulation.d.ts +28 -2
  74. package/package.json +2 -2
  75. package/template/.sunpeak/dev.tsx +28 -2
  76. package/template/README.md +5 -1
  77. package/template/playwright.config.ts +6 -3
  78. package/template/src/resources/albums/albums.test.tsx +1 -0
  79. package/template/src/resources/albums/albums.tsx +5 -2
  80. package/template/src/resources/albums/components/albums.test.tsx +22 -18
  81. package/template/src/resources/albums/components/albums.tsx +63 -7
  82. package/template/src/resources/albums/components/fullscreen-viewer.test.tsx +3 -25
  83. package/template/src/resources/albums/components/fullscreen-viewer.tsx +2 -3
  84. package/template/src/resources/carousel/carousel.test.tsx +12 -16
  85. package/template/src/resources/carousel/carousel.tsx +47 -5
  86. package/template/src/resources/map/components/map.tsx +65 -9
  87. package/template/src/resources/map/map.test.tsx +0 -1
  88. package/template/src/resources/review/review.test.tsx +230 -41
  89. package/template/src/resources/review/review.tsx +161 -102
  90. package/template/src/tools/review-diff.test.ts +73 -0
  91. package/template/src/tools/review-diff.ts +29 -2
  92. package/template/src/tools/review-post.test.ts +100 -0
  93. package/template/src/tools/review-post.ts +30 -2
  94. package/template/src/tools/review-purchase.test.ts +111 -0
  95. package/template/src/tools/review-purchase.ts +35 -2
  96. package/template/src/tools/review.test.ts +40 -0
  97. package/template/src/tools/review.ts +41 -0
  98. package/template/src/tools/show-albums.test.ts +42 -0
  99. package/template/src/tools/show-albums.ts +22 -2
  100. package/template/src/tools/show-carousel.test.ts +45 -0
  101. package/template/src/tools/show-carousel.ts +19 -2
  102. package/template/src/tools/show-map.test.ts +74 -0
  103. package/template/src/tools/show-map.ts +21 -2
  104. package/template/tests/e2e/albums.spec.ts +75 -0
  105. package/template/tests/e2e/carousel.spec.ts +65 -0
  106. package/template/tests/e2e/global-setup.ts +25 -0
  107. package/template/tests/e2e/map.spec.ts +60 -0
  108. package/template/tests/e2e/review.spec.ts +168 -11
  109. package/template/tests/simulations/review-diff.json +31 -10
  110. package/template/tests/simulations/review-post.json +26 -5
  111. package/template/tests/simulations/review-purchase.json +27 -6
  112. package/dist/chatgpt/chatgpt-simulator.d.ts +0 -10
  113. package/dist/discovery-CH80W5l9.js.map +0 -1
  114. package/dist/discovery-DmB8_4QL.cjs.map +0 -1
  115. package/dist/index-B8WZDM6g.cjs.map +0 -1
  116. package/dist/index-Ccuh7yMs.js.map +0 -1
  117. package/dist/index-CsGD9XQo.js +0 -511
  118. package/dist/index-CsGD9XQo.js.map +0 -1
  119. package/dist/index-DoKDj4GA.cjs +0 -526
  120. package/dist/index-DoKDj4GA.cjs.map +0 -1
  121. package/dist/simulator-BAcm4Pby.js.map +0 -1
  122. package/dist/simulator-CiVQFdZN.cjs.map +0 -1
  123. package/dist/simulator-url-CuLqtnSS.js.map +0 -1
  124. package/dist/simulator-url-rgg_KYOg.cjs.map +0 -1
package/README.md CHANGED
@@ -18,7 +18,7 @@
18
18
 
19
19
  Local-first MCP Apps framework.
20
20
 
21
- Quickstart, build, test, and ship your Claude or ChatGPT App!
21
+ Quickstart, build, test, and ship your Claude Connector or ChatGPT App!
22
22
 
23
23
  [Demo (Hosted)](https://sunpeak.ai/simulator) ~
24
24
  [Demo (Video)](https://cdn.sunpeak.ai/sunpeak-demo-prod.mp4) ~
@@ -62,14 +62,16 @@ my-app/
62
62
  ├── src/
63
63
  │ ├── resources/
64
64
  │ │ └── review/
65
- │ │ └── review.tsx # Review UI component + resource metadata.
65
+ │ │ └── review.tsx # Review UI component + resource metadata.
66
66
  │ ├── tools/
67
- │ │ ├── review-diff.ts # Tool with handler, schema, and resource reference.
68
- │ │ └── review-post.ts # Multiple tools can share one resource.
69
- │ └── server.ts # Optional: auth, server config.
67
+ │ │ ├── review-diff.ts # Tool with handler, schema, and optional resource link.
68
+ │ │ ├── review-post.ts # Multiple tools can share one resource.
69
+ └── review.ts # Backend-only tool (no resource, no UI).
70
+ │ └── server.ts # Optional: auth, server config.
70
71
  ├── tests/simulations/
71
- │ ├── review-diff.json # Mock state for testing.
72
- └── review-post.json # Mock state for testing.
72
+ │ ├── review-diff.json # Mock state for testing (includes serverTools).
73
+ ├── review-post.json # Mock state for testing (includes serverTools).
74
+ │ └── review-purchase.json # Mock state for testing (includes serverTools).
73
75
  └── package.json
74
76
  ```
75
77
 
@@ -77,7 +79,7 @@ my-app/
77
79
  2. UI components: Production-ready components following MCP App design guidelines.
78
80
  3. Convention over configuration:
79
81
  1. Create a UI by creating a `.tsx` file in `src/resources/{name}/` that exports a `ResourceConfig` and a React component ([example below](#resource-component)).
80
- 2. Create a tool by creating a `.ts` file in `src/tools/` that exports `tool` (metadata with resource reference), `schema` (Zod), and a `default` handler ([example below](#tool-file)).
82
+ 2. Create a tool by creating a `.ts` file in `src/tools/` that exports `tool` (metadata with optional resource link), `schema` (Zod), and a `default` handler ([example below](#tool-file)). Tools without a `resource` field are registered as plain MCP tools (no UI).
81
83
  3. Create test state (`Simulation`s) by creating a `.json` file in `tests/simulations/` ([example below](#simulation)).
82
84
 
83
85
  ### The `sunpeak` CLI
@@ -118,7 +120,7 @@ export function ReviewResource() {
118
120
 
119
121
  ### Tool File
120
122
 
121
- Each tool `.ts` file exports metadata (with a direct resource reference), a Zod schema, and a handler:
123
+ Each tool `.ts` file exports metadata (with an optional resource link for UI tools), a Zod schema, and a handler:
122
124
 
123
125
  ```ts
124
126
  // src/tools/review-diff.ts
@@ -75,17 +75,11 @@ export async function build(projectRoot = process.cwd(), { quiet = false } = {})
75
75
  const resourcesDir = path.join(projectRoot, 'src/resources');
76
76
  const templateFile = path.join(projectRoot, 'src/index-resource.tsx');
77
77
 
78
- // Validate project structure
79
- if (!existsSync(resourcesDir)) {
80
- console.error('Error: src/resources directory not found');
81
- console.error('Expected location: ' + resourcesDir);
82
- console.error('\nThe build command expects the standard Sunpeak project structure.');
83
- console.error('If you have customized your project structure, you may need to use');
84
- console.error('a custom build script instead of "sunpeak build".');
85
- process.exit(1);
86
- }
78
+ // Check if resources exist (optional — projects may have only plain tools)
79
+ const hasResources = existsSync(resourcesDir);
80
+ const hasTemplateFile = existsSync(templateFile);
87
81
 
88
- if (!existsSync(templateFile)) {
82
+ if (hasResources && !hasTemplateFile) {
89
83
  console.error('Error: src/index-resource.tsx not found');
90
84
  console.error('Expected location: ' + templateFile);
91
85
  console.error('\nThis file is the template entry point for building resources.');
@@ -93,56 +87,7 @@ export async function build(projectRoot = process.cwd(), { quiet = false } = {})
93
87
  process.exit(1);
94
88
  }
95
89
 
96
- // Import vite and plugins from the user's project (not from sunpeak's node_modules)
97
- // This allows sunpeak to work when installed globally
98
- // We resolve to ESM entry points to avoid the CJS deprecation warning from Vite
99
90
  const require = createRequire(path.join(projectRoot, 'package.json'));
100
- let viteBuild, react, tailwindcss;
101
- try {
102
- const [viteModule, reactModule, tailwindModule] = await Promise.all([
103
- import(resolveEsmEntry(require, 'vite')),
104
- import(resolveEsmEntry(require, '@vitejs/plugin-react')),
105
- import(resolveEsmEntry(require, '@tailwindcss/vite')),
106
- ]);
107
- viteBuild = viteModule.build;
108
- react = reactModule.default;
109
- tailwindcss = tailwindModule.default;
110
- } catch (error) {
111
- console.error('Error: Could not load build dependencies from your project.');
112
- console.error('\nMake sure you have these packages installed in your project:');
113
- console.error(' - vite');
114
- console.error(' - @vitejs/plugin-react');
115
- console.error(' - @tailwindcss/vite');
116
- console.error('\nRun: npm install -D vite @vitejs/plugin-react @tailwindcss/vite');
117
- console.error('\nOriginal error:', error.message);
118
- process.exit(1);
119
- }
120
-
121
- // Plugin factory to inline CSS into the JS bundle for all output files
122
- const inlineCssPlugin = (buildOutDir) => ({
123
- name: 'inline-css',
124
- closeBundle() {
125
- const cssFile = path.join(buildOutDir, 'style.css');
126
-
127
- if (existsSync(cssFile)) {
128
- const css = readFileSync(cssFile, 'utf-8');
129
- const injectCss = `(function(){var s=document.createElement('style');s.textContent=${JSON.stringify(css)};document.head.appendChild(s);})();`;
130
-
131
- // Find all .js files in the dist directory and inject CSS
132
- const files = readdirSync(buildOutDir);
133
- files.forEach((file) => {
134
- if (file.endsWith('.js')) {
135
- const jsFile = path.join(buildOutDir, file);
136
- const js = readFileSync(jsFile, 'utf-8');
137
- writeFileSync(jsFile, injectCss + js);
138
- }
139
- });
140
-
141
- // Remove the separate CSS file after injecting into all bundles
142
- unlinkSync(cssFile);
143
- }
144
- },
145
- });
146
91
 
147
92
  // Clean dist and temp directories
148
93
  if (existsSync(distDir)) {
@@ -152,161 +97,213 @@ export async function build(projectRoot = process.cwd(), { quiet = false } = {})
152
97
  rmSync(tempDir, { recursive: true });
153
98
  }
154
99
  mkdirSync(distDir, { recursive: true });
155
- mkdirSync(tempDir, { recursive: true });
156
100
 
157
- // Auto-discover all resources (each resource is a subdirectory)
158
- const resourceFiles = readdirSync(resourcesDir, { withFileTypes: true })
159
- .filter(entry => entry.isDirectory())
160
- .map(entry => {
161
- const kebabName = entry.name;
101
+ // ========================================================================
102
+ // Build resources (if any exist)
103
+ // ========================================================================
162
104
 
163
- const resourceFile = `${kebabName}.tsx`;
164
- const resourcePath = path.join(resourcesDir, kebabName, resourceFile);
105
+ let resourceFiles = [];
165
106
 
166
- // Skip directories without a resource file
167
- if (!existsSync(resourcePath)) {
168
- return null;
169
- }
107
+ if (hasResources && hasTemplateFile) {
108
+ // Import vite and plugins from the user's project (not from sunpeak's node_modules)
109
+ // We resolve to ESM entry points to avoid the CJS deprecation warning from Vite
110
+ let viteBuild, react, tailwindcss;
111
+ try {
112
+ const [viteModule, reactModule, tailwindModule] = await Promise.all([
113
+ import(resolveEsmEntry(require, 'vite')),
114
+ import(resolveEsmEntry(require, '@vitejs/plugin-react')),
115
+ import(resolveEsmEntry(require, '@tailwindcss/vite')),
116
+ ]);
117
+ viteBuild = viteModule.build;
118
+ react = reactModule.default;
119
+ tailwindcss = tailwindModule.default;
120
+ } catch (error) {
121
+ console.error('Error: Could not load build dependencies from your project.');
122
+ console.error('\nMake sure you have these packages installed in your project:');
123
+ console.error(' - vite');
124
+ console.error(' - @vitejs/plugin-react');
125
+ console.error(' - @tailwindcss/vite');
126
+ console.error('\nRun: npm install -D vite @vitejs/plugin-react @tailwindcss/vite');
127
+ console.error('\nOriginal error:', error.message);
128
+ process.exit(1);
129
+ }
170
130
 
171
- // Convert kebab-case to PascalCase: 'review' -> 'Review', 'my-widget' -> 'MyWidget'
172
- const pascalName = toPascalCase(kebabName);
173
- const componentFile = resourceFile.replace(/\.tsx$/, '');
174
-
175
- return {
176
- componentName: `${pascalName}Resource`,
177
- componentFile,
178
- kebabName,
179
- resourceDir: path.join(resourcesDir, kebabName),
180
- entry: `.tmp/index-${kebabName}.tsx`,
181
- jsOutput: `${kebabName}.js`,
182
- htmlOutput: `${kebabName}.html`,
183
- buildOutDir: path.join(buildDir, kebabName),
184
- distOutDir: path.join(distDir, kebabName), // Final output: dist/{resource}/
185
- };
186
- })
187
- .filter(Boolean);
188
-
189
- if (resourceFiles.length === 0) {
190
- console.error('Error: No resource directories found in src/resources/');
191
- console.error('Each resource should be a directory like: src/resources/review/review.tsx');
192
- process.exit(1);
193
- }
131
+ // Plugin factory to inline CSS into the JS bundle for all output files
132
+ const inlineCssPlugin = (buildOutDir) => ({
133
+ name: 'inline-css',
134
+ closeBundle() {
135
+ const cssFile = path.join(buildOutDir, 'style.css');
136
+
137
+ if (existsSync(cssFile)) {
138
+ const css = readFileSync(cssFile, 'utf-8');
139
+ const injectCss = `(function(){var s=document.createElement('style');s.textContent=${JSON.stringify(css)};document.head.appendChild(s);})();`;
140
+
141
+ // Find all .js files in the dist directory and inject CSS
142
+ const files = readdirSync(buildOutDir);
143
+ files.forEach((file) => {
144
+ if (file.endsWith('.js')) {
145
+ const jsFile = path.join(buildOutDir, file);
146
+ const js = readFileSync(jsFile, 'utf-8');
147
+ writeFileSync(jsFile, injectCss + js);
148
+ }
149
+ });
194
150
 
195
- log('Building all resources...\n');
151
+ // Remove the separate CSS file after injecting into all bundles
152
+ unlinkSync(cssFile);
153
+ }
154
+ },
155
+ });
196
156
 
197
- // Read and validate the template
198
- const template = readFileSync(templateFile, 'utf-8');
157
+ mkdirSync(tempDir, { recursive: true });
199
158
 
200
- // Verify template has required placeholders
201
- if (!template.includes('// RESOURCE_IMPORT')) {
202
- console.error('Error: src/index-resource.tsx is missing "// RESOURCE_IMPORT" placeholder');
203
- console.error('\nThe template file must include this comment where the resource import should go.');
204
- console.error('If you have customized this file, ensure it has the required placeholders.');
205
- process.exit(1);
206
- }
159
+ // Auto-discover all resources (each resource is a subdirectory)
160
+ resourceFiles = readdirSync(resourcesDir, { withFileTypes: true })
161
+ .filter(entry => entry.isDirectory())
162
+ .map(entry => {
163
+ const kebabName = entry.name;
207
164
 
208
- if (!template.includes('// RESOURCE_MOUNT')) {
209
- console.error('Error: src/index-resource.tsx is missing "// RESOURCE_MOUNT" placeholder');
210
- console.error('\nThe template file must include this comment where the resource mount should go.');
211
- console.error('If you have customized this file, ensure it has the required placeholders.');
212
- process.exit(1);
213
- }
165
+ const resourceFile = `${kebabName}.tsx`;
166
+ const resourcePath = path.join(resourcesDir, kebabName, resourceFile);
214
167
 
215
- // Build all resources (but don't copy yet)
216
- for (let i = 0; i < resourceFiles.length; i++) {
217
- const { componentName, componentFile, kebabName, entry, jsOutput, buildOutDir } = resourceFiles[i];
218
- log(`[${i + 1}/${resourceFiles.length}] Building ${kebabName}...`);
168
+ // Skip directories without a resource file
169
+ if (!existsSync(resourcePath)) {
170
+ return null;
171
+ }
219
172
 
220
- try {
221
- // Create build directory if it doesn't exist
222
- if (!existsSync(buildOutDir)) {
223
- mkdirSync(buildOutDir, { recursive: true });
173
+ // Convert kebab-case to PascalCase: 'review' -> 'Review', 'my-widget' -> 'MyWidget'
174
+ const pascalName = toPascalCase(kebabName);
175
+ const componentFile = resourceFile.replace(/\.tsx$/, '');
176
+
177
+ return {
178
+ componentName: `${pascalName}Resource`,
179
+ componentFile,
180
+ kebabName,
181
+ resourceDir: path.join(resourcesDir, kebabName),
182
+ entry: `.tmp/index-${kebabName}.tsx`,
183
+ jsOutput: `${kebabName}.js`,
184
+ htmlOutput: `${kebabName}.html`,
185
+ buildOutDir: path.join(buildDir, kebabName),
186
+ distOutDir: path.join(distDir, kebabName), // Final output: dist/{resource}/
187
+ };
188
+ })
189
+ .filter(Boolean);
190
+
191
+ if (resourceFiles.length > 0) {
192
+ log('Building all resources...\n');
193
+
194
+ // Read and validate the template
195
+ const template = readFileSync(templateFile, 'utf-8');
196
+
197
+ // Verify template has required placeholders
198
+ if (!template.includes('// RESOURCE_IMPORT')) {
199
+ console.error('Error: src/index-resource.tsx is missing "// RESOURCE_IMPORT" placeholder');
200
+ console.error('\nThe template file must include this comment where the resource import should go.');
201
+ console.error('If you have customized this file, ensure it has the required placeholders.');
202
+ process.exit(1);
203
+ }
204
+
205
+ if (!template.includes('// RESOURCE_MOUNT')) {
206
+ console.error('Error: src/index-resource.tsx is missing "// RESOURCE_MOUNT" placeholder');
207
+ console.error('\nThe template file must include this comment where the resource mount should go.');
208
+ console.error('If you have customized this file, ensure it has the required placeholders.');
209
+ process.exit(1);
224
210
  }
225
211
 
226
- // Create entry file from template in temp directory
227
- const entryContent = template
228
- .replace('// RESOURCE_IMPORT', `import { ${componentName}, resource } from '../src/resources/${kebabName}/${componentFile}';`)
229
- .replace('// RESOURCE_MOUNT', `createRoot(root).render(<AppProvider appInfo={{ name: ${JSON.stringify(appName)}, version: ${JSON.stringify(appVersion)} }}><${componentName} /></AppProvider>);`);
230
-
231
- const entryPath = path.join(projectRoot, entry);
232
- writeFileSync(entryPath, entryContent);
233
-
234
- // Build with vite programmatically
235
- await viteBuild({
236
- mode: 'production',
237
- root: projectRoot,
238
- ...(quiet && { logLevel: 'silent' }),
239
- plugins: [react(), tailwindcss(), inlineCssPlugin(buildOutDir)],
240
- define: {
241
- 'process.env.NODE_ENV': JSON.stringify('production'),
242
- },
243
- resolve: {
244
- conditions: ['style', 'import', 'module', 'browser', 'default'],
245
- alias: {
246
- '@': path.resolve(projectRoot, 'src'),
247
- // In workspace dev mode, use local sunpeak source
248
- ...(isTemplate && {
249
- sunpeak: parentSrc,
250
- }),
251
- },
252
- },
253
- build: {
254
- target: 'es2020',
255
- outDir: buildOutDir,
256
- emptyOutDir: true,
257
- cssCodeSplit: false,
258
- lib: {
259
- entry: entryPath,
260
- name: 'SunpeakApp',
261
- formats: ['iife'],
262
- fileName: () => jsOutput,
263
- },
264
- rollupOptions: {
265
- output: {
266
- inlineDynamicImports: true,
267
- assetFileNames: 'style.css',
212
+ // Build all resources (but don't copy yet)
213
+ for (let i = 0; i < resourceFiles.length; i++) {
214
+ const { componentName, componentFile, kebabName, entry, jsOutput, buildOutDir } = resourceFiles[i];
215
+ log(`[${i + 1}/${resourceFiles.length}] Building ${kebabName}...`);
216
+
217
+ try {
218
+ // Create build directory if it doesn't exist
219
+ if (!existsSync(buildOutDir)) {
220
+ mkdirSync(buildOutDir, { recursive: true });
221
+ }
222
+
223
+ // Create entry file from template in temp directory
224
+ const entryContent = template
225
+ .replace('// RESOURCE_IMPORT', `import { ${componentName}, resource } from '../src/resources/${kebabName}/${componentFile}';`)
226
+ .replace('// RESOURCE_MOUNT', `createRoot(root).render(<AppProvider appInfo={{ name: ${JSON.stringify(appName)}, version: ${JSON.stringify(appVersion)} }}><${componentName} /></AppProvider>);`);
227
+
228
+ const entryPath = path.join(projectRoot, entry);
229
+ writeFileSync(entryPath, entryContent);
230
+
231
+ // Build with vite programmatically
232
+ await viteBuild({
233
+ mode: 'production',
234
+ root: projectRoot,
235
+ ...(quiet && { logLevel: 'silent' }),
236
+ plugins: [react(), tailwindcss(), inlineCssPlugin(buildOutDir)],
237
+ define: {
238
+ 'process.env.NODE_ENV': JSON.stringify('production'),
268
239
  },
269
- },
270
- minify: true,
271
- cssMinify: true,
272
- },
273
- });
274
- } catch (error) {
275
- console.error(`Failed to build ${kebabName}`);
276
- console.error(error);
277
- process.exit(1);
278
- }
279
- }
240
+ resolve: {
241
+ conditions: ['style', 'import', 'module', 'browser', 'default'],
242
+ alias: {
243
+ '@': path.resolve(projectRoot, 'src'),
244
+ // In workspace dev mode, use local sunpeak source
245
+ ...(isTemplate && {
246
+ sunpeak: parentSrc,
247
+ }),
248
+ },
249
+ },
250
+ build: {
251
+ target: 'es2020',
252
+ outDir: buildOutDir,
253
+ emptyOutDir: true,
254
+ cssCodeSplit: false,
255
+ lib: {
256
+ entry: entryPath,
257
+ name: 'SunpeakApp',
258
+ formats: ['iife'],
259
+ fileName: () => jsOutput,
260
+ },
261
+ rollupOptions: {
262
+ output: {
263
+ inlineDynamicImports: true,
264
+ assetFileNames: 'style.css',
265
+ },
266
+ },
267
+ minify: true,
268
+ cssMinify: true,
269
+ },
270
+ });
271
+ } catch (error) {
272
+ console.error(`Failed to build ${kebabName}`);
273
+ console.error(error);
274
+ process.exit(1);
275
+ }
276
+ }
280
277
 
281
- // Now copy all files from build-output to dist/{resource}/
282
- log('\nCopying built files to dist/...');
283
- const timestamp = Date.now().toString(36);
278
+ // Now copy all files from build-output to dist/{resource}/
279
+ log('\nCopying built files to dist/...');
280
+ const timestamp = Date.now().toString(36);
284
281
 
285
- for (const { jsOutput, htmlOutput, buildOutDir, distOutDir, kebabName, componentFile, resourceDir } of resourceFiles) {
286
- // Create resource-specific output directory
287
- if (!existsSync(distOutDir)) {
288
- mkdirSync(distOutDir, { recursive: true });
289
- }
282
+ for (const { jsOutput, htmlOutput, buildOutDir, distOutDir, kebabName, componentFile, resourceDir } of resourceFiles) {
283
+ // Create resource-specific output directory
284
+ if (!existsSync(distOutDir)) {
285
+ mkdirSync(distOutDir, { recursive: true });
286
+ }
290
287
 
291
- // Extract resource metadata from .tsx file and write as JSON
292
- const srcTsx = path.join(resourceDir, `${componentFile}.tsx`);
293
- const destJson = path.join(distOutDir, `${kebabName}.json`);
294
-
295
- const meta = await extractResourceExport(srcTsx);
296
- // Inject name from directory key if not explicitly set
297
- meta.name = meta.name ?? kebabName;
298
- // Generate URI using resource name and build timestamp
299
- meta.uri = `ui://${meta.name}-${timestamp}`;
300
- writeFileSync(destJson, JSON.stringify(meta, null, 2));
301
- log(`✓ Generated ${kebabName}/${kebabName}.json (uri: ${meta.uri})`);
302
-
303
- // Read built JS file and wrap in HTML shell
304
- const builtJsFile = path.join(buildOutDir, jsOutput);
305
- const destHtmlFile = path.join(distOutDir, htmlOutput);
306
-
307
- if (existsSync(builtJsFile)) {
308
- const jsContents = readFileSync(builtJsFile, 'utf-8');
309
- const html = `<!DOCTYPE html>
288
+ // Extract resource metadata from .tsx file and write as JSON
289
+ const srcTsx = path.join(resourceDir, `${componentFile}.tsx`);
290
+ const destJson = path.join(distOutDir, `${kebabName}.json`);
291
+
292
+ const meta = await extractResourceExport(srcTsx);
293
+ // Inject name from directory key if not explicitly set
294
+ meta.name = meta.name ?? kebabName;
295
+ // Generate URI using resource name and build timestamp
296
+ meta.uri = `ui://${meta.name}-${timestamp}`;
297
+ writeFileSync(destJson, JSON.stringify(meta, null, 2));
298
+ log(`✓ Generated ${kebabName}/${kebabName}.json (uri: ${meta.uri})`);
299
+
300
+ // Read built JS file and wrap in HTML shell
301
+ const builtJsFile = path.join(buildOutDir, jsOutput);
302
+ const destHtmlFile = path.join(distOutDir, htmlOutput);
303
+
304
+ if (existsSync(builtJsFile)) {
305
+ const jsContents = readFileSync(builtJsFile, 'utf-8');
306
+ const html = `<!DOCTYPE html>
310
307
  <html>
311
308
  <head>
312
309
  <meta charset="UTF-8">
@@ -319,32 +316,34 @@ ${jsContents}
319
316
  </script>
320
317
  </body>
321
318
  </html>`;
322
- writeFileSync(destHtmlFile, html);
323
- log(`✓ Built ${kebabName}/${htmlOutput}`);
324
- } else {
325
- console.error(`Built file not found: ${builtJsFile}`);
326
- if (existsSync(buildOutDir)) {
327
- log(` Files in ${buildOutDir}:`, readdirSync(buildOutDir));
328
- } else {
329
- log(` Build directory doesn't exist: ${buildOutDir}`);
330
- }
331
- process.exit(1);
332
- }
319
+ writeFileSync(destHtmlFile, html);
320
+ log(`✓ Built ${kebabName}/${htmlOutput}`);
321
+ } else {
322
+ console.error(`Built file not found: ${builtJsFile}`);
323
+ if (existsSync(buildOutDir)) {
324
+ log(` Files in ${buildOutDir}:`, readdirSync(buildOutDir));
325
+ } else {
326
+ log(` Build directory doesn't exist: ${buildOutDir}`);
327
+ }
328
+ process.exit(1);
329
+ }
333
330
 
334
- }
331
+ }
335
332
 
336
- // Clean up temp and build directories
337
- if (existsSync(tempDir)) {
338
- rmSync(tempDir, { recursive: true });
339
- }
340
- if (existsSync(buildDir)) {
341
- rmSync(buildDir, { recursive: true });
342
- }
333
+ // Clean up temp and build directories
334
+ if (existsSync(tempDir)) {
335
+ rmSync(tempDir, { recursive: true });
336
+ }
337
+ if (existsSync(buildDir)) {
338
+ rmSync(buildDir, { recursive: true });
339
+ }
343
340
 
344
- log('\n✓ All resources built successfully!');
345
- log('\nBuilt resources:');
346
- for (const { kebabName } of resourceFiles) {
347
- log(` ${kebabName}`);
341
+ log('\n✓ All resources built successfully!');
342
+ log('\nBuilt resources:');
343
+ for (const { kebabName } of resourceFiles) {
344
+ log(` ${kebabName}`);
345
+ }
346
+ }
348
347
  }
349
348
 
350
349
  // ========================================================================
@@ -356,7 +355,7 @@ ${jsContents}
356
355
 
357
356
  // Find tool files
358
357
  const toolFiles = existsSync(toolsDir)
359
- ? readdirSync(toolsDir).filter(f => f.endsWith('.ts'))
358
+ ? readdirSync(toolsDir).filter(f => f.endsWith('.ts') && !f.endsWith('.test.ts'))
360
359
  : [];
361
360
 
362
361
  const hasServerEntry = existsSync(serverEntryPath);