@linktr.ee/linkapp 0.0.48 → 0.0.49

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 (203) hide show
  1. package/README.md +1 -1
  2. package/dev-server/components/form/array-field.tsx +115 -0
  3. package/dev-server/components/form/file-field.tsx +48 -0
  4. package/dev-server/components/form/form-element.tsx +304 -0
  5. package/dev-server/components/form/link-behavior-field.tsx +68 -0
  6. package/dev-server/components/form/location-field.tsx +60 -0
  7. package/dev-server/components/settings-preview.tsx +138 -302
  8. package/dev-server/components/ui/checkbox.tsx +29 -0
  9. package/dev-server/components/ui/dialog.tsx +2 -10
  10. package/dev-server/components/ui/field.tsx +24 -49
  11. package/dev-server/components/ui/input.tsx +20 -21
  12. package/dev-server/components/ui/label.tsx +4 -4
  13. package/dev-server/components/ui/radio-group.tsx +37 -0
  14. package/dev-server/components/ui/select.tsx +153 -0
  15. package/dev-server/components/ui/switch.tsx +31 -0
  16. package/dev-server/components/ui/tabs.tsx +1 -1
  17. package/dev-server/components/ui/textarea.tsx +18 -19
  18. package/dev-server/env.d.ts +4 -1
  19. package/dev-server/expanded/main.tsx +20 -22
  20. package/dev-server/expanded.html +0 -1
  21. package/dev-server/featured/main.tsx +29 -36
  22. package/dev-server/featured-carousel.html +0 -1
  23. package/dev-server/featured.html +0 -1
  24. package/dev-server/index.html +1 -7
  25. package/dev-server/lib/utils.ts +3 -3
  26. package/dev-server/package.json +3 -3
  27. package/dev-server/postcss/tailwind-source-fallback.js +2 -7
  28. package/dev-server/postcss.config.mjs +2 -2
  29. package/dev-server/preview/Preview.tsx +310 -350
  30. package/dev-server/preview/main.tsx +8 -8
  31. package/dev-server/preview/preview.css +0 -1
  32. package/dev-server/public/site.webmanifest +1 -1
  33. package/dev-server/rsbuild.config.ts +1 -1
  34. package/dev-server/shared/dev-parent-simulator.ts +219 -0
  35. package/dev-server/shared/theme-presets.ts +71 -75
  36. package/dev-server/shared/theme-utils.ts +11 -11
  37. package/dist/cli.js +18 -12
  38. package/dist/cli.js.map +1 -1
  39. package/dist/commands/add.d.ts.map +1 -1
  40. package/dist/commands/add.js +27 -42
  41. package/dist/commands/add.js.map +1 -1
  42. package/dist/commands/build.d.ts.map +1 -1
  43. package/dist/commands/build.js +26 -16
  44. package/dist/commands/build.js.map +1 -1
  45. package/dist/commands/deploy.d.ts +1 -11
  46. package/dist/commands/deploy.d.ts.map +1 -1
  47. package/dist/commands/deploy.js +3 -13
  48. package/dist/commands/deploy.js.map +1 -1
  49. package/dist/commands/dev.d.ts.map +1 -1
  50. package/dist/commands/dev.js +132 -388
  51. package/dist/commands/dev.js.map +1 -1
  52. package/dist/commands/login.d.ts.map +1 -1
  53. package/dist/commands/login.js +17 -29
  54. package/dist/commands/login.js.map +1 -1
  55. package/dist/commands/logout.d.ts.map +1 -1
  56. package/dist/commands/logout.js +6 -11
  57. package/dist/commands/logout.js.map +1 -1
  58. package/dist/commands/rollback.d.ts +10 -0
  59. package/dist/commands/rollback.d.ts.map +1 -0
  60. package/dist/commands/rollback.js +148 -0
  61. package/dist/commands/rollback.js.map +1 -0
  62. package/dist/commands/status.d.ts +8 -0
  63. package/dist/commands/status.d.ts.map +1 -0
  64. package/dist/commands/status.js +96 -0
  65. package/dist/commands/status.js.map +1 -0
  66. package/dist/commands/test-url-match-rules.d.ts.map +1 -1
  67. package/dist/commands/test-url-match-rules.js +20 -26
  68. package/dist/commands/test-url-match-rules.js.map +1 -1
  69. package/dist/lib/auth/device-flow.d.ts +1 -1
  70. package/dist/lib/auth/device-flow.d.ts.map +1 -1
  71. package/dist/lib/auth/device-flow.js +3 -3
  72. package/dist/lib/auth/device-flow.js.map +1 -1
  73. package/dist/lib/auth/token-storage.d.ts.map +1 -1
  74. package/dist/lib/auth/token-storage.js +14 -37
  75. package/dist/lib/auth/token-storage.js.map +1 -1
  76. package/dist/lib/build/detect-layouts.d.ts.map +1 -1
  77. package/dist/lib/build/detect-layouts.js +27 -13
  78. package/dist/lib/build/detect-layouts.js.map +1 -1
  79. package/dist/lib/config/load-config.d.ts.map +1 -1
  80. package/dist/lib/config/load-config.js +0 -2
  81. package/dist/lib/config/load-config.js.map +1 -1
  82. package/dist/lib/deploy/deploy-output.d.ts +2 -1
  83. package/dist/lib/deploy/deploy-output.d.ts.map +1 -1
  84. package/dist/lib/deploy/deploy-output.js +9 -1
  85. package/dist/lib/deploy/deploy-output.js.map +1 -1
  86. package/dist/lib/deploy/deploy-phases.d.ts +2 -0
  87. package/dist/lib/deploy/deploy-phases.d.ts.map +1 -1
  88. package/dist/lib/deploy/deploy-phases.js +9 -23
  89. package/dist/lib/deploy/deploy-phases.js.map +1 -1
  90. package/dist/lib/deploy/deploy-utils.d.ts +15 -7
  91. package/dist/lib/deploy/deploy-utils.d.ts.map +1 -1
  92. package/dist/lib/deploy/deploy-utils.js +49 -36
  93. package/dist/lib/deploy/deploy-utils.js.map +1 -1
  94. package/dist/lib/deploy/generate-manifest-files.d.ts.map +1 -1
  95. package/dist/lib/deploy/generate-manifest-files.js +13 -39
  96. package/dist/lib/deploy/generate-manifest-files.js.map +1 -1
  97. package/dist/lib/deploy/pack-project.d.ts.map +1 -1
  98. package/dist/lib/deploy/pack-project.js +34 -20
  99. package/dist/lib/deploy/pack-project.js.map +1 -1
  100. package/dist/lib/deploy/slot-manager.d.ts +54 -0
  101. package/dist/lib/deploy/slot-manager.d.ts.map +1 -0
  102. package/dist/lib/deploy/slot-manager.js +72 -0
  103. package/dist/lib/deploy/slot-manager.js.map +1 -0
  104. package/dist/lib/deploy/test-url-match-rules.d.ts +10 -2
  105. package/dist/lib/deploy/test-url-match-rules.d.ts.map +1 -1
  106. package/dist/lib/deploy/test-url-match-rules.js +1 -1
  107. package/dist/lib/deploy/test-url-match-rules.js.map +1 -1
  108. package/dist/lib/deploy/upload.d.ts +1 -0
  109. package/dist/lib/deploy/upload.d.ts.map +1 -1
  110. package/dist/lib/deploy/upload.js +15 -24
  111. package/dist/lib/deploy/upload.js.map +1 -1
  112. package/dist/lib/deploy/validation.d.ts.map +1 -1
  113. package/dist/lib/deploy/validation.js +43 -48
  114. package/dist/lib/deploy/validation.js.map +1 -1
  115. package/dist/lib/rsbuild/config-factory.d.ts.map +1 -1
  116. package/dist/lib/rsbuild/config-factory.js +10 -17
  117. package/dist/lib/rsbuild/config-factory.js.map +1 -1
  118. package/dist/lib/rsbuild/plugins/asset-versioning.d.ts.map +1 -1
  119. package/dist/lib/rsbuild/plugins/asset-versioning.js +4 -14
  120. package/dist/lib/rsbuild/plugins/asset-versioning.js.map +1 -1
  121. package/dist/lib/rsbuild/plugins/brotli-compression.d.ts.map +1 -1
  122. package/dist/lib/rsbuild/plugins/brotli-compression.js +4 -4
  123. package/dist/lib/rsbuild/plugins/brotli-compression.js.map +1 -1
  124. package/dist/lib/rsbuild/plugins/copy-public.d.ts.map +1 -1
  125. package/dist/lib/rsbuild/plugins/copy-public.js.map +1 -1
  126. package/dist/lib/rsbuild/postcss/tailwind-source-fallback.d.ts.map +1 -1
  127. package/dist/lib/rsbuild/postcss/tailwind-source-fallback.js +1 -3
  128. package/dist/lib/rsbuild/postcss/tailwind-source-fallback.js.map +1 -1
  129. package/dist/lib/utils/console.d.ts +8 -0
  130. package/dist/lib/utils/console.d.ts.map +1 -0
  131. package/dist/lib/utils/console.js +10 -0
  132. package/dist/lib/utils/console.js.map +1 -0
  133. package/dist/lib/utils/filesystem.d.ts +9 -0
  134. package/dist/lib/utils/filesystem.d.ts.map +1 -0
  135. package/dist/lib/utils/filesystem.js +30 -0
  136. package/dist/lib/utils/filesystem.js.map +1 -0
  137. package/dist/lib/utils/formatters.d.ts +8 -0
  138. package/dist/lib/utils/formatters.d.ts.map +1 -0
  139. package/dist/lib/utils/formatters.js +22 -0
  140. package/dist/lib/utils/formatters.js.map +1 -0
  141. package/dist/lib/utils/index.d.ts +7 -0
  142. package/dist/lib/utils/index.d.ts.map +1 -0
  143. package/dist/lib/utils/index.js +7 -0
  144. package/dist/lib/utils/index.js.map +1 -0
  145. package/dist/lib/utils/setup-runtime.d.ts.map +1 -1
  146. package/dist/lib/utils/setup-runtime.js +22 -63
  147. package/dist/lib/utils/setup-runtime.js.map +1 -1
  148. package/dist/schema/config.schema.d.ts +9 -48
  149. package/dist/schema/config.schema.d.ts.map +1 -1
  150. package/dist/schema/config.schema.js +119 -120
  151. package/dist/schema/config.schema.js.map +1 -1
  152. package/dist/sdk/hooks/mocks.d.ts +9 -0
  153. package/dist/sdk/hooks/mocks.d.ts.map +1 -0
  154. package/dist/sdk/hooks/mocks.js +17 -0
  155. package/dist/sdk/hooks/mocks.js.map +1 -0
  156. package/dist/sdk/hooks/use-audience-manager.d.ts +44 -0
  157. package/dist/sdk/hooks/use-audience-manager.d.ts.map +1 -0
  158. package/dist/sdk/hooks/use-audience-manager.js +109 -0
  159. package/dist/sdk/hooks/use-audience-manager.js.map +1 -0
  160. package/dist/sdk/hooks/use-ip.d.ts +45 -0
  161. package/dist/sdk/hooks/use-ip.d.ts.map +1 -0
  162. package/dist/sdk/hooks/use-ip.js +46 -0
  163. package/dist/sdk/hooks/use-ip.js.map +1 -0
  164. package/dist/sdk/hooks/use-sdk-request.d.ts +46 -0
  165. package/dist/sdk/hooks/use-sdk-request.d.ts.map +1 -0
  166. package/dist/sdk/hooks/use-sdk-request.js +65 -0
  167. package/dist/sdk/hooks/use-sdk-request.js.map +1 -0
  168. package/dist/sdk/hooks/use-theme.d.ts +45 -0
  169. package/dist/sdk/hooks/use-theme.d.ts.map +1 -0
  170. package/dist/sdk/hooks/use-theme.js +97 -0
  171. package/dist/sdk/hooks/use-theme.js.map +1 -0
  172. package/dist/sdk/hooks/use-visitor.d.ts +41 -0
  173. package/dist/sdk/hooks/use-visitor.d.ts.map +1 -0
  174. package/dist/sdk/hooks/use-visitor.js +42 -0
  175. package/dist/sdk/hooks/use-visitor.js.map +1 -0
  176. package/dist/sdk/hooks/validation.d.ts +8 -0
  177. package/dist/sdk/hooks/validation.d.ts.map +1 -0
  178. package/dist/sdk/hooks/validation.js +13 -0
  179. package/dist/sdk/hooks/validation.js.map +1 -0
  180. package/dist/sdk/index.d.ts +17 -5
  181. package/dist/sdk/index.d.ts.map +1 -1
  182. package/dist/sdk/index.js +16 -5
  183. package/dist/sdk/index.js.map +1 -1
  184. package/dist/sdk/message-bus.d.ts +59 -0
  185. package/dist/sdk/message-bus.d.ts.map +1 -0
  186. package/dist/sdk/message-bus.js +152 -0
  187. package/dist/sdk/message-bus.js.map +1 -0
  188. package/dist/sdk/messages.d.ts +121 -0
  189. package/dist/sdk/messages.d.ts.map +1 -0
  190. package/dist/sdk/messages.js +9 -0
  191. package/dist/sdk/messages.js.map +1 -0
  192. package/dist/sdk/send-message.d.ts +1 -1
  193. package/dist/sdk/send-message.js +18 -18
  194. package/dist/sdk/send-message.js.map +1 -1
  195. package/dist/sdk/use-expand-link-app.d.ts +3 -3
  196. package/dist/sdk/use-expand-link-app.d.ts.map +1 -1
  197. package/dist/sdk/use-expand-link-app.js +9 -5
  198. package/dist/sdk/use-expand-link-app.js.map +1 -1
  199. package/dist/types.d.ts +235 -55
  200. package/dist/types.d.ts.map +1 -1
  201. package/dist/types.js +8 -3
  202. package/dist/types.js.map +1 -1
  203. package/package.json +3 -9
@@ -1,16 +1,13 @@
1
1
  import pc from 'picocolors';
2
2
  import { createRsbuild, logger } from '@rsbuild/core';
3
3
  import { pluginReact } from '@rsbuild/plugin-react';
4
- import { dirname, resolve, join, extname, relative, sep } from 'node:path';
4
+ import { dirname, join, relative, resolve } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { validateLayouts, detectLayouts } from '../lib/build/detect-layouts.js';
7
7
  import { loadConfig } from '../lib/config/load-config.js';
8
8
  import { detect } from 'detect-port';
9
- import { existsSync, readFileSync, statSync, writeFileSync, mkdirSync } from 'node:fs';
9
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
10
10
  import chokidar from 'chokidar';
11
- const writeLine = (message = '') => {
12
- process.stdout.write(`${message}\n`);
13
- };
14
11
  const __filename = fileURLToPath(import.meta.url);
15
12
  const __dirname = dirname(__filename);
16
13
  /**
@@ -34,7 +31,7 @@ function createConfigWatcherPlugin(userProjectPath, onConfigChange) {
34
31
  ignorePermissionErrors: true,
35
32
  });
36
33
  const onChange = async (filePath) => {
37
- writeLine(pc.cyan(`\n ○ ${relative(userProjectPath, filePath)} changed, restarting dev server...\n`));
34
+ console.log(pc.cyan(`\n ○ ${relative(userProjectPath, filePath)} changed, restarting dev server...\n`));
38
35
  await onConfigChange();
39
36
  };
40
37
  watcher.on('change', onChange);
@@ -48,293 +45,79 @@ function createConfigWatcherPlugin(userProjectPath, onConfigChange) {
48
45
  };
49
46
  }
50
47
  /**
51
- * Creates middleware to serve static assets from the user's public directory
52
- * while matching the legacy dev server behavior.
48
+ * Serves app/icon.svg from user's project at /app-icon.svg
53
49
  */
54
- function createPublicDirPlugin(userPublicDir) {
55
- const contentTypes = {
56
- css: 'text/css',
57
- gif: 'image/gif',
58
- html: 'text/html',
59
- ico: 'image/x-icon',
60
- jpeg: 'image/jpeg',
61
- jpg: 'image/jpeg',
62
- js: 'application/javascript',
63
- json: 'application/json',
64
- png: 'image/png',
65
- svg: 'image/svg+xml',
66
- txt: 'text/plain',
67
- webp: 'image/webp',
68
- };
50
+ function createAppIconPlugin(userProjectPath) {
69
51
  return {
70
- name: 'linkapp:public-dir',
52
+ name: 'linkapp:app-icon',
71
53
  setup(api) {
72
54
  api.onBeforeStartDevServer(async ({ server }) => {
73
- if (!existsSync(userPublicDir)) {
74
- return;
75
- }
76
- const publicDirRoot = resolve(userPublicDir);
55
+ const iconPath = join(userProjectPath, 'app', 'icon.svg');
77
56
  server.middlewares.use((req, res, next) => {
78
- if (!req.url) {
79
- next();
80
- return;
81
- }
82
- const method = req.method ?? 'GET';
83
- if (method !== 'GET' && method !== 'HEAD') {
84
- next();
85
- return;
86
- }
87
- const [rawPath] = req.url.split('?');
88
- if (!rawPath || rawPath === '/' || rawPath === '') {
89
- next();
90
- return;
91
- }
92
- let decodedPath;
93
- try {
94
- decodedPath = decodeURIComponent(rawPath);
95
- }
96
- catch {
97
- next();
98
- return;
99
- }
100
- const normalizedPath = decodedPath.startsWith('/') ? decodedPath : `/${decodedPath}`;
101
- const targetPath = resolve(publicDirRoot, `.${normalizedPath}`);
102
- const relativePath = relative(publicDirRoot, targetPath);
103
- if (relativePath.startsWith('..') ||
104
- relativePath.includes(`..${sep}`) ||
105
- targetPath === publicDirRoot) {
106
- next();
107
- return;
108
- }
109
- let stats;
110
- try {
111
- stats = statSync(targetPath);
112
- }
113
- catch {
114
- next();
115
- return;
116
- }
117
- if (!stats.isFile()) {
118
- next();
57
+ if (req.url === '/app-icon.svg' && existsSync(iconPath)) {
58
+ res.setHeader('Content-Type', 'image/svg+xml');
59
+ res.setHeader('Cache-Control', 'no-cache');
60
+ res.end(readFileSync(iconPath));
119
61
  return;
120
62
  }
121
- const extension = extname(targetPath).slice(1).toLowerCase();
122
- const contentType = (extension && contentTypes[extension]) || 'application/octet-stream';
123
- res.setHeader('Content-Type', contentType);
124
- res.setHeader('Cache-Control', 'no-cache');
125
- if (method === 'HEAD') {
126
- res.end();
127
- return;
128
- }
129
- try {
130
- const content = readFileSync(targetPath);
131
- res.end(content);
132
- }
133
- catch (error) {
134
- next(error);
135
- }
63
+ next();
136
64
  });
137
65
  });
138
66
  },
139
67
  };
140
68
  }
141
69
  /**
142
- * Generates the expanded entry point dynamically
70
+ * Generates an entry point for layout components (expanded, featured, featured-carousel)
143
71
  */
144
- function generateExpandedEntryPoint(devServerPath, expandedImportPath) {
145
- // Use absolute paths to shared theme files in the dev-server directory
72
+ function generateEntryPoint(devServerPath, config) {
73
+ const { componentImport, componentName, includeFont = false, includeGroupLayoutOption = false } = config;
146
74
  const themePresetsPath = resolve(devServerPath, 'shared/theme-presets');
147
75
  const themeUtilsPath = resolve(devServerPath, 'shared/theme-utils');
148
- return `import Expanded from "${expandedImportPath}";
149
- import { StrictMode } from "react";
150
- import { createRoot } from "react-dom/client";
151
- import "@/app/globals.css";
152
- import { THEME_PRESETS } from "${themePresetsPath}";
153
- import { getThemeFromUrl, mergeThemeProps } from "${themeUtilsPath}";
154
-
155
- // Declare global window properties for theme and font application
156
- declare global {
157
- interface Window {
158
- __linkapp_applyTheme?: (variables: Record<string, string>) => void
159
- __linkapp_applyFont?: (fontData: {
160
- fontFamily: string
161
- fontStyle?: string
162
- cssUrl?: string
163
- }) => void
164
- }
165
- }
166
-
167
- // Preview props injected by dev server via Rsbuild define
168
- declare const __PREVIEW_PROPS__: Record<string, unknown>;
169
-
170
- // Extract just the variables from THEME_PRESETS for theme lookups
171
- const THEME_VARS = Object.fromEntries(
172
- Object.entries(THEME_PRESETS).map(([key, { variables }]) => [key, variables])
173
- );
174
-
175
- // Get theme variables from URL (defaults to 'default' theme)
176
- const themeVariables = getThemeFromUrl(THEME_VARS) || THEME_PRESETS.default.variables;
177
-
178
- // Get the selected theme key from URL or default to "default"
179
- const urlParams = new URLSearchParams(window.location.search);
180
- const themeKey = urlParams.get("theme") || "default";
181
- const selectedTheme = THEME_PRESETS[themeKey as keyof typeof THEME_PRESETS] || THEME_PRESETS.default;
182
-
183
- // Merge with preview props
184
- const previewProps = __PREVIEW_PROPS__ || {};
185
- const mergedProps = mergeThemeProps(themeVariables, previewProps);
186
-
187
- // Apply theme CSS variables on mount (always apply, even if default)
188
- if (window.__linkapp_applyTheme) {
189
- window.__linkapp_applyTheme(themeVariables);
190
- }
191
-
192
- // Apply font on mount
76
+ const fontDeclaration = includeFont
77
+ ? ' __linkapp_applyFont?: (fontData: { fontFamily: string; fontStyle?: string; cssUrl?: string }) => void'
78
+ : '';
79
+ const fontApplication = includeFont
80
+ ? `
193
81
  if (selectedTheme.font && window.__linkapp_applyFont) {
194
82
  window.__linkapp_applyFont(selectedTheme.font);
195
- }
196
-
197
- const rootElement = document.getElementById("root");
198
- if (!rootElement) {
199
- throw new Error("Root element not found");
200
- }
201
-
202
- createRoot(rootElement).render(
203
- <StrictMode>
204
- <Expanded {...mergedProps} />
205
- </StrictMode>,
206
- );
207
- `;
208
- }
209
- /**
210
- * Generates the featured-carousel entry point (always uses FeaturedCarousel component)
211
- */
212
- function generateFeaturedCarouselEntryPoint(devServerPath) {
213
- // Use absolute paths to shared theme files in the dev-server directory
214
- const themePresetsPath = resolve(devServerPath, 'shared/theme-presets');
215
- const themeUtilsPath = resolve(devServerPath, 'shared/theme-utils');
216
- return `import FeaturedCarousel from "@/app/featured-carousel";
217
- import { StrictMode } from "react";
218
- import { createRoot } from "react-dom/client";
219
- import "@/app/globals.css";
220
- import { THEME_PRESETS } from "${themePresetsPath}";
221
- import { getThemeFromUrl, mergeThemeProps } from "${themeUtilsPath}";
222
-
223
- // Declare global window property for theme application
224
- declare global {
225
- interface Window {
226
- __linkapp_applyTheme?: (variables: Record<string, string>) => void
227
- }
228
- }
229
-
230
- // Preview props injected by dev server via Rsbuild define
231
- declare const __PREVIEW_PROPS__: Record<string, unknown>;
232
-
233
- // Extract just the variables from THEME_PRESETS for theme lookups
234
- const THEME_VARS = Object.fromEntries(
235
- Object.entries(THEME_PRESETS).map(([key, { variables }]) => [key, variables])
236
- );
237
-
238
- // Get theme variables from URL
239
- const themeVariables = getThemeFromUrl(THEME_VARS) || THEME_PRESETS.default.variables;
240
-
241
- // Merge with preview props
242
- const previewProps = __PREVIEW_PROPS__ || {};
243
- const mergedProps = mergeThemeProps(themeVariables, previewProps);
244
-
245
- // Apply theme CSS variables on mount (always apply, even if default)
246
- if (window.__linkapp_applyTheme) {
247
- window.__linkapp_applyTheme(themeVariables);
248
- }
249
-
250
- const rootElement = document.getElementById("root");
251
- if (!rootElement) {
252
- throw new Error("Root element not found");
253
- }
254
-
255
- createRoot(rootElement).render(
256
- <StrictMode>
257
- <FeaturedCarousel {...mergedProps} />
258
- </StrictMode>,
259
- );
260
- `;
261
- }
262
- /**
263
- * Generates the featured entry point dynamically based on available layouts
264
- */
265
- function generateFeaturedEntryPoint(devServerPath, hasFeaturedCarousel) {
266
- // Use absolute paths to shared theme files in the dev-server directory
267
- const themePresetsPath = resolve(devServerPath, 'shared/theme-presets');
268
- const themeUtilsPath = resolve(devServerPath, 'shared/theme-utils');
269
- const featuredCarouselImport = hasFeaturedCarousel
270
- ? `import FeaturedCarousel from "@/app/featured-carousel";`
83
+ }`
271
84
  : '';
272
- const componentSelection = hasFeaturedCarousel
273
- ? `// Select the appropriate component based on groupLayoutOption parameter
274
- const LayoutComponent = (groupLayoutOptionParam === 'carousel' && FeaturedCarousel)
275
- ? FeaturedCarousel
276
- : Featured;`
277
- : `// Only Featured layout is available
278
- const LayoutComponent = Featured;`;
279
- return `import Featured from "@/app/featured";
280
- ${featuredCarouselImport}
281
- import { StrictMode } from "react";
85
+ return `import ${componentName} from "${componentImport}";
86
+ ${config.additionalImports || ''}import { StrictMode } from "react";
282
87
  import { createRoot } from "react-dom/client";
283
88
  import "@/app/globals.css";
284
89
  import { THEME_PRESETS } from "${themePresetsPath}";
285
90
  import { getThemeFromUrl, mergeThemeProps } from "${themeUtilsPath}";
286
91
 
287
- // Declare global window properties for theme and font application
288
92
  declare global {
289
93
  interface Window {
290
94
  __linkapp_applyTheme?: (variables: Record<string, string>) => void
291
- __linkapp_applyFont?: (fontData: {
292
- fontFamily: string
293
- fontStyle?: string
294
- cssUrl?: string
295
- }) => void
296
- }
95
+ ${fontDeclaration} }
297
96
  }
298
97
 
299
- // Preview props injected by dev server via Rsbuild define
300
98
  declare const __PREVIEW_PROPS__: Record<string, unknown>;
301
99
 
302
- // Extract just the variables from THEME_PRESETS for theme lookups
303
100
  const THEME_VARS = Object.fromEntries(
304
101
  Object.entries(THEME_PRESETS).map(([key, { variables }]) => [key, variables])
305
102
  );
306
103
 
307
- // Get theme variables and groupLayoutOption from URL
308
104
  const params = new URLSearchParams(window.location.search);
309
- const groupLayoutOptionParam = params.get('groupLayoutOption');
105
+ ${includeGroupLayoutOption ? `const groupLayoutOptionParam = params.get('groupLayoutOption');\n` : ''}const themeKey = params.get("theme") || "default";
310
106
  const themeVariables = getThemeFromUrl(THEME_VARS) || THEME_PRESETS.default.variables;
311
-
312
- // Get the selected theme for font application
313
- const themeKey = params.get("theme") || "default";
314
107
  const selectedTheme = THEME_PRESETS[themeKey as keyof typeof THEME_PRESETS] || THEME_PRESETS.default;
315
108
 
316
- // Merge with preview props and add groupLayoutOption
317
109
  const previewProps = __PREVIEW_PROPS__ || {};
318
- const mergedProps = mergeThemeProps(themeVariables, previewProps, {
319
- groupLayoutOption: groupLayoutOptionParam || undefined,
320
- });
110
+ const mergedProps = mergeThemeProps(themeVariables, previewProps${includeGroupLayoutOption ? ', { groupLayoutOption: groupLayoutOptionParam || undefined }' : ''});
321
111
 
322
- // Apply theme CSS variables on mount (always apply, even if default)
323
112
  if (window.__linkapp_applyTheme) {
324
113
  window.__linkapp_applyTheme(themeVariables);
325
114
  }
326
-
327
- // Apply font on mount
328
- if (selectedTheme.font && window.__linkapp_applyFont) {
329
- window.__linkapp_applyFont(selectedTheme.font);
330
- }
115
+ ${fontApplication}
331
116
 
332
117
  const rootElement = document.getElementById("root");
333
- if (!rootElement) {
334
- throw new Error("Root element not found");
335
- }
118
+ if (!rootElement) throw new Error("Root element not found");
336
119
 
337
- ${componentSelection}
120
+ ${config.componentSelection || `const LayoutComponent = ${componentName};`}
338
121
 
339
122
  createRoot(rootElement).render(
340
123
  <StrictMode>
@@ -353,41 +136,78 @@ export async function devCommand(options) {
353
136
  });
354
137
  try {
355
138
  const startTime = Date.now();
139
+ // Detect headless mode
140
+ const layoutDetection = detectLayouts(userProjectPath);
141
+ const isHeadless = layoutDetection.layouts.length === 0;
142
+ if (isHeadless) {
143
+ console.log(`
144
+ ${pc.cyan('📋 Headless LinkApp - No frontend to serve')}
145
+ ${pc.dim('This LinkApp only contains settings schema')}
146
+ `);
147
+ try {
148
+ const config = loadConfig(userProjectPath);
149
+ if (config.settings) {
150
+ console.log(`${pc.bold('Settings schema:')}
151
+ ${JSON.stringify(config.settings, null, 2)}
152
+ `);
153
+ }
154
+ }
155
+ catch (error) {
156
+ console.log(`${pc.yellow('⚠ Warning: Could not load config')}
157
+ ${pc.dim(` ${error instanceof Error ? error.message : error}`)}
158
+ `);
159
+ }
160
+ console.log(pc.dim('To develop UI, work in linktr.ee-profiles repo\n'));
161
+ return;
162
+ }
356
163
  // Validate layouts exist
357
164
  const validation = validateLayouts(userProjectPath);
358
165
  if (!validation.valid) {
359
- writeLine(pc.red('✗ Invalid project structure'));
166
+ console.log(pc.red('✗ Invalid project structure'));
360
167
  for (const error of validation.errors) {
361
- writeLine(pc.red(` ${error}`));
168
+ console.log(pc.red(` ${error}`));
362
169
  }
363
170
  process.exit(1);
364
171
  }
365
172
  // Get dev server path (needed for generating entry points and later for Rsbuild config)
366
173
  const devServerPath = resolve(__dirname, '../../dev-server');
367
- // Detect available layouts for dynamic entry generation
368
- const layoutDetection = detectLayouts(userProjectPath);
369
- const hasFeatured = layoutDetection.layouts.some(l => l.name === 'featured');
370
- const hasFeaturedCarousel = layoutDetection.layouts.some(l => l.name === 'featured-carousel');
371
- const expandedLayout = layoutDetection.layouts.find(l => l.name === 'expanded');
174
+ // Detect available layouts for dynamic entry generation (already detected above for headless check)
175
+ const hasFeatured = layoutDetection.layouts.some((l) => l.name === 'featured');
176
+ const hasFeaturedCarousel = layoutDetection.layouts.some((l) => l.name === 'featured-carousel');
177
+ const expandedLayout = layoutDetection.layouts.find((l) => l.name === 'expanded');
372
178
  const expandedImportPath = expandedLayout
373
179
  ? `@/app/${expandedLayout.fileName.replace(/\.(jsx?|tsx?)$/, '')}`
374
180
  : '@/app/expanded';
375
181
  // Generate dynamic entry points in .linkapp directory
376
- const linkappDir = join(userProjectPath, ".linkapp");
182
+ const linkappDir = join(userProjectPath, '.linkapp');
377
183
  if (!existsSync(linkappDir)) {
378
184
  mkdirSync(linkappDir, { recursive: true });
379
185
  }
380
- const expandedEntryPath = join(linkappDir, "dev-expanded-main.tsx");
381
- const featuredEntryPath = join(linkappDir, "dev-featured-main.tsx");
382
- const featuredCarouselEntryPath = join(linkappDir, "dev-featured-carousel-main.tsx");
383
- writeFileSync(expandedEntryPath, generateExpandedEntryPoint(devServerPath, expandedImportPath), "utf-8");
384
- // Only generate featured entry if the layout exists
186
+ const expandedEntryPath = join(linkappDir, 'dev-expanded-main.tsx');
187
+ const featuredEntryPath = join(linkappDir, 'dev-featured-main.tsx');
188
+ const featuredCarouselEntryPath = join(linkappDir, 'dev-featured-carousel-main.tsx');
189
+ writeFileSync(expandedEntryPath, generateEntryPoint(devServerPath, {
190
+ componentImport: expandedImportPath,
191
+ componentName: 'Expanded',
192
+ includeFont: true,
193
+ }), 'utf-8');
385
194
  if (hasFeatured) {
386
- writeFileSync(featuredEntryPath, generateFeaturedEntryPoint(devServerPath, hasFeaturedCarousel), "utf-8");
195
+ writeFileSync(featuredEntryPath, generateEntryPoint(devServerPath, {
196
+ componentImport: '@/app/featured',
197
+ componentName: 'Featured',
198
+ includeFont: true,
199
+ includeGroupLayoutOption: true,
200
+ additionalImports: hasFeaturedCarousel ? `import FeaturedCarousel from "@/app/featured-carousel";\n` : '',
201
+ componentSelection: hasFeaturedCarousel
202
+ ? `const LayoutComponent = (groupLayoutOptionParam === 'carousel' && FeaturedCarousel) ? FeaturedCarousel : Featured;`
203
+ : undefined,
204
+ }), 'utf-8');
387
205
  }
388
- // Only generate featured-carousel entry if the layout exists
389
206
  if (hasFeaturedCarousel) {
390
- writeFileSync(featuredCarouselEntryPath, generateFeaturedCarouselEntryPoint(devServerPath), "utf-8");
207
+ writeFileSync(featuredCarouselEntryPath, generateEntryPoint(devServerPath, {
208
+ componentImport: '@/app/featured-carousel',
209
+ componentName: 'FeaturedCarousel',
210
+ }), 'utf-8');
391
211
  }
392
212
  // Helper to load config (called on initial start and on config changes)
393
213
  const loadProjectConfig = () => {
@@ -396,82 +216,40 @@ export async function devCommand(options) {
396
216
  return {
397
217
  previewProps: config.preview_props || {},
398
218
  settingsConfig: config.settings || {},
219
+ manifestConfig: config.manifest || { name: 'LinkApp', tagline: '' },
399
220
  };
400
221
  }
401
222
  catch (error) {
402
- writeLine(pc.yellow('⚠ Warning: Could not load config, using default preview props'));
403
- writeLine(pc.dim(` ${error instanceof Error ? error.message : error}`));
223
+ console.log(pc.yellow('⚠ Warning: Could not load config, using default preview props'));
224
+ console.log(pc.dim(` ${error instanceof Error ? error.message : error}`));
404
225
  return {
405
226
  previewProps: {},
406
227
  settingsConfig: {},
228
+ manifestConfig: { name: 'LinkApp', tagline: '' },
407
229
  };
408
230
  }
409
231
  };
410
232
  // Load config initially
411
- let { previewProps, settingsConfig } = loadProjectConfig();
233
+ let { previewProps, settingsConfig, manifestConfig } = loadProjectConfig();
412
234
  const requestedPort = options.port || 3000;
413
235
  const availablePort = await detect(requestedPort);
414
236
  // Show warning if the requested port was occupied
415
237
  if (requestedPort !== availablePort) {
416
- writeLine(pc.yellow(` ⚠ Port ${requestedPort} is in use, using ${availablePort} instead`));
417
- writeLine();
238
+ console.log(pc.yellow(` ⚠ Port ${requestedPort} is in use, using ${availablePort} instead`));
239
+ console.log();
418
240
  }
419
241
  const port = availablePort;
420
242
  const userPublicDir = join(userProjectPath, 'public');
421
243
  const printReadyMessage = (readyTime, urls) => {
422
- writeLine();
423
- writeLine(pc.bold(pc.cyan(' LinkApp')) + pc.dim(' ready'));
424
- writeLine();
244
+ console.log();
245
+ console.log(pc.bold(pc.cyan(' LinkApp')) + pc.dim(' ready'));
246
+ console.log();
425
247
  if (urls.length > 0) {
426
- writeLine(` ${pc.green('➜')} ${pc.bold('Local:')} ${pc.cyan(urls[0])}`);
248
+ console.log(` ${pc.green('➜')} ${pc.bold('Local:')} ${pc.cyan(urls[0])}`);
427
249
  }
428
- writeLine();
429
- writeLine(pc.green(` ✓ Ready in ${readyTime}s`));
250
+ console.log();
251
+ console.log(pc.green(` ✓ Ready in ${readyTime}s`));
430
252
  };
431
- const setupKeyboardShortcuts = (handlers) => {
432
- if (!process.stdin.isTTY) {
433
- return () => { };
434
- }
435
- const handleData = (data) => {
436
- if (data === '\u0003') {
437
- void handlers.exit();
438
- return;
439
- }
440
- const key = data.trim().toLowerCase();
441
- if (key === '') {
442
- return;
443
- }
444
- switch (key) {
445
- case 'r':
446
- void handlers.restart();
447
- break;
448
- case 'u':
449
- handlers.showUrls();
450
- break;
451
- case 'c':
452
- handlers.clear();
453
- break;
454
- case 'q':
455
- void handlers.exit();
456
- break;
457
- default:
458
- break;
459
- }
460
- };
461
- process.stdin.setRawMode(true);
462
- process.stdin.resume();
463
- process.stdin.setEncoding('utf8');
464
- process.stdin.on('data', handleData);
465
- return () => {
466
- process.stdin.off('data', handleData);
467
- if (process.stdin.isTTY) {
468
- process.stdin.setRawMode(false);
469
- }
470
- process.stdin.pause();
471
- };
472
- };
473
- // Callback wrapper for config watcher (will be assigned after restartServer is defined)
474
- let restartCallback;
475
253
  // Helper to create Rsbuild instance with current config values
476
254
  const createRsbuildInstance = async () => {
477
255
  return await createRsbuild({
@@ -481,26 +259,24 @@ export async function devCommand(options) {
481
259
  },
482
260
  plugins: [
483
261
  pluginReact(),
484
- createPublicDirPlugin(userPublicDir),
485
- createConfigWatcherPlugin(userProjectPath, async () => {
486
- await restartCallback?.();
487
- }),
262
+ createAppIconPlugin(userProjectPath),
263
+ // Use lazy callback to avoid TDZ error - restartServer is defined after this function
264
+ createConfigWatcherPlugin(userProjectPath, () => restartServer()),
488
265
  ],
489
266
  source: {
490
267
  entry: {
491
- index: resolve(devServerPath, "preview/main.tsx"),
268
+ index: resolve(devServerPath, 'preview/main.tsx'),
492
269
  expanded: expandedEntryPath,
493
270
  sheet: expandedEntryPath, // Legacy alias for expanded layout
494
271
  ...(hasFeatured ? { featured: featuredEntryPath } : {}),
495
- ...(hasFeaturedCarousel
496
- ? { "featured-carousel": featuredCarouselEntryPath }
497
- : {}),
272
+ ...(hasFeaturedCarousel ? { 'featured-carousel': featuredCarouselEntryPath } : {}),
498
273
  },
499
274
  define: {
500
275
  __PREVIEW_PROPS__: JSON.stringify(previewProps),
501
276
  __SETTINGS_CONFIG__: JSON.stringify(settingsConfig),
277
+ __MANIFEST_CONFIG__: JSON.stringify(manifestConfig),
502
278
  __HAS_FEATURED__: JSON.stringify(hasFeatured),
503
- __HAS_CAROUSEL__: JSON.stringify(hasFeaturedCarousel),
279
+ __HAS_FEATURED_CAROUSEL__: JSON.stringify(hasFeaturedCarousel),
504
280
  },
505
281
  },
506
282
  resolve: {
@@ -510,6 +286,7 @@ export async function devCommand(options) {
510
286
  dedupe: ['react', 'react-dom'],
511
287
  },
512
288
  html: {
289
+ title: manifestConfig.name || 'LinkApp',
513
290
  template({ entryName }) {
514
291
  const templates = {
515
292
  index: resolve(devServerPath, 'index.html'),
@@ -535,24 +312,26 @@ export async function devCommand(options) {
535
312
  tools: {
536
313
  postcss: (opts) => {
537
314
  // Load PostCSS config from dev-server directory
538
- opts.postcssOptions = {
539
- ...opts.postcssOptions,
540
- config: resolve(devServerPath, 'postcss.config.mjs'),
541
- };
315
+ const postcssOptions = (opts.postcssOptions ?? {});
316
+ postcssOptions.config = resolve(devServerPath, 'postcss.config.mjs');
317
+ opts.postcssOptions = postcssOptions;
542
318
  return opts;
543
319
  },
544
320
  rspack: (config) => {
545
321
  // Add linkapp package's node_modules to module resolution paths
546
322
  // This allows dev-server files to import dependencies from @linktr.ee/linkapp
547
323
  // In workspaces, dependencies might be hoisted, so we check multiple locations
548
- config.resolve = config.resolve || {};
549
- const possibleNodeModulesPaths = [
550
- resolve(__dirname, '../../node_modules'), // Package root (when published standalone)
551
- resolve(__dirname, '../../../node_modules'), // Monorepo root (workspace)
552
- resolve(__dirname, '../../../../node_modules'), // Parent monorepo
553
- 'node_modules', // Current directory
324
+ config.resolve = config.resolve ?? {};
325
+ // Get the linkapp package root (from dist/commands/ go up 2 levels)
326
+ const linkappPackageRoot = resolve(__dirname, '../..');
327
+ const additionalNodeModulesPaths = [
328
+ resolve(linkappPackageRoot, 'node_modules'), // Package's own node_modules
329
+ resolve(linkappPackageRoot, '../node_modules'), // packages/node_modules (if hoisted to packages level)
330
+ resolve(linkappPackageRoot, '../../node_modules'), // Root monorepo node_modules
554
331
  ];
555
- config.resolve.modules = possibleNodeModulesPaths;
332
+ // Append to existing modules array rather than replacing
333
+ const existingModules = config.resolve.modules ?? ['node_modules'];
334
+ config.resolve.modules = [...additionalNodeModulesPaths, ...existingModules];
556
335
  return config;
557
336
  },
558
337
  },
@@ -562,87 +341,52 @@ export async function devCommand(options) {
562
341
  // Create initial Rsbuild instance
563
342
  let rsbuild = await createRsbuildInstance();
564
343
  let currentServer;
565
- let cleanupShortcuts;
566
344
  let isRestarting = false;
567
345
  let isShuttingDown = false;
568
- let currentUrls = [];
569
- let lastReadyTime = '';
570
- const startServer = async (startedAt) => {
571
- const result = await rsbuild.startDevServer();
572
- currentServer = result;
573
- currentUrls = result.urls;
574
- lastReadyTime = ((Date.now() - startedAt) / 1000).toFixed(1);
575
- printReadyMessage(lastReadyTime, currentUrls);
576
- };
577
346
  const restartServer = async () => {
578
347
  if (isShuttingDown || isRestarting || !currentServer) {
579
348
  return;
580
349
  }
581
350
  isRestarting = true;
582
- writeLine(pc.cyan(' ↻ Restarting dev server...'));
351
+ const restartStartTime = Date.now();
352
+ console.log(pc.cyan(' ↻ Restarting dev server...'));
583
353
  try {
584
- // Close current server
585
354
  await currentServer.server.close();
586
- // Reload config to get fresh preview props
587
355
  const freshConfig = loadProjectConfig();
588
356
  previewProps = freshConfig.previewProps;
589
357
  settingsConfig = freshConfig.settingsConfig;
590
- // Recreate Rsbuild instance with fresh config
358
+ manifestConfig = freshConfig.manifestConfig;
591
359
  rsbuild = await createRsbuildInstance();
592
- // Start new server
593
- const restartStart = Date.now();
594
- await startServer(restartStart);
360
+ currentServer = await rsbuild.startDevServer();
361
+ const restartTime = ((Date.now() - restartStartTime) / 1000).toFixed(1);
362
+ printReadyMessage(restartTime, currentServer.urls);
595
363
  }
596
364
  catch (error) {
597
365
  currentServer = undefined;
598
- writeLine(pc.red('✗ Failed to restart dev server'));
599
- writeLine(pc.dim(` ${error instanceof Error ? error.message : error}`));
366
+ console.log(pc.red('✗ Failed to restart dev server'));
367
+ console.log(pc.dim(` ${error instanceof Error ? error.message : error}`));
600
368
  }
601
369
  finally {
602
370
  isRestarting = false;
603
371
  }
604
372
  };
605
- const showUrls = () => {
606
- writeLine();
607
- if (currentUrls.length > 0) {
608
- writeLine(` ${pc.green('➜')} ${pc.bold('Local:')} ${pc.cyan(currentUrls[0])}`);
609
- }
610
- writeLine();
611
- };
612
- const clearConsole = () => {
613
- console.clear();
614
- if (lastReadyTime) {
615
- printReadyMessage(lastReadyTime, currentUrls);
616
- }
617
- };
618
373
  const shutdown = async () => {
619
- if (isShuttingDown) {
374
+ if (isShuttingDown)
620
375
  return;
621
- }
622
376
  isShuttingDown = true;
623
- cleanupShortcuts?.();
624
377
  try {
625
- if (currentServer) {
626
- await currentServer.server.close();
627
- }
378
+ await currentServer?.server.close();
628
379
  }
629
- catch (closeError) {
630
- writeLine(pc.red('✗ Error while closing dev server'));
631
- writeLine(pc.dim(` ${closeError instanceof Error ? closeError.message : closeError}`));
380
+ catch {
381
+ // Ignore close errors during shutdown
632
382
  }
633
383
  finally {
634
384
  process.exit(0);
635
385
  }
636
386
  };
637
- // Assign restart callback for config watcher
638
- restartCallback = restartServer;
639
- await startServer(startTime);
640
- cleanupShortcuts = setupKeyboardShortcuts({
641
- restart: restartServer,
642
- showUrls,
643
- clear: clearConsole,
644
- exit: shutdown,
645
- });
387
+ currentServer = await rsbuild.startDevServer();
388
+ const readyTime = ((Date.now() - startTime) / 1000).toFixed(1);
389
+ printReadyMessage(readyTime, currentServer.urls);
646
390
  process.on('SIGINT', () => {
647
391
  void shutdown();
648
392
  });
@@ -650,7 +394,7 @@ export async function devCommand(options) {
650
394
  await new Promise(() => { });
651
395
  }
652
396
  catch (error) {
653
- writeLine(pc.red('✗ Failed to start server'));
397
+ console.log(pc.red('✗ Failed to start server'));
654
398
  console.error(pc.red('✗ Error:'), error);
655
399
  process.exit(1);
656
400
  }